diff --git a/.gitignore b/.gitignore
index c1772d1..b114afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,9 @@ node
asciidoctor.css
README.html
built
+dump.rdb
+package-lock.json
+.classpath
+.project
+org.eclipse.*
+settings.json
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
index 5fd4d50..2cc7d4a 100644
Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index eb91947..642d572 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5245521..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-language: java
-jdk:
-- oraclejdk8
-cache:
- directories:
- - $HOME/.m2
-notifications:
- slack:
- secure: 126ELo8iNKUg7K2a3c27cfEN3tPluL3YuSe2I4GqViBIQKIlSSlVK/3OPQhWRk13OP/BuNB6QK/8/5QbRuWS4Bwbf3bvfLJBcpLpRprDLmejt99Gxsee6mDOHrhdnXstJkGrVdTY0O/Hwu5WxIh8T1oVbGM7H0dH1Vtnw92j5CiItR4gXyVLDwtYL9pbmRdYTLjTnSlwo+eAKH/fH2DUIq3IJ1ThidvFbb5ghe7cYQEfNr77CZpeg2ztXC3xGF3asJ+lTSdyPyZ3QJPVDOagfqwWmrjK7bnxw76VSIvP0wr9rofdijwEwgWaRY4KeUX+CNEGtpoRaDb4NzIbTZCo1ctz6hJue+Hc2nR9cchCgnDnWB7CGZwER0YvIbCJ8SMskC4sLAU1QXw6KKRP5jXH9O1BnCiZdHDFoqLgrm5w4bNNA2+2sP5mGbzpexGdyGyKOxbJKi1ZO5ogmAkz+uxwKtTdyyg9yl/w6eMYjxcYdsC4+OXuP/WCH0CrXqBJeEMaBt+RVOinVXSzwVl3tFOc5lLIbEJCRhjEWn3GYQoUkccxUySpZiIaIOqAwEyD67AsA/sGR9LNQTcgijRNkQiLK9srI09mcPE1X3JG1vrYjZXy+Ko1iXALpSsNmxcNf+24E22sVOEC+bT1+z5O6OBTXjtdfg4zRWB7XvMiHvSZUyE=
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..bafafc8
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,44 @@
+pipeline {
+ agent none
+
+ triggers {
+ pollSCM 'H/10 * * * *'
+ }
+
+ options {
+ disableConcurrentBuilds()
+ buildDiscarder(logRotator(numToKeepStr: '14'))
+ }
+
+ stages {
+ stage("test: baseline (jdk8)") {
+ agent {
+ docker {
+ image 'adoptopenjdk/openjdk8:latest'
+ args '-v $HOME/.m2:/tmp/jenkins-home/.m2'
+ }
+ }
+ options { timeout(time: 30, unit: 'MINUTES') }
+ steps {
+ sh 'test/run.sh'
+ }
+ }
+
+ }
+
+ post {
+ changed {
+ script {
+ slackSend(
+ color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger',
+ channel: '#sagan-content',
+ message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}")
+ emailext(
+ subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}",
+ mimeType: 'text/html',
+ recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']],
+ body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}")
+ }
+ }
+ }
+}
diff --git a/LICENSE.code.txt b/LICENSE.code.txt
index 36bd6bb..4b5cde9 100644
--- a/LICENSE.code.txt
+++ b/LICENSE.code.txt
@@ -6,7 +6,7 @@
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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,
diff --git a/LICENSE.writing.txt b/LICENSE.writing.txt
index 90247b3..9d21229 100644
--- a/LICENSE.writing.txt
+++ b/LICENSE.writing.txt
@@ -1 +1 @@
-Except where otherwise noted, this work is licensed under http://creativecommons.org/licenses/by-nd/3.0/
+Except where otherwise noted, this work is licensed under https://creativecommons.org/licenses/by-nd/3.0/
diff --git a/README.adoc b/README.adoc
index 5528e2f..ea7f320 100644
--- a/README.adoc
+++ b/README.adoc
@@ -1,7 +1,6 @@
----
-tags: [rest,hateoas,data,react,security]
-projects: [spring-data-rest,spring-data-jpa,spring-hateoas,spring-security,spring-boot,]
----
+:doctype: book
+:tags: [rest,hateoas,data,react,security]
+:projects: [spring-data-rest,spring-data-jpa,spring-hateoas,spring-security,spring-boot,]
:toc: left
:icons: font
:source-highlighter: prettify
@@ -9,10 +8,10 @@ projects: [spring-data-rest,spring-data-jpa,spring-hateoas,spring-security,sprin
= React.js and Spring Data REST
-This tutorial shows a collection of apps that use Spring Data REST and its powerful backend functionality combined with React's sophisticated features to build an easy-to-grok UI.
+This tutorial shows a collection of apps that use Spring Data REST and its powerful backend functionality, combined with React's sophisticated features to build an easy-to-understand UI.
* https://www.youtube.com/watch?v=TgCr7v9tdKM[Spring Data REST] provides a fast way to build hypermedia-powered repositories.
-* http://facebook.github.io/react/index.html[React] is Facebook's solution to efficient, fast, and easy-to-use views in the land of JavaScript.
+* https://facebook.github.io/react/index.html[React] is Facebook's solution to efficient, fast, and easy-to-use views in JavaScript.
include::basic/README.adoc[leveloffset=+1]
include::hypermedia/README.adoc[leveloffset=+1]
diff --git a/basic/.mvn/wrapper/MavenWrapperDownloader.java b/basic/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/basic/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/basic/.mvn/wrapper/maven-wrapper.jar b/basic/.mvn/wrapper/maven-wrapper.jar
index 5fd4d50..2cc7d4a 100644
Binary files a/basic/.mvn/wrapper/maven-wrapper.jar and b/basic/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/basic/.mvn/wrapper/maven-wrapper.properties b/basic/.mvn/wrapper/maven-wrapper.properties
index eb91947..642d572 100644
--- a/basic/.mvn/wrapper/maven-wrapper.properties
+++ b/basic/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/basic/README.adoc b/basic/README.adoc
index d648eec..8ca248e 100644
--- a/basic/README.adoc
+++ b/basic/README.adoc
@@ -1,114 +1,123 @@
[[react-and-spring-data-rest-part-1]]
-= Part 1 - Basic Features
+= Part 1 -- Basic Features
:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master
-Welcome Spring community,
+Welcome, Spring community.
-In this section, you will see how to get a bare-bones Spring Data REST application up and running quickly. Then you will build a simple UI on top of it using Facebook's React.js toolset.
+This section shows how to get a bare-bones Spring Data REST application up and running quickly. Then it shows how to build a simple UI on top of it by using Facebook's React.js toolset.
-== Step 0 - Setting up your environment
+== Step 0 -- Setting up Your Environment
Feel free to {sourcedir}/basic[grab the code] from this repository and follow along.
-If you want to do it yourself, visit http://start.spring.io and pick these items:
+If you want to do it yourself, visit https://start.spring.io and pick the following dependencies:
* Rest Repositories
* Thymeleaf
* JPA
* H2
-* Lombok (May want to ensure your IDE has support for this as well.)
-This demo uses Java 8, Maven Project, and the latest stable release of Spring Boot. It also uses React.js coded in http://es6-features.org/[ES6]. This will give you a clean, empty project. From there, you can add the various files shown explicitly in this section, and/or borrow from the repository listed above.
+This demo uses Java 8, Maven Project, and the latest stable release of Spring Boot. It also uses React.js coded in http://es6-features.org/[ES6]. This will give you a clean, empty project. From there, you can add the various files shown explicitly in this section and/or borrow from the repository listed earlier.
-== In the beginning...
+== In the Beginning...
-In the beginning there was data. And it was good. But then people wanted to access the data through various means. Over the years, people cobbled together lots of MVC controllers, many using Spring's powerful REST support. But doing over and over cost a lot of time.
+In the beginning, there was data. And it was good. But then people wanted to access the data through various means. Over the years, people cobbled together lots of MVC controllers, many using Spring's powerful REST support. But doing over and over cost a lot of time.
Spring Data REST addresses how simple this problem can be if some assumptions are made:
* The developer uses a Spring Data project that supports the repository model.
-* The system uses well accepted, industry standard protocols, like HTTP verbs, standardized media types, and IANA-approved link names.
+* The system uses well accepted, industry standard protocols, such as HTTP verbs, standardized media types, and https://www.iana.org/assignments/link-relations/link-relations.xhtml[IANA-approved link names].
=== Declaring your domain
-The cornerstone of any Spring Data REST-based application are the domain objects. For this section, you will build an application to track the employees for a company. Kick that off by creating a data type like this:
+Domain objects form the cornerstone of any Spring Data REST-based application. In this section, you will build an application to track the employees for a company. Kick that off by creating a data type, as follows:
.src/main/java/com/greglturnquist/payroll/Employee.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/Employee.java[tag=code]
----
-* `@Entity` is a JPA annotation that denotes the whole class for storage in a relational table.
-* `@Id` and `@GeneratedValue` are JPA annotations to note the primary key and that is generated automatically when needed.
-* `@Data` is a Project Lombok annotation to autogenerate getters, setters, constructors, toString, hash, equals, and other things. It cuts down on the boilerplate.
+<1> `@Entity` is a JPA annotation that denotes the whole class for storage in a relational table.
+<2> `@Id` and `@GeneratedValue` are JPA annotations to note the primary key and that is generated automatically when needed.
+====
-This entity is used to track employee information. In this case, their name and job description.
+This entity is used to track employee information -- in this case, their names and job descriptions.
-NOTE: Spring Data REST isn't confined to JPA. It supports many NoSQL data stores, but you won't be covering those here.
+NOTE: Spring Data REST is not confined to JPA. It supports many NoSQL data stores, though you will not see those in this tutorial. For more information, see https://spring.io/guides/gs/accessing-neo4j-data-rest/[Accessing Neo4j Data with REST], https://spring.io/guides/gs/accessing-data-rest/[Accessing JPA Data with REST], and https://spring.io/guides/gs/accessing-mongodb-data-rest/[Accessing MongoDB Data with REST].
-== Defining the repository
+== Defining the Repository
-Another key piece of a Spring Data REST application is to create a corresponding repository definition.
+Another key piece of a Spring Data REST application is a corresponding repository definition, as follows:
.src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/EmployeeRepository.java[tag=code]
----
-* The repository extends Spring Data Commons' `CrudRepository` and plugs in the type of the domain object and its primary key
+<1> The repository extends Spring Data Commons' `CrudRepository` and plugs in the type of the domain object and its primary key
+====
-That is all that is needed! In fact, you don't even have to annotate this if it's top-level and visible. If you use your IDE and open up `CrudRepository`, you'll find a fist full of pre-built methods already defined.
+That is all that is needed! In fact, you need not even annotate interface if it is top-level and visible. If you use your IDE and open up `CrudRepository`, you will find a collection of pre-defined methods.
-NOTE: You can define http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[your own repository] if you wish. Spring Data REST supports that as well.
+NOTE: You can define https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[your own repository] if you wish. Spring Data REST supports that as well.
-== Pre-loading the demo
+== Pre-loading the Demo
-To work with this application, you need to pre-load it with some data like this:
+To work with this application, you need to pre-load it with some data, as follows:
.src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[tag=code]
----
-* This class is marked with Spring's `@Component` annotation so that it is automatically picked up by `@SpringBootApplication`.
-* It implements Spring Boot's `CommandLineRunner` so that it gets run after all the beans are created and registered.
-* It uses constructor injection and autowiring to get Spring Data's automatically created `EmployeeRepository`.
-* The `run()` method is invoked with command line arguments, loading up your data.
+<1> This class is marked with Spring's `@Component` annotation so that it is automatically picked up by `@SpringBootApplication`.
+<2> It implements Spring Boot's `CommandLineRunner` so that it gets run after all the beans are created and registered.
+<3> It uses constructor injection and autowiring to get Spring Data's automatically created `EmployeeRepository`.
+<4> The `run()` method is invoked with command line arguments, loading up your data.
+====
-One of the biggest, most powerful features of Spring Data is its ability to write JPA queries for you. This not only cuts down on your development time, but also reduces the risk of bugs and errors. Spring Data http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.details[looks at the name of methods] in a repository class and figures out the operation you need including saving, deleting, and finding.
+One of the biggest, most powerful features of Spring Data is its ability to write JPA queries for you. This not only cuts down on your development time, but it also reduces the risk of bugs and errors. Spring Data https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.details[looks at the name of methods] in a repository class and figures out the operations you need, including saving, deleting, and finding.
That is how we can write an empty interface and inherit already built save, find, and delete operations.
-== Adjusting the root URI
+== Adjusting the Root URI
-By default, Spring Data REST hosts a root collection of links at `/`. Because you will host a web UI on the same path, you need to change the root URI.
+By default, Spring Data REST hosts a root collection of links at `/`. Because you will host a web UI on that path, you need to change the root URI, as follows:
.src/main/resources/application.properties
+====
----
include::src/main/resources/application.properties[]
----
+====
-== Launching the backend
+== Launching the Backend
-The last step needed to get a fully operational REST API off the ground is to write a `public static void main` using Spring Boot:
+The last step needed to get a fully operational REST API off the ground is to write a `public static void main` method by using Spring Boot, as follows:
.src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java[tag=code]
----
+====
-Assuming the previous class as well as your Maven build file were generated from http://start.spring.io, you can now launch it either by running that `main()` method inside your IDE, or type `./mvnw spring-boot:run` on the command line. (mvnw.bat for Windows users).
+Assuming the previous class as well as your Maven build file were generated from https://start.spring.io, you can now launch it either by running that `main()` method inside your IDE or by typing `./mvnw spring-boot:run` on the command line. (`mvnw.bat` for Windows users).
-NOTE: If you aren't up-to-date on Spring Boot and how it works, you should consider watch one of https://www.youtube.com/watch?v=sbPSjI4tt10[Josh Long's introductory presentations]. Did it? Press on!
+NOTE: If you are not up-to-date on Spring Boot and how it works, you should watch one of https://www.youtube.com/watch?v=sbPSjI4tt10[Josh Long's introductory presentations]. Did it? Press on!
-== Touring your REST service
+== Touring Your REST Service
-With the app running, you can check things out on the command line using http://curl.haxx.se/[cURL] (or any other tool you like).
+With the application running, you can check things out on the command line by using https://curl.haxx.se/[cURL] (or any other tool you like). The following command (shown with its output) lists the links in the application:
+====
----
$ curl localhost:8080/api
{
@@ -122,15 +131,17 @@ $ curl localhost:8080/api
}
}
----
+====
-When you ping the root node, you get back a collection of links wrapped up in a http://stateless.co/hal_specification.html[HAL-formatted JSON document].
+When you ping the root node, you get back a collection of links wrapped in a http://stateless.co/hal_specification.html[HAL-formatted JSON document].
-* *_links* is a the collection of links available.
-* *employees* points to an aggregate root for the employee objects defined by the `EmployeeRepository` interface.
-* *profile* is an IANA-standard relation and points to discoverable metadata about the entire service. We'll explore this in a later section.
+* `_links` is the collection of available links.
+* `employees` points to an aggregate root for the employee objects defined by the `EmployeeRepository` interface.
+* `profile` is an IANA-standard relation and points to discoverable metadata about the entire service. We explore this in a later section.
-You can further dig into this service by navigating the *employees* link.
+You can further dig into this service by navigating the `employees` link. The following command (shown with its output) does so:
+====
----
$ curl localhost:8080/api/employees
{
@@ -148,15 +159,17 @@ $ curl localhost:8080/api/employees
}
}
----
+====
At this stage, you are viewing the entire collection of employees.
-What's included along with the data you pre-loaded earlier is a *_links* attribute with a *self* link. This is the canonical link for that particular employee. What is canonical? It means free of context. For example, the same user could be fetched through a link like /api/orders/1/processor, in which the employee is assocated with processing a particular order. Here, there is no relationship to other entities.
+Along with the data you pre-loaded earlier, a `_links` attribute with a `self` link is included. This is the canonical link for that particular employee. What is canonical? It means "`free of context`". For example, the same user could be fetched through `/api/orders/1/processor`, in which the employee is associated with processing a particular order. Here, there is no relationship to other entities.
-IMPORTANT: Links are a critical facet of REST. They provide the power to navigate to related items. It makes it possible for other parties to navigate around your API without having to rewrite things everytime there is a change. Updates in the client is a common problem when the clients hard code paths to resources. Restructuring resources can cause big upheavals in code. If links are used and instead the navigation route is maintained, then it becomes easy and flexible to make such adjustments.
+IMPORTANT: Links are a critical facet of REST. They provide the power to navigate to related items. It makes it possible for other parties to navigate around your API without having to rewrite things every time there is a change. Updates in the client is a common problem when the clients hard-code paths to resources. Restructuring resources can cause big upheavals in code. If links are used and the navigation route is maintained, it becomes easy and flexible to make such adjustments.
-You can decide to view that one employee if you wish.
+You can decide to view that one employee if you wish. The following command (shown with its output) does so:
+====
----
$ curl localhost:8080/api/employees/1
{
@@ -170,13 +183,15 @@ $ curl localhost:8080/api/employees/1
}
}
----
+====
-Little change here, except that there is no need for the *_embedded* wrapper since there is only domain object.
+Little has changed here, except that there is no need for the `_embedded` wrapper since there is only the domain object.
-That's all and good, but you are probably itching to create some new entries.
+That is all well and good, but you are probably itching to create some new entries. The following command (shown with its output) does so:
+====
----
-$ curl -X POST localhost:8080/api/employees -d '{"firstName": "Bilbo", "lastName": "Baggins", "description": "burglar"}' -H 'Content-Type:application/json'
+$ curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
{
"firstName" : "Bilbo",
"lastName" : "Baggins",
@@ -188,123 +203,187 @@ $ curl -X POST localhost:8080/api/employees -d '{"firstName": "Bilbo", "lastName
}
}
----
+====
-You can also PUT, PATCH, and DELETE as shown in https://spring.io/guides/gs/accessing-data-rest/[this related guide]. But let's not dig into that. You have already spent way too much time interacting with this REST service manually. Don't you want to build a slick UI instead?
+You can also `PUT`, `PATCH`, and `DELETE`, as shown in https://spring.io/guides/gs/accessing-data-rest/[this related guide]. For now, though, we will move on to building a slick UI.
-== Setting up a custom UI controller
+== Setting up a Custom UI Controller
-Spring Boot makes it super simple to stand up a custom web page. First, you need a Spring MVC controller.
+Spring Boot makes it super simple to stand up a custom web page. First, you need a Spring MVC controller, as follows:
.src/main/java/com/greglturnquist/payroll/HomeController.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/HomeController.java[tag=code]
----
-* `@Controller` marks this class as a Spring MVC controller.
-* `@RequestMapping` flags the `index()` method to support the `/` route.
-* It returns `index` as the name of the template, which Spring Boot's autoconfigured view resolver will map to `src/main/resources/templates/index.html`.
+<1> `@Controller` marks this class as a Spring MVC controller.
+<2> `@RequestMapping` flags the `index()` method to support the `/` route.
+<3> It returns `index` as the name of the template, which Spring Boot's autoconfigured view resolver will map to `src/main/resources/templates/index.html`.
+====
== Defining an HTML template
-You are using Thymeleaf, although you won't really use many of its features.
+You are using Thymeleaf, although you will not really use many of its features. To get started you need an index page, as follows:
.src/main/resources/templates/index.html
+====
[source,html]
----
include::src/main/resources/templates/index.html[]
----
+====
The key part in this template is the `
` component in the middle. It is where you will direct React to plug in the rendered output.
-== Loading JavaScript modules
+You may also wonder where that `bundle.js` file came from. The way it is built is shown in the next section.
-This tutorial won't go into extensive detail on how it uses https://webpack.github.io/[webpack] to load JavaScript modules. But thanks to the *frontend-maven-plugin*, you don't _have_ to install any of the node.js tools to build and run the code.
+IMPORTANT: This tutorial does not show `main.css`, but you can see it linked up above. When it comes to CSS, Spring Boot will automatically serve anything found in `src/main/resources/static`. Put your own `main.css` file there. It is not shown in the tutorial, since our focus is on React and Spring Data REST, not CSS.
-The following JavaScript modules will be used:
+== Loading JavaScript Modules
-* webpack
-* babel
-* react.js
-* rest.js
+This section contains the barebones information to get the JavaScript bits off the ground. While you _can_ install all of JavaScripts command line tools, you need not do so -- at least, not yet. Instead, all you need to do is add the following to your `pom.xml` build file:
-With the power of babel, the JavaScript is written in ES6.
+.The `frontend-maven-plugin` used to build JavaScript bits
+====
+[source,xml,indent=0]
+----
+include::pom.xml[tag=frontend-maven-plugin]
+----
+====
-If you're interested, the paths for the JavaScript moodules are defined in https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/basic/src/main/resources/static/webpack.config.js[webpack.config.js]. This is then used by webpack to generate a JavaScript bundle, which is loaded inside the template.
+This little plugin perform multiple steps:
-NOTE: Want to see your JavaScript changes automatically? Move into the `src/main/resource/static`, and run `npm run-script watch` to put webpack into watch mode. It will regenerate bundle.js as you edit the source. Assuming you've http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping[setup your IDE properly], *spring-boot-devtools* combined with this should speed up changes.
+* The `install-node-and-npm` command will install node.js and its package management tool, `npm`, into the `target` folder. (This ensures the binaries are NOT pulled under source control and can be cleaned out with `clean`).
+* The `npm` command will execute the npm binary with the provided argument (`install`). This installs the modules defined in `package.json`.
+* The `webpack` command will execute webpack binary, which compiles all the JavaScript code based on `webpack.config.js`.
+
+These steps are run in sequence, essentially installing node.js, downloading JavaScript modules, and building the JS bits.
+
+What modules are installed? JavaScript developers typically use `npm` to build up a `package.json` file, such as the following:
+
+.package.json
+====
+[source,javascript]
+----
+include::package.json[]
+----
+====
-With all that in place, you can focus on the React bits which are fetched after the DOM is loaded. It's broken down into parts as below:
+Key dependencies include:
+
+* react.js: The toolkit used by this tutorial
+* rest.js: CujoJS toolkit used to make REST calls
+* webpack: A toolkit used to compile JavaScript components into a single, loadable bundle
+* babel: To write your JavaScript code using ES6 and compile it into ES5 to run in the browser
+
+To build the JavaScript code you'll use later, you need to define a build file for https://webpack.github.io/[webpack], as follows:
+
+.webpack.config.js
+====
+[source,javascript]
+----
+include::webpack.config.js[]
+----
+====
+
+This webpack configuration file:
+
+* Defines the *entry point* as `./src/main/js/app.js`. In essence, `app.js` (a module you will write shortly) is the proverbial `public static void main()` of our JavaScript application. `webpack` must know this in order to know _what_ to launch when the final bundle is loaded by the browser.
+* Creates *sourcemaps* so that, when you are debugging JS code in the browser, you can link back to original source code.
+* Compile ALL of the JavaScript bits into `./src/main/resources/static/built/bundle.js`, which is a JavaScript equivalent to a Spring Boot uber JAR. All your custom code AND the modules pulled in by the `require()` calls are stuffed into this file.
+* It hooks into the babel engine, using both `es2015` and `react` presets, in order to compile ES6 React code into a format able to be run in any standard browser.
+
+For more details on how each of these JavaScript tools operates, please read their corresponding reference docs.
+
+NOTE: Want to see your JavaScript changes automatically? Run `npm run-script watch` to put webpack into watch mode. It will regenerate `bundle.js` as you edit the source.
+
+With all that in place, you can focus on the React bits, which are fetched after the DOM is loaded. It is broken down into parts, as follows:
Since you are using webpack to assemble things, go ahead and fetch the modules you need:
-.src/main/resources/static/app.js
+.src/main/js/app.js
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=vars]
+include::src/main/js/app.js[tag=vars]
----
-* `React` is the main library from Facebook for building this app.
-* `client` is custom code that configures rest.js to include support for HAL, URI Templates, and other things. It also sets the default *Accept* request header to *application/hal+json*. You can https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/basic/src/main/resources/static/client.js[read the code here].
+<1> `React` is one of the main libraries from Facebook used to build this app.
+<2> `ReactDOM` is the package that serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package.
+<3> `client` is custom code that configures rest.js to include support for HAL, URI Templates, and other things. It also sets the default `Accept` request header to `application/hal+json`. You can https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/basic/src/main/js/client.js[read the code here].
+====
+
+IMPORTANT: The code for `client` is not shown because what you use to make REST calls is not important. Feel free to check the source, but the point is, you can plugin Restangular (or anything you like), and the concepts still apply.
== Diving into React
-React is based on defining components. Oftentimes, one component can hold multiple instances of another in a parent-child relationship. It's easy for this concept to extend several layers.
+React is based on defining components. Often, one component can hold multiple instances of another in a parent-child relationship. This concept can extend to several layers.
-To start things off, it's very handy to have a top level container for all components. (This will become more evident as you expand upon the code throughout this series.) Right now, you only have the employee list. But you might need some other related components later on, so let's start with this:
+To start things off, it is very handy to have a top level container for all components. (This will become more evident as you expand upon the code throughout this series.) Right now, you only have the employee list. But you might need some other related components later on, so start with the following:
-.src/main/resources/static/app.js - App component
+.src/main/js/app.js - App component
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=app]
+include::src/main/js/app.js[tag=app]
----
-* `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.
+<1> `class App extends React.Component{...}` is the method that creates a React component.
+<2> `componentDidMount` is the API invoked after React renders a component in the DOM.
+<3> `render` is the API that "`draws`" the component on the screen.
+====
NOTE: In React, uppercase is the convention for naming components.
-In the *App* component, an array of employees is fetched from the Spring Data REST backend and stored in this component's *state* data.
+In the `App` component, an array of employees is fetched from the Spring Data REST backend and stored in this component's `state` data.
[[NOTE]]
====
-React components have two types of data: *state* and *properties*.
-
-*State* is data that the component is expected to handle itself. It is also data that can fluctuate and change. To read the state, you use `this.state`. To update it, you use `this.setState()`. Every time `this.setState()` is called, React updates the state, calculates a diff between the previous state and the new state, and injects a set of changes to the DOM on the page. This results a fast and efficient updates to your UI.
+React components have two types of data: *state* and *properties*.
-The common convention is to initialize state with all your attributes empty in the constructor. Then you lookup data from the server using `componentDidMount` and populate your attributes. From there on, updates can be driven by user action or other events.
+*State* is data that the component is expected to handle itself. It is also data that can fluctuate and change. To read the state, you use `this.state`. To update it, you use `this.setState()`. Every time `this.setState()` is called, React updates the state, calculates a diff between the previous state and the new state, and injects a set of changes to the DOM on the page. This results in fast and efficient updates to your UI.
-*Properties* encompass data that is passed into the component. Properties do NOT change but are instead fixed values. To set them, you assign them to attributes when creating a new component and you'll soon see.
+The common convention is to initialize state with all your attributes empty in the constructor. Then you look up data from the server by using `componentDidMount` and populating your attributes. From there on, updates can be driven by user action or other events.
-WARNING: JavaScript doesn't lock down data structures like other languages. You can try to subvert properties by assigning values, but this doesn't work with React's differential engine and should be avoided.
+*Properties* encompass data that is passed into the component. Properties do NOT change but are instead fixed values. To set them, you assign them to attributes when creating a new component, as you will soon see.
====
-In this code, the function loads data via `client`, a https://promisesaplus.com/[Promise compliant] instance of rest.js. When it is done retrieving from `/api/employees`, it then invokes the function inside `done()` and set's the state based on it's HAL document (`response.entity._embedded.employees`). You might remember the structure of `curl /api/employees` <> and see how it maps onto this structure.
+WARNING: JavaScript does not lock down data structures as other languages do. You can try to subvert properties by assigning values, but this does not work with React's differential engine and should be avoided.
-When the state is updated, the `render()` function is invoked by the framework. The employee state data is included in creation of the `` React component as an input parameter.
+In this code, the function loads data through `client`, a https://promisesaplus.com/[Promise-compliant] instance of rest.js. When it is done retrieving from `/api/employees`, it then invokes the function inside `done()` and sets the state based on its HAL document (`response.entity._embedded.employees`). See the structure of `curl /api/employees` <> and see how it maps onto this structure.
-Below is the definition for an `EmployeeList`.
+When the state is updated, the `render()` function is invoked by the framework. The employee state data is included in the creation of the `` React component as an input parameter.
-.src/main/resources/static/app.js - EmployeeList component
+The following listing shows the definition for an `EmployeeList`:
+
+.src/main/js/app.js - EmployeeList component
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=employee-list]
+include::src/main/js/app.js[tag=employee-list]
----
+====
+
+Using JavaScript's map function, `this.props.employees` is transformed from an array of employee records into an array of `` React components (which you will see a little later).
-Using JavaScript's map function, `this.props.employees` is transformed from an array of employee records into an array of `` React components (which you'll see a little further down).
+Consider the following listing:
+====
[source,javascript]
----
----
+====
-This shows a new React component (note the uppercase format) being created along with two properties: *key* and *data*. These are supplied the values from `employee._links.self.href` and `employee`.
+The preceding listing creates a new React component (note the uppercase format) with two properties: *key* and *data*. These are supplied with values from `employee._links.self.href` and `employee`.
-IMPORTANT: Whenever you work with Spring Data REST, the *self* link IS the key for a given resource. React needs a unique identifer for child nodes, and `_links.self.href` is perfect.
+IMPORTANT: Whenever you work with Spring Data REST, the `self` link is the key for a given resource. React needs a unique identifier for child nodes, and `_links.self.href` is perfect.
-Finally, you return an HTML table wrapped around the array of `employees` built with mapping.
+Finally, you return an HTML table wrapped around the array of `employees` built with mapping, as follows:
+====
[source,html]
----
@@ -316,71 +395,78 @@ Finally, you return an HTML table wrapped around the array of `employees` built
{employees}
----
+====
-This simple layout of state, properties, and HTML shows how React lets you declaritively create a simple and easy-to-understand component.
+This simple layout of state, properties, and HTML shows how React lets you declaratively create a simple and easy-to-understand component.
[[NOTE]]
====
-Does this code contain both HTML _and_ JavaScript? Yes, this is https://facebook.github.io/js/[JSX]. There is no requirement to use it. React can be written using pure JavaScript, but the JSX syntax is quite terse. Thanks to rapid work on the Babel.js, the transpiler provides both JSX and ES6 support all at once
+Does this code contain both HTML _and_ JavaScript? Yes, this is https://facebook.github.io/jsx/[JSX]. There is no requirement to use it. React can be written using pure JavaScript, but the JSX syntax is quite terse. Thanks to rapid work on the Babel.js, the transpiler provides both JSX and ES6 support all at once.
-JSX also includes bits and pieces of http://es6-features.org/#Constants[ES6]. The one used in the code is the http://es6-features.org/#ExpressionBodies[arrow function]. It avoids creating a nested function() with its own scoped *this*, and avoids needing a http://stackoverflow.com/a/962040/28214[*self* variable].
+JSX also includes bits and pieces of http://es6-features.org/#Constants[ES6]. The one used in this code is the http://es6-features.org/#ExpressionBodies[arrow function]. It avoids creating a nested `function()` with its own scoped `this` and avoids needing a https://stackoverflow.com/a/962040/28214[`self` variable].
Worried about mixing logic with your structure? React's APIs encourage nice, declarative structure combined with state and properties. Instead of mixing a bunch of unrelated JavaScript and HTML, React encourages building simple components with small bits of related state and properties that work well together. It lets you look at a single component and understand the design. Then they are easy to combine together for bigger structures.
====
-Next, you need to actually define what an `` is.
+Next, you need to actually define what an `` is, as follows:
-.src/main/resources/static/app.js - Employee component
+.src/main/js/app.js - Employee component
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=employee]
+include::src/main/js/app.js[tag=employee]
----
+====
-This component is very simple. It has a single HTML table row wrapped around the employee's three properties. The property itself is `this.props.employee`. Notice how passing in a JavaScript object makes it easy to pass along data fetched from the server?
+This component is very simple. It has a single HTML table row wrapped around the employee's three properties. The property itself is `this.props.employee`. Notice how passing in a JavaScript object makes it easy to pass along data fetched from the server.
-Because this component doesn't manage any state nor does it deal with user input, there is nothing else to do. This might tempt you to cram it into the `` up above. Don't do it! Instead, splitting your app up into small components that each do one job will make it easier to build up functionality in the future.
+Because this component does not manage any state nor deal with user input, there is nothing else to do. This might tempt you to cram it into the `` up above. Do not do it! Splitting your app up into small components that each do one job will make it easier to build up functionality in the future.
-The last step is to render the whole thing.
+The last step is to render the whole thing, as follows:
-.src/main/resources/static/app.js - rendering code
+.src/main/js/app.js - rendering code
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=render]
+include::src/main/js/app.js[tag=render]
----
+====
`React.render()` accepts two arguments: a React component you defined as well as a DOM node to inject it into. Remember how you saw the `` item earlier from the HTML page? This is where it gets picked up and plugged in.
-With all this in place, re-run the application (`./mvnw spring-boot:run`) and visit http://localhost:8080.
+With all this in place, re-run the application (`./mvnw spring-boot:run`) and visit http://localhost:8080. The following image shows the updated application:
image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/basic/images/basic-1.png[]
You can see the initial employee loaded up by the system.
-Remember using cURL to create new entries? Do that again.
+Remember using cURL to create new entries? Do that again with the following command:
+====
----
-curl -X POST localhost:8080/api/employees -d '{"firstName": "Bilbo", "lastName": "Baggins", "description": "burglar"}' -H 'Content-Type:application/json'
+curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
----
+====
Refresh the browser, and you should see the new entry:
image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/basic/images/basic-2.png[]
-And now you can see both of them listed on the web site.
+Now you can see both of them listed on the web site.
== Review
In this section:
* You defined a domain object and a corresponding repository.
-* You let Spring Data REST export it with full blown hypermedia controls.
+* You let Spring Data REST export it with full-blown hypermedia controls.
* You created two simple React components in a parent-child relationship.
* You fetched server data and rendered them in as a simple, static HTML structure.
Issues?
-* The web page wasn't dynamic. You had to refresh the browser to fetch new records.
-* The web page didn't use any hypermedia controls or metadata. Instead, it was hardcoded to fetch data from `/api/employees`.
-* It's read only. While you can alter records using cURL, the web page offers none of that.
+* The web page was not dynamic. You had to refresh the browser to fetch new records.
+* The web page did not use any hypermedia controls or metadata. Instead, it was hardcoded to fetch data from `/api/employees`.
+* It is read only. While you can alter records using cURL, the web page offers no interactivity.
-These are things we can address in the next section.
+We address these shortcomings in the next section.
diff --git a/basic/mvnw b/basic/mvnw
index c67cd41..a16b543 100755
--- a/basic/mvnw
+++ b/basic/mvnw
@@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# 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
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Maven2 Start Up Batch script
+# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
@@ -54,38 +54,16 @@ case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
- #
- # Look for the Apple JDKs first to preserve the existing behaviour, and then look
- # for the new JDKs provided by Oracle.
- #
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
- #
- # Oracle JDKs
- #
- export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=`/usr/libexec/java_home`
- fi
- ;;
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
esac
if [ -z "$JAVA_HOME" ] ; then
@@ -130,13 +108,12 @@ if $cygwin ; then
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
-# For Migwn, ensure paths are in UNIX format before anything is touched
+# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
- # TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
@@ -184,27 +161,28 @@ fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin; then
- [ -n "$M2_HOME" ] &&
- M2_HOME=`cygpath --path --windows "$M2_HOME"`
- [ -n "$JAVA_HOME" ] &&
- JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
- [ -n "$CLASSPATH" ] &&
- CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
-fi
-
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
- local basedir=$(pwd)
- local wdir=$(pwd)
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
while [ "$wdir" != '/' ] ; do
- wdir=$(cd "$wdir/.."; pwd)
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
done
echo "${basedir}"
}
@@ -216,10 +194,109 @@ concat_lines() {
fi
}
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
-# Provide a "standardized" way to retrieve the CLI args that will
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
@@ -228,7 +305,6 @@ WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
- -classpath "./.mvn/wrapper/maven-wrapper.jar" \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
- ${WRAPPER_LAUNCHER} "$@"
-
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/conditional/mvnw.bat b/basic/mvnw.cmd
similarity index 68%
rename from conditional/mvnw.bat
rename to basic/mvnw.cmd
index 7ca42b9..c8d4337 100644
--- a/conditional/mvnw.bat
+++ b/basic/mvnw.cmd
@@ -7,7 +7,7 @@
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@@ -18,7 +18,7 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
-@REM Maven2 Start Up Batch script
+@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@@ -26,7 +26,7 @@
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -35,7 +35,9 @@
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
-@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
@@ -66,7 +68,7 @@ echo.
goto error
:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto chkMHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
@@ -76,42 +78,10 @@ echo location of your Java installation. >&2
echo.
goto error
-:chkMHome
-if not "%M2_HOME%"=="" goto valMHome
-
-SET "M2_HOME=%~dp0.."
-if not "%M2_HOME%"=="" goto valMHome
-
-echo.
-echo Error: M2_HOME not found in your environment. >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation. >&2
-echo.
-goto error
-
-:valMHome
-
-:stripMHome
-if not "_%M2_HOME:~-1%"=="_\" goto checkMCmd
-set "M2_HOME=%M2_HOME:~0,-1%"
-goto stripMHome
-
-:checkMCmd
-if exist "%M2_HOME%\bin\mvn.cmd" goto init
-
-echo.
-echo Error: M2_HOME is set to an invalid directory. >&2
-echo M2_HOME = "%M2_HOME%" >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation >&2
-echo.
-goto error
@REM ==== END VALIDATION ====
:init
-set MAVEN_CMD_LINE_ARGS=%*
-
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
@@ -147,13 +117,48 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-
-for %%i in ("%M2_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i"
-
-set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.home=%M2_HOME%" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
diff --git a/hypermedia/src/main/resources/static/package.json b/basic/package.json
similarity index 59%
rename from hypermedia/src/main/resources/static/package.json
rename to basic/package.json
index b6ae278..dbb9c5a 100644
--- a/hypermedia/src/main/resources/static/package.json
+++ b/basic/package.json
@@ -14,19 +14,25 @@
"react"
],
"author": "Greg L. Turnquist",
- "license": "ASLv2",
+ "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": {
- "babel-core": "^5.8.25",
- "babel-loader": "^5.3.2",
- "react": "^0.13.3",
- "rest": "^1.3.1",
- "webpack": "^1.12.2"
+ "react": "^16.5.2",
+ "react-dom": "^16.5.2",
+ "rest": "^1.3.1"
},
"scripts": {
- "watch": "webpack --watch -d"
+ "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js"
+ },
+ "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"
}
}
diff --git a/basic/pom.xml b/basic/pom.xml
index e81ea88..fb6b706 100644
--- a/basic/pom.xml
+++ b/basic/pom.xml
@@ -1,20 +1,17 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- org.springframework.boot
- spring-boot-starter-parent
- 1.3.3.RELEASE
-
+ com.greglturnquist
+ react-and-spring-data-rest
+ 0.0.1-SNAPSHOT
-
- com.greglturnquist
+
react-and-spring-data-rest-basic0.0.1-SNAPSHOT
- jarReact.js and Spring Data REST - BasicAn SPA with ReactJS in the frontend and Spring Data REST in the backend
@@ -41,12 +38,6 @@
org.springframework.bootspring-boot-devtools
-
- org.projectlombok
- lombok
- 1.16.4
- provided
- com.h2databaseh2
@@ -66,41 +57,12 @@
spring-boot-maven-plugin
+
com.github.eirslettfrontend-maven-plugin
- 0.0.24
-
- src/main/resources/static
-
-
-
- install node and npm
-
- install-node-and-npm
-
-
- v0.10.33
- 1.3.8
-
-
-
- npm install
-
- npm
-
-
- install
-
-
-
- webpack build
-
- webpack
-
-
-
+
diff --git a/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
index 42f934b..017ee7b 100644
--- a/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
+++ b/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -23,19 +23,19 @@
* @author Greg Turnquist
*/
// tag::code[]
-@Component
-public class DatabaseLoader implements CommandLineRunner {
+@Component // <1>
+public class DatabaseLoader implements CommandLineRunner { // <2>
private final EmployeeRepository repository;
- @Autowired
+ @Autowired // <3>
public DatabaseLoader(EmployeeRepository repository) {
this.repository = repository;
}
@Override
- public void run(String... strings) throws Exception {
+ public void run(String... strings) throws Exception { // <4>
this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
}
}
-// end::code[]
\ No newline at end of file
+// end::code[]
diff --git a/basic/src/main/java/com/greglturnquist/payroll/Employee.java b/basic/src/main/java/com/greglturnquist/payroll/Employee.java
index b9e57cf..80ec33c 100644
--- a/basic/src/main/java/com/greglturnquist/payroll/Employee.java
+++ b/basic/src/main/java/com/greglturnquist/payroll/Employee.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,21 +15,20 @@
*/
package com.greglturnquist.payroll;
+import java.util.Objects;
+
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
-import lombok.Data;
-
/**
* @author Greg Turnquist
*/
// tag::code[]
-@Data
-@Entity
+@Entity // <1>
public class Employee {
- private @Id @GeneratedValue Long id;
+ private @Id @GeneratedValue Long id; // <2>
private String firstName;
private String lastName;
private String description;
@@ -41,5 +40,64 @@ public Employee(String firstName, String lastName, String description) {
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[]
\ No newline at end of file
+// end::code[]
diff --git a/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
index 12f65ee..17cbee8 100644
--- a/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
+++ b/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -21,7 +21,7 @@
* @author Greg Turnquist
*/
// tag::code[]
-public interface EmployeeRepository extends CrudRepository {
+public interface EmployeeRepository extends CrudRepository { // <1>
}
// end::code[]
diff --git a/basic/src/main/java/com/greglturnquist/payroll/HomeController.java b/basic/src/main/java/com/greglturnquist/payroll/HomeController.java
index 465724d..98f5711 100644
--- a/basic/src/main/java/com/greglturnquist/payroll/HomeController.java
+++ b/basic/src/main/java/com/greglturnquist/payroll/HomeController.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -22,13 +22,13 @@
* @author Greg Turnquist
*/
// tag::code[]
-@Controller
+@Controller // <1>
public class HomeController {
- @RequestMapping(value = "/")
+ @RequestMapping(value = "/") // <2>
public String index() {
- return "index";
+ return "index"; // <3>
}
}
-// end::code[]
\ No newline at end of file
+// end::code[]
diff --git a/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
index 0ba0a93..7b2fc31 100644
--- a/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
+++ b/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/events/src/main/resources/static/api/uriListConverter.js b/basic/src/main/js/api/uriListConverter.js
similarity index 81%
rename from events/src/main/resources/static/api/uriListConverter.js
rename to basic/src/main/js/api/uriListConverter.js
index 1c2124e..8d9dc2e 100644
--- a/events/src/main/resources/static/api/uriListConverter.js
+++ b/basic/src/main/js/api/uriListConverter.js
@@ -9,9 +9,7 @@ define(function() {
write: function(obj /*, opts */) {
// If this is an Array, extract the self URI and then join using a newline
if (obj instanceof Array) {
- return obj.map(function(resource) {
- return resource._links.self.href;
- }).join('\n');
+ return obj.map(resource => resource._links.self.href).join('\n');
} else { // otherwise, just return the self URI
return obj._links.self.href;
}
diff --git a/conditional/src/main/resources/static/api/uriTemplateInterceptor.js b/basic/src/main/js/api/uriTemplateInterceptor.js
similarity index 62%
rename from conditional/src/main/resources/static/api/uriTemplateInterceptor.js
rename to basic/src/main/js/api/uriTemplateInterceptor.js
index 269165f..c16ba33 100644
--- a/conditional/src/main/resources/static/api/uriTemplateInterceptor.js
+++ b/basic/src/main/js/api/uriTemplateInterceptor.js
@@ -1,11 +1,11 @@
define(function(require) {
'use strict';
- var interceptor = require('rest/interceptor');
+ const interceptor = require('rest/interceptor');
return interceptor({
request: function (request /*, config, meta */) {
- /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */
+ /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */
if (request.path.indexOf('{') === -1) {
return request;
} else {
diff --git a/basic/src/main/resources/static/app.js b/basic/src/main/js/app.js
similarity index 68%
rename from basic/src/main/resources/static/app.js
rename to basic/src/main/js/app.js
index a9fe0c7..dea346c 100644
--- a/basic/src/main/resources/static/app.js
+++ b/basic/src/main/js/app.js
@@ -1,25 +1,26 @@
'use strict';
// tag::vars[]
-const React = require('react');
-const client = require('./client');
+const React = require('react'); // <1>
+const ReactDOM = require('react-dom'); // <2>
+const client = require('./client'); // <3>
// end::vars[]
// tag::app[]
-class App extends React.Component {
+class App extends React.Component { // <1>
constructor(props) {
super(props);
this.state = {employees: []};
}
- componentDidMount() {
+ componentDidMount() { // <2>
client({method: 'GET', path: '/api/employees'}).done(response => {
this.setState({employees: response.entity._embedded.employees});
});
}
- render() {
+ render() { // <3>
return (
)
@@ -30,17 +31,19 @@ class App extends React.Component {
// tag::employee-list[]
class EmployeeList extends React.Component{
render() {
- var employees = this.props.employees.map(employee =>
+ const employees = this.props.employees.map(employee =>
);
return (
-
-
First Name
-
Last Name
-
Description
-
- {employees}
+
+
+
First Name
+
Last Name
+
Description
+
+ {employees}
+
)
}
@@ -62,9 +65,8 @@ class Employee extends React.Component{
// end::employee[]
// tag::render[]
-React.render(
+ReactDOM.render(
,
document.getElementById('react')
)
// end::render[]
-
diff --git a/basic/src/main/js/client.js b/basic/src/main/js/client.js
new file mode 100644
index 0000000..45c6102
--- /dev/null
+++ b/basic/src/main/js/client.js
@@ -0,0 +1,19 @@
+'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' }});
diff --git a/basic/src/main/resources/static/client.js b/basic/src/main/resources/static/client.js
deleted file mode 100644
index 8e542c9..0000000
--- a/basic/src/main/resources/static/client.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-var rest = require('rest');
-var defaultRequest = require('rest/interceptor/defaultRequest');
-var mime = require('rest/interceptor/mime');
-var uriTemplateInterceptor = require('./api/uriTemplateInterceptor');
-var errorCode = require('rest/interceptor/errorCode');
-var baseRegistry = require('rest/mime/registry');
-
-var 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' }});
diff --git a/basic/src/main/resources/static/webpack.config.js b/basic/src/main/resources/static/webpack.config.js
deleted file mode 100644
index 16e9fbd..0000000
--- a/basic/src/main/resources/static/webpack.config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var path = require('path');
-
-module.exports = {
- entry: './app.js',
- devtool: 'sourcemaps',
- cache: true,
- debug: true,
- output: {
- path: __dirname,
- filename: './built/bundle.js'
- },
- module: {
- loaders: [
- {
- test: path.join(__dirname, '.'),
- exclude: /(node_modules)/,
- loader: 'babel-loader'
- }
- ]
- }
-};
\ No newline at end of file
diff --git a/basic/src/main/resources/templates/index.html b/basic/src/main/resources/templates/index.html
index 802ffdb..c5f2c1e 100644
--- a/basic/src/main/resources/templates/index.html
+++ b/basic/src/main/resources/templates/index.html
@@ -1,5 +1,5 @@
-
+
ReactJS + Spring Data REST
diff --git a/basic/webpack.config.js b/basic/webpack.config.js
new file mode 100644
index 0000000..81bec01
--- /dev/null
+++ b/basic/webpack.config.js
@@ -0,0 +1,26 @@
+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"]
+ }
+ }]
+ }
+ ]
+ }
+};
\ No newline at end of file
diff --git a/conditional/.mvn/wrapper/MavenWrapperDownloader.java b/conditional/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/conditional/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/conditional/.mvn/wrapper/maven-wrapper.jar b/conditional/.mvn/wrapper/maven-wrapper.jar
index 5fd4d50..2cc7d4a 100644
Binary files a/conditional/.mvn/wrapper/maven-wrapper.jar and b/conditional/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/conditional/.mvn/wrapper/maven-wrapper.properties b/conditional/.mvn/wrapper/maven-wrapper.properties
index eb91947..642d572 100644
--- a/conditional/.mvn/wrapper/maven-wrapper.properties
+++ b/conditional/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/conditional/README.adoc b/conditional/README.adoc
index 5a188a0..a3494e6 100644
--- a/conditional/README.adoc
+++ b/conditional/README.adoc
@@ -2,115 +2,134 @@
= Part 3 - Conditional Operations
:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master
-In the <>, you found out how to turn on Spring Data REST's hypermedia controls, have the UI navigate by paging, and dynamically resize based on changing the page size. You added the ability to create and delete employees and have the pages adjust. But no solution is complete with taking into consideration updates made by other users on the same bit of data you are currently editing.
+In the <>, you found out how to turn on Spring Data REST's hypermedia controls, have the UI navigate by paging, and dynamically resize based on changing the page size. You added the ability to create and delete employees and have the pages adjust. But no solution is complete without taking into consideration updates made by other users on the same bit of data you are currently editing.
-Feel free to {sourcedir}/conditional[grab the code] from this repository and follow along. This section is based on the previous section's app with extra things added.
+Feel free to {sourcedir}/conditional[grab the code] from this repository and follow along. This section is based on the previous section but with extra features added.
-== To PUT or not to PUT, that is the question
+== To PUT or Not to PUT? That is the Question.
-When you fetch a resource, there is risk is that it might go stale if someone else updates it. To deal with this, Spring Data REST integrates two technologies: versioning of resources and ETags.
+When you fetch a resource, the risk is that it might go stale if someone else updates it. To deal with this, Spring Data REST integrates two technologies: versioning of resources and ETags.
-By versioning resources on the backend and using ETags in the frontend, it is possible to conditially PUT a change. In other words, you can detect if a resource has changed and prevent a PUT (or a PATCH) from stomping on someone else's update. Let's check it out.
+By versioning resources on the backend and using ETags in the frontend, it is possible to conditionally `PUT` a change. In other words, you can detect whether a resource has changed and prevent a `PUT` (or a `PATCH`) from stomping on someone else's update.
-== Versioning REST resources
+== Versioning REST Resources
-To support versioning of resources, define a version attribute for your domain objects that need this type of protection.
+To support versioning of resources, define a version attribute for your domain objects that need this type of protection. The following listing shows how to do so for the `Employee` object:
.src/main/java/com/greglturnquist/payroll/Employee.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/Employee.java[tag=code]
----
+====
-* The *version* field is annotated with `javax.persistence.Version`. It causes a value to be automatically stored and updated everytime a row is inserted and updated.
+* The `version` field is annotated with `javax.persistence.Version`. It causes a value to be automatically stored and updated every time a row is inserted and updated.
-When fetching an individual resource (not a collection resource), Spring Data REST will automatically add an http://tools.ietf.org/html/rfc7232#section-2.3[ETag response header] with the value of this field.
+When fetching an individual resource (not a collection resource), Spring Data REST automatically adds an https://tools.ietf.org/html/rfc7232#section-2.3[ETag response header] with the value of this field.
-== Fetching individual resources and their headers
+== Fetching Individual Resources and Their Headers
-In the <> you used the collection resource to gather data and populate the UI's HTML table. With Spring Data REST, the *_embedded* data set is considered a preview of data. While useful for glancing at data, to get headers like ETags, you need to fetch each resource individually.
+In the <>, you used the collection resource to gather data and populate the UI's HTML table. With Spring Data REST, the `_embedded` data set is considered a preview of data. While useful for glancing at data, to get headers like ETags, you need to fetch each resource individually.
-In this version, `loadFromServer` is updated to fetch the collection and then use the URIs to retrieve each individual resource.
+In this version, `loadFromServer` is updated to fetch the collection. Then you can use the URIs to retrieve each individual resource:
-.src/main/resources/static/app.js - Fetching each resource
+.src/main/js/app.js - Fetching each resource
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=follow-2]
+include::src/main/js/app.js[tag=follow-2]
----
-. The `follow()` function goes to the *employees* collection resource.
-. The `then(employeeCollection => ...)` clause creates a call to fetch JSON Schema data. This has a sub-then clause to store the metadata and navigational links in the `` component.
-* Notice that this embedded promise returns the employeeCollection. That way, the collection can be passed onto the next call while letting you grab the metadata along the way.
-. The second `then(employeeCollection => ...)` clause converts the collection of employees into an array of GET promises to fetch each individual resource. *This is what you need to fetch an ETag header for each employee.*
-. The `then(employeePromises => ...)` clause takes the array of GET promises and merges them into a single promise with `when.all()`, resolved when all the GET promises are resolved.
-. `loadFromServer` wraps up with `done(employees => ...)` where the UI state is updated using this amalgamation of data.
+<1> The `follow()` function goes to the `employees` collection resource.
-This chain is implemented in other places as well. For example, `onNavigate()`, which is used to jump to different pages, has been updated to fetch individual resources. Since it's mostly the same as what's shown above, it's been left out of this section.
+<2> The first `then(employeeCollection => ...)` clause creates a call to fetch JSON Schema data. This has an inner then clause to store the metadata and navigational links in the `` component.
++
+Notice that this embedded promise returns the `employeeCollection`. That way, the collection can be passed onto the next call, letting you grab the metadata along the way.
+
+<3> The second `then(employeeCollection => ...)` clause converts the collection of employees into an array of `GET` promises to fetch each individual resource. *This is what you need to fetch an ETag header for each employee.*
+
+<4> The `then(employeePromises => ...)` clause takes the array of `GET` promises and merges them into a single promise with `when.all()`, which is resolved when all the GET promises are resolved.
+
+<5> `loadFromServer` wraps up with `done(employees => ...)` where the UI state is updated using this amalgamation of data.
+====
-== Updating existing resources
+This chain is implemented in other places as well. For example, `onNavigate()` (which is used to jump to different pages) has been updated to fetch individual resources. Since it is mostly the same as what is shown here, it has been left out of this section.
-In this section, you are adding an `UpdateDialog` React component to edit existing employee records.
+== Updating Existing Resources
-.src/main/resources/static/app.js - UpdateDialog component
+In this section, you are adding an `UpdateDialog` React component to edit existing employee records:
+
+.src/main/js/app.js - UpdateDialog component
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=update-dialog]
+include::src/main/js/app.js[tag=update-dialog]
----
+====
-This new component has both a `handleSubmit()` function as well as the expected `render()` function, similar to the `` component.
+This new component has both a `handleSubmit()` function and the expected `render()` function, similar to the `` component.
-Let's dig into these functions in reverse order, and first look at the `render()` function.
+We dig into these functions in reverse order and first look at the `render()` function.
=== Rendering
This component uses the same CSS/HTML tactics to show and hide the dialog as the `` from the previous section.
-It converts the array of JSON Schema attributes into an array of HTML inputs, wrapped in paragraph elements for styling. This is also the same as the `` with one difference: the fields are loaded with *this.props.employee*. In the CreateDialog component, the fields are empty.
+It converts the array of JSON Schema attributes into an array of HTML inputs, wrapped in paragraph elements for styling. This is also the same as the `` with one difference: The fields are loaded with `this.props.employee`. In the `CreateDialog` component, the fields are empty.
-The *id* field is built differently. There is only one CreateDialog link on the entire UI, but a separate UpdateDialog link for every row displayed. Hence, the *id* field is based on the *self* link's URI. This is used in both the
element's React *key* as well as the HTML anchor tag and the hidden pop-up.
+The `id` field is built differently. There is only one `CreateDialog` link on the entire UI, but a separate `UpdateDialog` link for every row displayed. Hence, the `id` field is based on the `self` link's URI. This is used in the `
` element's React `key`, the HTML anchor tag, and the hidden pop-up.
-=== Handling user input
+=== Handling User Input
-The submit button is linked to the component's `handleSubmit()` function. This handily uses `React.findDOMNode()` to extract the details of the pop-up using http://facebook.github.io/react/docs/more-about-refs.html[React refs].
+The submit button is linked to the component's `handleSubmit()` function. This handily uses `React.findDOMNode()` to extract the details of the pop-up by using https://facebook.github.io/react/docs/more-about-refs.html[React refs].
-After the input values are extracted and loaded into the `updatedEmployee` object, the top-level `onUpdate()` method is invoked. This continues React's style of one-way binding where the functions to call are pushed from upper level components into the lower level ones. This way, state is still managed at the top.
+After the input values are extracted and loaded into the `updatedEmployee` object, the top-level `onUpdate()` method is invoked. This continues React's style of one-way binding where the functions to call are pushed from upper-level components into the lower-level ones. This way, state is still managed at the top.
== Conditional PUT
-So you've gone to all this effort to embed versioning in the data model. Spring Data REST has served up that value as an ETag response header. Here is where you get to put it to good use!
+So you have gone to all this effort to embed versioning in the data model. Spring Data REST has served up that value as an ETag response header. Here is where you get to put it to good use:
-.src/main/resources/static/app.js - onUpdate function
+.src/main/js/app.js - onUpdate function
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=update]
+include::src/main/js/app.js[tag=update]
----
+====
-PUT with an http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24[If-Match request header] causes Spring Data REST to check the value against the current version. If the incoming *If-Match* value doesn't match the data store's version value, Spring Data REST will fail with an *HTTP 412 Precondition Failed*.
+A `PUT` with an https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24[`If-Match` request header] causes Spring Data REST to check the value against the current version. If the incoming `If-Match` value does not match the data store's version value, Spring Data REST will fail with an `HTTP 412 Precondition Failed`.
-NOTE: The spec for https://promisesaplus.com/[Promises/A+] actually defines their API as `then(successFunction, errorFunction)`. So far, you've only seen it used with success functions. In the code fragment above, there are two functions. The success function invokes `loadFromServer` while the error function displays a browser alert about the stale data.
+NOTE: The specification for https://promisesaplus.com/[Promises/A+] actually defines their API as `then(successFunction, errorFunction)`. So far, you have seen it used only with success functions. In the preceding code fragment, there are two functions. The success function invokes `loadFromServer`, while the error function displays a browser alert about the stale data.
-== Putting it all together
+== Putting It All Together
With your `UpdateDialog` React component defined and nicely linked to the top-level `onUpdate` function, the last step is to wire it into the existing layout of components.
The `CreateDialog` created in the previous section was put at the top of the `EmployeeList` because there is only one instance. However, `UpdateDialog` is tied directly to specific employees. So you can see it plugged in below in the `Employee` React component:
-.src/main/resources/static/app.js - Employee with UpdateDialog
+.src/main/js/app.js - Employee with UpdateDialog
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=employee]
+include::src/main/js/app.js[tag=employee]
----
+====
-In this section, you switch from using the collection resource to individual resources. The fields for an employee record are now found at `this.props.employee.entity`. It gives us access to `this.props.employee.headers` where we can find ETags.
+In this section, you switch from using the collection resource to using individual resources. The fields for an employee record are now found at `this.props.employee.entity`. It gives us access to `this.props.employee.headers`, where we can find ETags.
-There are other headers supported by Spring Data REST (like http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29[Last-Modified]) which aren't part of this series. So structuring your data this way is handy.
+There are other headers supported by Spring Data REST (such as https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29[Last-Modified]) that are not part of this series. So structuring your data this way is handy.
IMPORTANT: The structure of `.entity` and `.headers` is only pertinent when using https://github.com/cujojs/rest[rest.js] as the REST library of choice. If you use a different library, you will have to adapt as necessary.
-== Seeing things in action
+== Seeing Things in Action
+
+To see the modified application work:
-. Start up the app (`./mvnw spring-boot:run`).
-. Open up a tab and navigate to http://localhost:8080.
+. Start the application by running `./mvnw spring-boot:run`.
+. Open a browser tab and navigate to http://localhost:8080.
++
+You should see a page similar to the following image:
+
image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-1.png[]
+
@@ -119,26 +138,28 @@ image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/maste
. Make a change to the record in the first tab.
. Try to make a change in the second tab.
+
+You should see the browser tabs change, as the following images show
++
image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-2.png[]
image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-3.png[]
-With these mods, you have increased data integrity by avoiding collisions.
+With these modifications, you have increased data integrity by avoiding collisions.
== Review
In this section:
-* You configured your domain model with an `@Version` field for JPA-based optimistic locking.
+* You configured your domain model with a `@Version` field for JPA-based optimistic locking.
* You adjusted the frontend to fetch individual resources.
-* You plugged the ETag header from an individual resource into an *If-Match* request header to make PUTs conditional.
-* You coded a new UpdateDialog for each employee shown on the list.
+* You plugged the ETag header from an individual resource into an `If-Match` request header to make PUTs conditional.
+* You coded a new `UpdateDialog` for each employee shown on the list.
-With this plugged in, it's easy to avoid colliding with other users, or simply overwriting their edits.
+With this plugged in, itis easy to avoid colliding with other users or overwriting their edits.
Issues?
-It's certainly nice to know when you're editing a bad record. But is it best to wait until you click "Submit" to find out?
+It is certainly nice to know when you are editing a bad record. But is it best to wait until you click "Submit" to find out?
The logic to fetch resources is very similar in both `loadFromServer` and `onNavigate`. Do you see ways to avoid duplicate code?
diff --git a/conditional/mvnw b/conditional/mvnw
index c67cd41..a16b543 100755
--- a/conditional/mvnw
+++ b/conditional/mvnw
@@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# 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
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Maven2 Start Up Batch script
+# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
@@ -54,38 +54,16 @@ case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
- #
- # Look for the Apple JDKs first to preserve the existing behaviour, and then look
- # for the new JDKs provided by Oracle.
- #
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
- #
- # Oracle JDKs
- #
- export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=`/usr/libexec/java_home`
- fi
- ;;
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
esac
if [ -z "$JAVA_HOME" ] ; then
@@ -130,13 +108,12 @@ if $cygwin ; then
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
-# For Migwn, ensure paths are in UNIX format before anything is touched
+# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
- # TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
@@ -184,27 +161,28 @@ fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin; then
- [ -n "$M2_HOME" ] &&
- M2_HOME=`cygpath --path --windows "$M2_HOME"`
- [ -n "$JAVA_HOME" ] &&
- JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
- [ -n "$CLASSPATH" ] &&
- CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
-fi
-
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
- local basedir=$(pwd)
- local wdir=$(pwd)
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
while [ "$wdir" != '/' ] ; do
- wdir=$(cd "$wdir/.."; pwd)
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
done
echo "${basedir}"
}
@@ -216,10 +194,109 @@ concat_lines() {
fi
}
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
-# Provide a "standardized" way to retrieve the CLI args that will
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
@@ -228,7 +305,6 @@ WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
- -classpath "./.mvn/wrapper/maven-wrapper.jar" \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
- ${WRAPPER_LAUNCHER} "$@"
-
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/basic/mvnw.bat b/conditional/mvnw.cmd
similarity index 68%
rename from basic/mvnw.bat
rename to conditional/mvnw.cmd
index 7ca42b9..c8d4337 100644
--- a/basic/mvnw.bat
+++ b/conditional/mvnw.cmd
@@ -7,7 +7,7 @@
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@@ -18,7 +18,7 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
-@REM Maven2 Start Up Batch script
+@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@@ -26,7 +26,7 @@
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -35,7 +35,9 @@
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
-@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
@@ -66,7 +68,7 @@ echo.
goto error
:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto chkMHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
@@ -76,42 +78,10 @@ echo location of your Java installation. >&2
echo.
goto error
-:chkMHome
-if not "%M2_HOME%"=="" goto valMHome
-
-SET "M2_HOME=%~dp0.."
-if not "%M2_HOME%"=="" goto valMHome
-
-echo.
-echo Error: M2_HOME not found in your environment. >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation. >&2
-echo.
-goto error
-
-:valMHome
-
-:stripMHome
-if not "_%M2_HOME:~-1%"=="_\" goto checkMCmd
-set "M2_HOME=%M2_HOME:~0,-1%"
-goto stripMHome
-
-:checkMCmd
-if exist "%M2_HOME%\bin\mvn.cmd" goto init
-
-echo.
-echo Error: M2_HOME is set to an invalid directory. >&2
-echo M2_HOME = "%M2_HOME%" >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation >&2
-echo.
-goto error
@REM ==== END VALIDATION ====
:init
-set MAVEN_CMD_LINE_ARGS=%*
-
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
@@ -147,13 +117,48 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-
-for %%i in ("%M2_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i"
-
-set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.home=%M2_HOME%" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
diff --git a/basic/src/main/resources/static/package.json b/conditional/package.json
similarity index 59%
rename from basic/src/main/resources/static/package.json
rename to conditional/package.json
index b6ae278..dbb9c5a 100644
--- a/basic/src/main/resources/static/package.json
+++ b/conditional/package.json
@@ -14,19 +14,25 @@
"react"
],
"author": "Greg L. Turnquist",
- "license": "ASLv2",
+ "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": {
- "babel-core": "^5.8.25",
- "babel-loader": "^5.3.2",
- "react": "^0.13.3",
- "rest": "^1.3.1",
- "webpack": "^1.12.2"
+ "react": "^16.5.2",
+ "react-dom": "^16.5.2",
+ "rest": "^1.3.1"
},
"scripts": {
- "watch": "webpack --watch -d"
+ "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js"
+ },
+ "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"
}
}
diff --git a/conditional/pom.xml b/conditional/pom.xml
index 7b1cd9f..0674c8d 100644
--- a/conditional/pom.xml
+++ b/conditional/pom.xml
@@ -1,23 +1,20 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.greglturnquist
+
+ com.greglturnquist
+ react-and-spring-data-rest
+ 0.0.1-SNAPSHOT
+
+
react-and-spring-data-rest-conditional0.0.1-SNAPSHOT
- jarReact.js and Spring Data REST - ConditionalAn SPA with ReactJS in the frontend and Spring Data REST in the backend
-
- org.springframework.boot
- spring-boot-starter-parent
- 1.3.3.RELEASE
-
-
-
UTF-81.8
@@ -40,12 +37,6 @@
org.springframework.bootspring-boot-devtools
-
- org.projectlombok
- lombok
- 1.16.4
- provided
- com.h2databaseh2
@@ -68,37 +59,6 @@
com.github.eirslettfrontend-maven-plugin
- 0.0.24
-
- src/main/resources/static
-
-
-
- install node and npm
-
- install-node-and-npm
-
-
- v0.10.33
- 1.3.8
-
-
-
- npm install
-
- npm
-
-
- install
-
-
-
- webpack build
-
- webpack
-
-
-
diff --git a/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
index 7afedce..cb339c2 100644
--- a/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
+++ b/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/conditional/src/main/java/com/greglturnquist/payroll/Employee.java b/conditional/src/main/java/com/greglturnquist/payroll/Employee.java
index 654a1a7..aedf65d 100644
--- a/conditional/src/main/java/com/greglturnquist/payroll/Employee.java
+++ b/conditional/src/main/java/com/greglturnquist/payroll/Employee.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,20 +15,19 @@
*/
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 lombok.Data;
-
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Greg Turnquist
*/
// tag::code[]
-@Data
@Entity
public class Employee {
@@ -46,5 +45,74 @@ public Employee(String firstName, String lastName, String description) {
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[]
\ No newline at end of file
diff --git a/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
index a678141..748125c 100644
--- a/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
+++ b/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java b/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java
index 465724d..abaaba2 100644
--- a/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java
+++ b/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
index 0ba0a93..7b2fc31 100644
--- a/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
+++ b/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/conditional/src/main/resources/static/api/uriListConverter.js b/conditional/src/main/js/api/uriListConverter.js
similarity index 81%
rename from conditional/src/main/resources/static/api/uriListConverter.js
rename to conditional/src/main/js/api/uriListConverter.js
index 1c2124e..8d9dc2e 100644
--- a/conditional/src/main/resources/static/api/uriListConverter.js
+++ b/conditional/src/main/js/api/uriListConverter.js
@@ -9,9 +9,7 @@ define(function() {
write: function(obj /*, opts */) {
// If this is an Array, extract the self URI and then join using a newline
if (obj instanceof Array) {
- return obj.map(function(resource) {
- return resource._links.self.href;
- }).join('\n');
+ return obj.map(resource => resource._links.self.href).join('\n');
} else { // otherwise, just return the self URI
return obj._links.self.href;
}
diff --git a/events/src/main/resources/static/api/uriTemplateInterceptor.js b/conditional/src/main/js/api/uriTemplateInterceptor.js
similarity index 62%
rename from events/src/main/resources/static/api/uriTemplateInterceptor.js
rename to conditional/src/main/js/api/uriTemplateInterceptor.js
index 269165f..c16ba33 100644
--- a/events/src/main/resources/static/api/uriTemplateInterceptor.js
+++ b/conditional/src/main/js/api/uriTemplateInterceptor.js
@@ -1,11 +1,11 @@
define(function(require) {
'use strict';
- var interceptor = require('rest/interceptor');
+ const interceptor = require('rest/interceptor');
return interceptor({
request: function (request /*, config, meta */) {
- /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */
+ /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */
if (request.path.indexOf('{') === -1) {
return request;
} else {
diff --git a/conditional/src/main/resources/static/app.js b/conditional/src/main/js/app.js
similarity index 81%
rename from conditional/src/main/resources/static/app.js
rename to conditional/src/main/js/app.js
index f7596db..ae3b110 100644
--- a/conditional/src/main/resources/static/app.js
+++ b/conditional/src/main/js/app.js
@@ -1,6 +1,7 @@
'use strict';
const React = require('react');
+const ReactDOM = require('react-dom');
const when = require('when');
const client = require('./client');
@@ -22,9 +23,9 @@ class App extends React.Component {
// tag::follow-2[]
loadFromServer(pageSize) {
- follow(client, root, [
+ follow(client, root, [ // <1>
{rel: 'employees', params: {size: pageSize}}]
- ).then(employeeCollection => {
+ ).then(employeeCollection => { // <2>
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
@@ -34,16 +35,16 @@ class App extends React.Component {
this.links = employeeCollection.entity._links;
return employeeCollection;
});
- }).then(employeeCollection => {
+ }).then(employeeCollection => { // <3>
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
- }).then(employeePromises => {
+ }).then(employeePromises => { // <4>
return when.all(employeePromises);
- }).done(employees => {
+ }).done(employees => { // <5>
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
@@ -56,7 +57,7 @@ class App extends React.Component {
// tag::create[]
onCreate(newEmployee) {
- var self = this;
+ const self = this;
follow(client, root, ['employees']).then(response => {
return client({
method: 'POST',
@@ -67,7 +68,11 @@ class App extends React.Component {
}).then(response => {
return follow(client, root, [{rel: 'employees', params: {'size': self.state.pageSize}}]);
}).done(response => {
- self.onNavigate(response.entity._links.last.href);
+ if (typeof response.entity._links.last !== "undefined") {
+ this.onNavigate(response.entity._links.last.href);
+ } else {
+ this.onNavigate(response.entity._links.self.href);
+ }
});
}
// end::create[]
@@ -169,21 +174,21 @@ class CreateDialog extends React.Component {
handleSubmit(e) {
e.preventDefault();
- var newEmployee = {};
+ const newEmployee = {};
this.props.attributes.forEach(attribute => {
- newEmployee[attribute] = React.findDOMNode(this.refs[attribute]).value.trim();
+ newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
this.props.attributes.forEach(attribute => {
- React.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
+ ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
});
window.location = "#";
}
render() {
- var inputs = this.props.attributes.map(attribute =>
+ const inputs = this.props.attributes.map(attribute =>
{navLinks}
@@ -379,7 +386,7 @@ class Employee extends React.Component {
}
// end::employee[]
-React.render(
+ReactDOM.render(
,
document.getElementById('react')
)
diff --git a/conditional/src/main/js/client.js b/conditional/src/main/js/client.js
new file mode 100644
index 0000000..dfecbea
--- /dev/null
+++ b/conditional/src/main/js/client.js
@@ -0,0 +1,19 @@
+'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' }});
diff --git a/security/src/main/resources/static/follow.js b/conditional/src/main/js/follow.js
similarity index 90%
rename from security/src/main/resources/static/follow.js
rename to conditional/src/main/js/follow.js
index 5da616c..59efe70 100644
--- a/security/src/main/resources/static/follow.js
+++ b/conditional/src/main/js/follow.js
@@ -1,11 +1,11 @@
module.exports = function follow(api, rootPath, relArray) {
- var root = api({
+ const root = api({
method: 'GET',
path: rootPath
});
return relArray.reduce(function(root, arrayItem) {
- var rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel;
+ const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel;
return traverseNext(root, rel, arrayItem);
}, root);
diff --git a/conditional/src/main/resources/static/client.js b/conditional/src/main/resources/static/client.js
deleted file mode 100644
index 8e542c9..0000000
--- a/conditional/src/main/resources/static/client.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-var rest = require('rest');
-var defaultRequest = require('rest/interceptor/defaultRequest');
-var mime = require('rest/interceptor/mime');
-var uriTemplateInterceptor = require('./api/uriTemplateInterceptor');
-var errorCode = require('rest/interceptor/errorCode');
-var baseRegistry = require('rest/mime/registry');
-
-var 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' }});
diff --git a/conditional/src/main/resources/static/webpack.config.js b/conditional/src/main/resources/static/webpack.config.js
deleted file mode 100644
index e65b5c6..0000000
--- a/conditional/src/main/resources/static/webpack.config.js
+++ /dev/null
@@ -1,28 +0,0 @@
-var path = require('path');
-
-var node_dir = __dirname + '/node_modules';
-
-module.exports = {
- entry: './app.js',
- devtool: 'sourcemaps',
- cache: true,
- debug: true,
- resolve: {
- alias: {
- 'when': node_dir + '/rest/node_modules/when/when.js'
- }
- },
- output: {
- path: __dirname,
- filename: './built/bundle.js'
- },
- module: {
- loaders: [
- {
- test: path.join(__dirname, '.'),
- exclude: /(node_modules)/,
- loader: 'babel-loader'
- }
- ]
- }
-};
\ No newline at end of file
diff --git a/conditional/src/main/resources/templates/index.html b/conditional/src/main/resources/templates/index.html
index 802ffdb..c5f2c1e 100644
--- a/conditional/src/main/resources/templates/index.html
+++ b/conditional/src/main/resources/templates/index.html
@@ -1,5 +1,5 @@
-
+
ReactJS + Spring Data REST
diff --git a/conditional/webpack.config.js b/conditional/webpack.config.js
new file mode 100644
index 0000000..8d782ae
--- /dev/null
+++ b/conditional/webpack.config.js
@@ -0,0 +1,26 @@
+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"]
+ }
+ }]
+ }
+ ]
+ }
+};
\ No newline at end of file
diff --git a/events/.mvn/wrapper/MavenWrapperDownloader.java b/events/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/events/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/events/.mvn/wrapper/maven-wrapper.jar b/events/.mvn/wrapper/maven-wrapper.jar
index 5fd4d50..2cc7d4a 100644
Binary files a/events/.mvn/wrapper/maven-wrapper.jar and b/events/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/events/.mvn/wrapper/maven-wrapper.properties b/events/.mvn/wrapper/maven-wrapper.properties
index eb91947..642d572 100644
--- a/events/.mvn/wrapper/maven-wrapper.properties
+++ b/events/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/events/README.adoc b/events/README.adoc
index 77777f1..9a520e4 100644
--- a/events/README.adoc
+++ b/events/README.adoc
@@ -1,162 +1,178 @@
[[react-and-spring-data-rest-part-4]]
= Part 4 - Events
-:sourcedir: https://github.com/sprig-guides/tut-react-and-spring-data-rest/tree/master
+:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master
-In the <>, you introduced conditional updates to avoid collisions with other users when editing the same data. You also learned how to version data on the backend with optimistic locking. You got a tip off if someone edited the same record so you could refresh the page and get the update.
+In the <>, you introduced conditional updates to avoid collisions with other users when editing the same data. You also learned how to version data on the backend with optimistic locking. You got a notification if someone edited the same record so you could refresh the page and get the update.
-That's good. But do you know what's even better? Having the UI dynamically respond when other people update the resources.
+That is good. But do you know what is even better? Having the UI dynamically respond when other people update the resources.
-In this section you'll learn how to use Spring Data REST's built in event system to detect changes in the backend and publish updates to ALL users through Spring's WebSocket support. Then you'll be able to dynamically adjust clients as the data updates.
+In this section, you will learn how to use Spring Data REST's built in event system to detect changes in the backend and publish updates to ALL users through Spring's WebSocket support. Then you will be able to dynamically adjust clients as the data updates.
-Feel free to {sourcedir}/events[grab the code] from this repository and follow along. This section is based on the previous section's app with extra things added.
+Feel free to {sourcedir}/events[grab the code] from this repository and follow along. This section is based on the previous section's application, with extra things added.
-== Adding Spring WebSocket support to the project
+== Adding Spring WebSocket Support to the Project
Before getting underway, you need to add a dependency to your project's pom.xml file:
+====
[source,xml,indent=0]
----
include::pom.xml[tag=websocket]
----
+====
-This bring in Spring Boot's WebSocket starter.
+This dependency brings in Spring Boot's WebSocket starter.
== Configuring WebSockets with Spring
-http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#websocket[Spring comes with powerful WebSocket support]. One thing to recognize is that a WebSocket is a very low level protocol. It does little more than offer the means to transmit data between client and server. The recommendation is to use a sub-protocol (STOMP for this section) to actually encode data and routes.
+https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#websocket[Spring comes with powerful WebSocket support]. One thing to recognize is that a WebSocket is a very low-level protocol. It does little more than offer the means to transmit data between client and server. The recommendation is to use a sub-protocol (STOMP for this section) to actually encode data and routes.
-The follow code is used to configure WebSocket support on the server side:
+The following code configures WebSocket support on the server side:
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java[tag=code]
----
-* `@EnableWebSocketMessageBroker` turns on WebSocket support.
-* `AbstractWebSocketMessageBrokerConfigurer` provides a convenient base class to configure basic features.
-* *MESSAGE_PREFIX* is the prefix you will prepend to every message's route.
-* `registerStompEndpoints()` is used to configure the endpoint on the backend for clients and server to link (`/payroll`).
-* `configureMessageBroker()` is used to configure the broker used to relay messages between server and client.
+<1> `@EnableWebSocketMessageBroker` turns on WebSocket support.
+<2> `WebSocketMessageBrokerConfigurer` provides a convenient base class to configure basic features.
+<3> *MESSAGE_PREFIX* is the prefix you will prepend to every message's route.
+<4> `registerStompEndpoints()` is used to configure the endpoint on the backend for clients and server to link (`/payroll`).
+<5> `configureMessageBroker()` is used to configure the broker used to relay messages between server and client.
+====
-With this configuration, it's now possible to tap into Spring Data REST events and publish them over a WebSocket.
+With this configuration, you can now tap into Spring Data REST events and publish them over a WebSocket.
-== Subscribing to Spring Data REST events
+== Subscribing to Spring Data REST Events
-Spring Data REST generates several http://docs.spring.io/spring-data/rest/docs/current/reference/html/#events[application events] based on actions occurring on the repositories. The follow code shows how to subscribe to some of these events:
+Spring Data REST generates several https://docs.spring.io/spring-data/rest/docs/current/reference/html/#events[application events] based on actions occurring on the repositories. The following code shows how to subscribe to some of these events:
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/EventHandler.java[tag=code]
----
-* `@RepositoryEventHandler(Employee.class)` flags this class to trap events based on *employees*.
-* `SimpMessagingTemplate` and `EntityLinks` are autowired from the application context.
-* The `@HandleXYZ` annotations flag the methods that need to listen to. These methods must be public.
+<1> `@RepositoryEventHandler(Employee.class)` flags this class to trap events based on *employees*.
+<2> `SimpMessagingTemplate` and `EntityLinks` are autowired from the application context.
+<3> The `@HandleXYZ` annotations flag the methods that need to listen to events. These methods must be public.
+====
Each of these handler methods invokes `SimpMessagingTemplate.convertAndSend()` to transmit a message over the WebSocket. This is a pub-sub approach so that one message is relayed to every attached consumer.
-The route of each message is different, allowing multiple messages to be sent to distinct receivers on the client while only needing one open WebSocket, a resource-efficient approach.
+The route of each message is different, allowing multiple messages to be sent to distinct receivers on the client while needing only one open WebSocket -- a resource-efficient approach.
`getPath()` uses Spring Data REST's `EntityLinks` to look up the path for a given class type and id. To serve the client's needs, this `Link` object is converted to a Java URI with its path extracted.
NOTE: `EntityLinks` comes with several utility methods to programmatically find the paths of various resources, whether single or for collections.
-In essense, you are listening for create, update, and delete events, and after they are completed, sending notice of them to all clients. It's also possible to intercept such operations BEFORE they happen, and perhaps log them, block them for some reason, or decorate the domain objects with extra information. (In the next section, we'll see a VERY handy use for this!)
+In essence, you are listening for create, update, and delete events, and, after they are completed, sending notice of them to all clients. You can also intercept such operations BEFORE they happen, and perhaps log them, block them for some reason, or decorate the domain objects with extra information. (In the next section, we will see a handy use for this.)
== Configuring a JavaScript WebSocket
-Next step is to write some client-side code to consume WebSocket events. The follow chunk in them main app pulls in a module.
+The next step is to write some client-side code to consume WebSocket events. The following chunk in the main application pulls in a module:
+====
[source,javascript]
----
var stompClient = require('./websocket-listener')
----
+====
That module is shown below:
+====
[source,javascript]
----
-include::src/main/resources/static/websocket-listener.js[]
+include::src/main/js/websocket-listener.js[]
----
-<1> You pull in the SockJS JavaScript library for talking over WebSockets.
-<2> You pull in the stomp-websocket JavaScript library to use the STOMP sub-protocol.
-<3> Here is where the WebSocket is pointed at the application's `/payroll` endpoint.
-<4> Iterate over the array of `registrations` supplied so each can subscribe for callback as messages arrive.
+<1> Pull in the SockJS JavaScript library for talking over WebSockets.
+<2> Pull in the stomp-websocket JavaScript library to use the STOMP sub-protocol.
+<3> Point the WebSocket at the application's `/payroll` endpoint.
+<4> Iterate over the array of `registrations` supplied so that each can subscribe for callback as messages arrive.
+====
Each registration entry has a `route` and a `callback`. In the next section, you can see how to register event handlers.
-== Registering for WebSocket events
+== Registering for WebSocket Events
-In React, a component's `componentDidMount()` is the function that gets called after it has been rendered in the DOM. That is also the right time to register for WebSocket events, because the component is now online and ready for business. Checkout the code below:
+In React, a component's `componentDidMount()` function gets called after it has been rendered in the DOM. That is also the right time to register for WebSocket events, because the component is now online and ready for business. The following code does so:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=register-handlers]
+include::src/main/js/app.js[tag=register-handlers]
----
+====
The first line is the same as before, where all the employees are fetched from the server using page size. The second line shows an array of JavaScript objects being registered for WebSocket events, each with a `route` and a `callback`.
-When a new employee is created, the behavior is to refresh the data set and then use the paging links to navigate to the *last* page. Why refresh the data before navigating to the end? It's possible that adding a new record causes a new page to get created. While it's possible to calculate if this will happen, it subverts the point of hypermedia. Instead of cobbling together customize page counts, it's better to use existing links and only go down that road if there is a performance-driving reason to do so.
+When a new employee is created, the behavior is to refresh the data set and then use the paging links to navigate to the *last* page. Why refresh the data before navigating to the end? It is possible that adding a new record causes a new page to get created. While it is possible to calculate if this will happen, it subverts the point of hypermedia. Instead of cobbling together customized page counts, it is better to use existing links and only go down that road if there is a performance-driving reason to do so.
-When an employee is updated or deleted, the behavior is to refresh the current page. When you update a record, it impacts the page your are viewing. When you delete a record on the current page, a record from the next page will get pulled into the current one, hence the need to also refresh the current page.
+When an employee is updated or deleted, the behavior is to refresh the current page. When you update a record, it impacts the page your are viewing. When you delete a record on the current page, a record from the next page will get pulled into the current one -- hence the need to also refresh the current page.
-NOTE: There is no requirement for these WebSocket messages to start with `/topic`. It is simply a common convention that indicates pub-sub semantics.
+NOTE: There is no requirement for these WebSocket messages to start with `/topic`. It is a common convention that indicates pub-sub semantics.
In the next section, you can see the actual operations to perform these operations.
-== Reacting to WebSocket events and updating the UI state
+== Reacting to WebSocket Events and Updating the UI State
-The following chunk of code contains the two callbacks used to update UI state when a WebSocket event is received.
+The following chunk of code contains the two callbacks used to update UI state when a WebSocket event is received:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=websocket-handlers]
+include::src/main/js/app.js[tag=websocket-handlers]
----
+====
-`refreshAndGoToLastPage()` uses the familiar `follow()` function to navigate to the *employees* link with the *size* parameter applied, plugging in `this.state.pageSize`. When the response is received, you then invoke the same `onNavigate()` function from the last section, and jump to the *last* page, the one where the new record will be found.
+`refreshAndGoToLastPage()` uses the familiar `follow()` function to navigate to the `employees` link with the `size` parameter applied, plugging in `this.state.pageSize`. When the response is received, you then invoke the same `onNavigate()` function from the last section and jump to the *last* page, the one where the new record will be found.
-`refreshCurrentPage()` also uses the `follow()` function but applies `this.state.pageSize` to *size* and `this.state.page.number` to *page*. This fetches the same page you are currently looking at and updates the state accordingly.
+`refreshCurrentPage()` also uses the `follow()` function but applies `this.state.pageSize` to `size` and `this.state.page.number` to `page`. This fetches the same page you are currently looking at and updates the state accordingly.
-NOTE: This behavior tells every client to refresh their current page when an update or delete message is sent. It's possible that their current page may have nothing to do with the current event. However, it can be tricky to figure that out. What if the record that was deleted was on page two and you are looking at page three? Every entry would change. But is this desired behavior at all? Maybe, maybe not.
+NOTE: This behavior tells every client to refresh their current page when an update or delete message is sent. It is possible that their current page may have nothing to do with the current event. However, it can be tricky to figure that out. What if the record that was deleted was on page two and you are looking at page three? Every entry would change. But is this desired behavior at all? Maybe. Maybe not.
-== Moving state management out of the local updates
+== Moving State Management Out of the Local Updates
Before you finish this section, there is something to recognize. You just added a new way for the state in the UI to get updated: when a WebSocket message arrives. But the old way to update the state is still there.
-To simplify your code's management of state, it simplifies things if you remove the old way. In other words, submit your *POST*, *PUT*, and *DELETE* calls, but don't use their results to update the UI's state. Instead, wait for the WebSocket event to circle back and then do the update.
+To simplify your code's management of state, remove the old way. In other words, submit your `POST`, `PUT`, and `DELETE` calls, but do not use their results to update the UI's state. Instead, wait for the WebSocket event to circle back and then do the update.
The follow chunk of code shows the same `onCreate()` function as the previous section, only simplified:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=on-create]
+include::src/main/js/app.js[tag=on-create]
----
+====
-Here, the `follow()` function is used to get to the *employees* link, and then the *POST* operation is applied. Notice how `client({method: 'GET' ...})` has no `then()` or `done()` like before? The event handler to listen for updates is now found in `refreshAndGoToLastPage()` which you just looked at.
+Here, the `follow()` function is used to get to the `employees` link, and then the `POST` operation is applied. Notice how `client({method: 'GET' ...})` has no `then()` or `done()`, as before? The event handler to listen for updates is now found in `refreshAndGoToLastPage()`, which you just looked at.
-== Putting it all together
+== Putting It All Together
-With all these mods in place, fire up the app (`./mvnw spring-boot:run`) and poke around with it. Open up two browser tabs and resize so you can see them both. Start making updates in one and see how they instantly update the other tab. Open up your phone and visit the same page. Find a friend and ask him or her to do the same thing. You might find this type of dynamic updating more keen.
+With all these modifications in place, fire up the application (`./mvnw spring-boot:run`) and poke around with it. Open up two browser tabs and resize so you can see them both. Start making updates in one and see how they instantly update the other tab. Open up your phone and visit the same page. Find a friend and ask that person to do the same thing. You might find this type of dynamic updating more keen.
-Want a challenge? Try the exercise from the previous section where you open the same record in two different browser tabs. Try to update it in one and NOT see it update in the other. If it's possible, the conditional PUT code should still protect you. But it may be trickier to pull that off!
+Want a challenge? Try the exercise from the previous section where you open the same record in two different browser tabs. Try to update it in one and NOT see it update in the other. If it is possible, the conditional `PUT` code should still protect you. But it may be trickier to pull that off!
== Review
-In this section:
+In this section, you:
-* You configured Spring's WebSocket suport with SockJS fallback.
-* You subscribed for create, update, and delete events from Spring Data REST to dynamically update the UI.
-* You published the URI of affected REST resources along with a contextual message ("/topic/newEmployee", "/topic/updateEmployee", etc.).
-* You registered WebSocket listeners in the UI to listen for these events.
-* You wired the listeners to handlers to update the UI state.
+* Configured Spring's WebSocket support with a SockJS fallback.
+* Subscribed for create, update, and delete events from Spring Data REST to dynamically update the UI.
+* Published the URI of affected REST resources along with a contextual message ("/topic/newEmployee", "/topic/updateEmployee", and so on).
+* Registered WebSocket listeners in the UI to listen for these events.
+* Wired the listeners to handlers to update the UI state.
-With all these features, it's easy to run two browsers, side-by-side, and see how updating one ripples to the other.
+With all these features, it is easy to run two browsers, side-by-side, and see how updating one ripples to the other.
Issues?
While multiple displays nicely update, polishing the precise behavior is warranted. For example, creating a new user will cause ALL users to jump to the end. Any thoughts on how this should be handled?
-Paging is useful, but offers a tricky state to manage. The costs are low on this sample app, and React at very efficient at updating the DOM without causing lots of flickering in the UI. But with a more complex app, not all of these approaches will fit.
+Paging is useful, but it offers a tricky state to manage. The costs are low on this sample application, and React is very efficient at updating the DOM without causing lots of flickering in the UI. But with a more complex application, not all of these approaches will fit.
-When designing with paging in mind, you have to decide what is the expected behavior between clients and if there needs to updates or not. Depending on your requirements and performance of the system, the existing navigational hypermedia may be sufficent.
+When designing with paging in mind, you have to decide what is the expected behavior between clients and if there needs to be updates or not. Depending on your requirements and performance of the system, the existing navigational hypermedia may be sufficient.
diff --git a/events/mvnw b/events/mvnw
index c67cd41..a16b543 100755
--- a/events/mvnw
+++ b/events/mvnw
@@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# 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
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Maven2 Start Up Batch script
+# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
@@ -54,38 +54,16 @@ case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
- #
- # Look for the Apple JDKs first to preserve the existing behaviour, and then look
- # for the new JDKs provided by Oracle.
- #
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
- #
- # Oracle JDKs
- #
- export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
- fi
-
- if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
- #
- # Apple JDKs
- #
- export JAVA_HOME=`/usr/libexec/java_home`
- fi
- ;;
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
esac
if [ -z "$JAVA_HOME" ] ; then
@@ -130,13 +108,12 @@ if $cygwin ; then
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
-# For Migwn, ensure paths are in UNIX format before anything is touched
+# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
- # TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
@@ -184,27 +161,28 @@ fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin; then
- [ -n "$M2_HOME" ] &&
- M2_HOME=`cygpath --path --windows "$M2_HOME"`
- [ -n "$JAVA_HOME" ] &&
- JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
- [ -n "$CLASSPATH" ] &&
- CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
-fi
-
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
- local basedir=$(pwd)
- local wdir=$(pwd)
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
while [ "$wdir" != '/' ] ; do
- wdir=$(cd "$wdir/.."; pwd)
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
done
echo "${basedir}"
}
@@ -216,10 +194,109 @@ concat_lines() {
fi
}
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
-# Provide a "standardized" way to retrieve the CLI args that will
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
@@ -228,7 +305,6 @@ WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
- -classpath "./.mvn/wrapper/maven-wrapper.jar" \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
- ${WRAPPER_LAUNCHER} "$@"
-
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/hypermedia/mvnw.bat b/events/mvnw.cmd
similarity index 68%
rename from hypermedia/mvnw.bat
rename to events/mvnw.cmd
index 7ca42b9..c8d4337 100644
--- a/hypermedia/mvnw.bat
+++ b/events/mvnw.cmd
@@ -7,7 +7,7 @@
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@@ -18,7 +18,7 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
-@REM Maven2 Start Up Batch script
+@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@@ -26,7 +26,7 @@
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -35,7 +35,9 @@
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
-@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
@@ -66,7 +68,7 @@ echo.
goto error
:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto chkMHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
@@ -76,42 +78,10 @@ echo location of your Java installation. >&2
echo.
goto error
-:chkMHome
-if not "%M2_HOME%"=="" goto valMHome
-
-SET "M2_HOME=%~dp0.."
-if not "%M2_HOME%"=="" goto valMHome
-
-echo.
-echo Error: M2_HOME not found in your environment. >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation. >&2
-echo.
-goto error
-
-:valMHome
-
-:stripMHome
-if not "_%M2_HOME:~-1%"=="_\" goto checkMCmd
-set "M2_HOME=%M2_HOME:~0,-1%"
-goto stripMHome
-
-:checkMCmd
-if exist "%M2_HOME%\bin\mvn.cmd" goto init
-
-echo.
-echo Error: M2_HOME is set to an invalid directory. >&2
-echo M2_HOME = "%M2_HOME%" >&2
-echo Please set the M2_HOME variable in your environment to match the >&2
-echo location of the Maven installation >&2
-echo.
-goto error
@REM ==== END VALIDATION ====
:init
-set MAVEN_CMD_LINE_ARGS=%*
-
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
@@ -147,13 +117,48 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-
-for %%i in ("%M2_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i"
-
-set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.home=%M2_HOME%" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
diff --git a/security/src/main/resources/static/package.json b/events/package.json
similarity index 61%
rename from security/src/main/resources/static/package.json
rename to events/package.json
index 13ae0f6..977b4e9 100644
--- a/security/src/main/resources/static/package.json
+++ b/events/package.json
@@ -14,21 +14,27 @@
"react"
],
"author": "Greg L. Turnquist",
- "license": "ASLv2",
+ "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": {
- "babel-core": "^5.8.25",
- "babel-loader": "^5.3.2",
- "react": "^0.13.3",
+ "react": "^16.5.2",
+ "react-dom": "^16.5.2",
"rest": "^1.3.1",
"sockjs-client": "^1.0.3",
- "stompjs": "^2.3.3",
- "webpack": "^1.12.2"
+ "stompjs": "^2.3.3"
},
"scripts": {
- "watch": "webpack --watch -d"
+ "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js"
+ },
+ "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"
}
}
diff --git a/events/pom.xml b/events/pom.xml
index 492eaec..21d2e5f 100644
--- a/events/pom.xml
+++ b/events/pom.xml
@@ -1,17 +1,15 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- org.springframework.boot
- spring-boot-starter-parent
- 1.3.3.RELEASE
-
+ com.greglturnquist
+ react-and-spring-data-rest
+ 0.0.1-SNAPSHOT
- com.greglturnquistreact-and-spring-data-rest-events0.0.1-SNAPSHOTjar
@@ -47,12 +45,6 @@
spring-boot-starter-websocket
-
- org.projectlombok
- lombok
- 1.16.4
- provided
- com.h2databaseh2
@@ -72,41 +64,10 @@
spring-boot-maven-plugin
-
- com.github.eirslett
- frontend-maven-plugin
- 0.0.24
-
- src/main/resources/static
-
-
-
- install node and npm
-
- install-node-and-npm
-
-
- v0.10.33
- 1.3.8
-
-
-
- npm install
-
- npm
-
-
- install
-
-
-
- webpack build
-
- webpack
-
-
-
-
+
+ com.github.eirslett
+ frontend-maven-plugin
+ com.rimerosolutions.maven.plugins
diff --git a/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
index 7afedce..cb339c2 100644
--- a/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
+++ b/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/events/src/main/java/com/greglturnquist/payroll/Employee.java b/events/src/main/java/com/greglturnquist/payroll/Employee.java
index 654a1a7..aedf65d 100644
--- a/events/src/main/java/com/greglturnquist/payroll/Employee.java
+++ b/events/src/main/java/com/greglturnquist/payroll/Employee.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,20 +15,19 @@
*/
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 lombok.Data;
-
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Greg Turnquist
*/
// tag::code[]
-@Data
@Entity
public class Employee {
@@ -46,5 +45,74 @@ public Employee(String firstName, String lastName, String description) {
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[]
\ No newline at end of file
diff --git a/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
index a678141..748125c 100644
--- a/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
+++ b/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/events/src/main/java/com/greglturnquist/payroll/EventHandler.java b/events/src/main/java/com/greglturnquist/payroll/EventHandler.java
index 743ef0b..07f4bcd 100644
--- a/events/src/main/java/com/greglturnquist/payroll/EventHandler.java
+++ b/events/src/main/java/com/greglturnquist/payroll/EventHandler.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -22,7 +22,7 @@
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.hateoas.server.EntityLinks;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
@@ -31,10 +31,10 @@
*/
// tag::code[]
@Component
-@RepositoryEventHandler(Employee.class)
+@RepositoryEventHandler(Employee.class) // <1>
public class EventHandler {
- private final SimpMessagingTemplate websocket;
+ private final SimpMessagingTemplate websocket; // <2>
private final EntityLinks entityLinks;
@@ -44,19 +44,19 @@ public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) {
this.entityLinks = entityLinks;
}
- @HandleAfterCreate
+ @HandleAfterCreate // <3>
public void newEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/newEmployee", getPath(employee));
}
- @HandleAfterDelete
+ @HandleAfterDelete // <3>
public void deleteEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/deleteEmployee", getPath(employee));
}
- @HandleAfterSave
+ @HandleAfterSave // <3>
public void updateEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/updateEmployee", getPath(employee));
@@ -68,7 +68,7 @@ public void updateEmployee(Employee employee) {
* @param employee
*/
private String getPath(Employee employee) {
- return this.entityLinks.linkForSingleResource(employee.getClass(),
+ return this.entityLinks.linkForItemResource(employee.getClass(),
employee.getId()).toUri().getPath();
}
diff --git a/events/src/main/java/com/greglturnquist/payroll/HomeController.java b/events/src/main/java/com/greglturnquist/payroll/HomeController.java
index 465724d..abaaba2 100644
--- a/events/src/main/java/com/greglturnquist/payroll/HomeController.java
+++ b/events/src/main/java/com/greglturnquist/payroll/HomeController.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
index 0ba0a93..7b2fc31 100644
--- a/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
+++ b/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java b/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java
index d7965cf..871a1ba 100644
--- a/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java
+++ b/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -17,29 +17,29 @@
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.stereotype.Component;
-import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
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 extends AbstractWebSocketMessageBrokerConfigurer {
+@EnableWebSocketMessageBroker // <1>
+public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { // <2>
- static final String MESSAGE_PREFIX = "/topic";
+ static final String MESSAGE_PREFIX = "/topic"; // <3>
@Override
- public void registerStompEndpoints(StompEndpointRegistry registry) {
+ public void registerStompEndpoints(StompEndpointRegistry registry) { // <4>
registry.addEndpoint("/payroll").withSockJS();
}
@Override
- public void configureMessageBroker(MessageBrokerRegistry registry) {
+ public void configureMessageBroker(MessageBrokerRegistry registry) { // <5>
registry.enableSimpleBroker(MESSAGE_PREFIX);
registry.setApplicationDestinationPrefixes("/app");
}
}
-// end::code[]
\ No newline at end of file
+// end::code[]
diff --git a/basic/src/main/resources/static/api/uriListConverter.js b/events/src/main/js/api/uriListConverter.js
similarity index 81%
rename from basic/src/main/resources/static/api/uriListConverter.js
rename to events/src/main/js/api/uriListConverter.js
index 1c2124e..8d9dc2e 100644
--- a/basic/src/main/resources/static/api/uriListConverter.js
+++ b/events/src/main/js/api/uriListConverter.js
@@ -9,9 +9,7 @@ define(function() {
write: function(obj /*, opts */) {
// If this is an Array, extract the self URI and then join using a newline
if (obj instanceof Array) {
- return obj.map(function(resource) {
- return resource._links.self.href;
- }).join('\n');
+ return obj.map(resource => resource._links.self.href).join('\n');
} else { // otherwise, just return the self URI
return obj._links.self.href;
}
diff --git a/hypermedia/src/main/resources/static/api/uriTemplateInterceptor.js b/events/src/main/js/api/uriTemplateInterceptor.js
similarity index 62%
rename from hypermedia/src/main/resources/static/api/uriTemplateInterceptor.js
rename to events/src/main/js/api/uriTemplateInterceptor.js
index 269165f..c16ba33 100644
--- a/hypermedia/src/main/resources/static/api/uriTemplateInterceptor.js
+++ b/events/src/main/js/api/uriTemplateInterceptor.js
@@ -1,11 +1,11 @@
define(function(require) {
'use strict';
- var interceptor = require('rest/interceptor');
+ const interceptor = require('rest/interceptor');
return interceptor({
request: function (request /*, config, meta */) {
- /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */
+ /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */
if (request.path.indexOf('{') === -1) {
return request;
} else {
diff --git a/events/src/main/resources/static/app.js b/events/src/main/js/app.js
similarity index 85%
rename from events/src/main/resources/static/app.js
rename to events/src/main/js/app.js
index e8314fd..d3cafd6 100644
--- a/events/src/main/resources/static/app.js
+++ b/events/src/main/js/app.js
@@ -1,6 +1,7 @@
'use strict';
const React = require('react');
+const ReactDOM = require('react-dom');
const when = require('when');
const client = require('./client');
@@ -132,7 +133,11 @@ class App extends React.Component {
rel: 'employees',
params: {size: this.state.pageSize}
}]).done(response => {
- this.onNavigate(response.entity._links.last.href);
+ if (response.entity._links.last !== undefined) {
+ this.onNavigate(response.entity._links.last.href);
+ } else {
+ this.onNavigate(response.entity._links.self.href);
+ }
})
}
@@ -205,22 +210,22 @@ class CreateDialog extends React.Component {
handleSubmit(e) {
e.preventDefault();
- var newEmployee = {};
+ const newEmployee = {};
this.props.attributes.forEach(attribute => {
- newEmployee[attribute] = React.findDOMNode(this.refs[attribute]).value.trim();
+ newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
this.props.attributes.forEach(attribute => {
- React.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
+ ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
});
window.location = "#";
}
render() {
- var inputs = this.props.attributes.map(attribute =>
-
Employees - Page {this.props.page.number + 1} of {this.props.page.totalPages}
: null;
- var employees = this.props.employees.map(employee =>
+ const employees = this.props.employees.map(employee =>
);
- var navLinks = [];
+ const navLinks = [];
if ("first" in this.props.links) {
navLinks.push();
}
@@ -365,14 +370,16 @@ class EmployeeList extends React.Component {
{pageInfo}
-
-
First Name
-
Last Name
-
Description
-
-
-
- {employees}
+
+
+
First Name
+
Last Name
+
Description
+
+
+
+ {employees}
+
{navLinks}
@@ -412,7 +419,7 @@ class Employee extends React.Component {
}
}
-React.render(
+ReactDOM.render(
,
document.getElementById('react')
)
diff --git a/events/src/main/js/client.js b/events/src/main/js/client.js
new file mode 100644
index 0000000..dfecbea
--- /dev/null
+++ b/events/src/main/js/client.js
@@ -0,0 +1,19 @@
+'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' }});
diff --git a/conditional/src/main/resources/static/follow.js b/events/src/main/js/follow.js
similarity index 90%
rename from conditional/src/main/resources/static/follow.js
rename to events/src/main/js/follow.js
index 5da616c..59efe70 100644
--- a/conditional/src/main/resources/static/follow.js
+++ b/events/src/main/js/follow.js
@@ -1,11 +1,11 @@
module.exports = function follow(api, rootPath, relArray) {
- var root = api({
+ const root = api({
method: 'GET',
path: rootPath
});
return relArray.reduce(function(root, arrayItem) {
- var rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel;
+ const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel;
return traverseNext(root, rel, arrayItem);
}, root);
diff --git a/events/src/main/resources/static/websocket-listener.js b/events/src/main/js/websocket-listener.js
similarity index 69%
rename from events/src/main/resources/static/websocket-listener.js
rename to events/src/main/js/websocket-listener.js
index 6807f44..c87a1c5 100644
--- a/events/src/main/resources/static/websocket-listener.js
+++ b/events/src/main/js/websocket-listener.js
@@ -1,11 +1,11 @@
'use strict';
-var SockJS = require('sockjs-client'); // <1>
+const SockJS = require('sockjs-client'); // <1>
require('stompjs'); // <2>
function register(registrations) {
- var socket = SockJS('/payroll'); // <3>
- var stompClient = Stomp.over(socket);
+ 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);
diff --git a/events/src/main/resources/static/client.js b/events/src/main/resources/static/client.js
deleted file mode 100644
index 8e542c9..0000000
--- a/events/src/main/resources/static/client.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-var rest = require('rest');
-var defaultRequest = require('rest/interceptor/defaultRequest');
-var mime = require('rest/interceptor/mime');
-var uriTemplateInterceptor = require('./api/uriTemplateInterceptor');
-var errorCode = require('rest/interceptor/errorCode');
-var baseRegistry = require('rest/mime/registry');
-
-var 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' }});
diff --git a/events/src/main/resources/static/webpack.config.js b/events/src/main/resources/static/webpack.config.js
deleted file mode 100644
index 1ff3682..0000000
--- a/events/src/main/resources/static/webpack.config.js
+++ /dev/null
@@ -1,29 +0,0 @@
-var path = require('path');
-
-var node_dir = __dirname + '/node_modules';
-
-module.exports = {
- entry: './app.js',
- devtool: 'sourcemaps',
- cache: true,
- debug: true,
- resolve: {
- alias: {
- 'stompjs': node_dir + '/stompjs/lib/stomp.js',
- 'when': node_dir + '/rest/node_modules/when/when.js'
- }
- },
- output: {
- path: __dirname,
- filename: './built/bundle.js'
- },
- module: {
- loaders: [
- {
- test: path.join(__dirname, '.'),
- exclude: /(node_modules)/,
- loader: 'babel-loader'
- }
- ]
- }
-};
\ No newline at end of file
diff --git a/events/src/main/resources/templates/index.html b/events/src/main/resources/templates/index.html
index 802ffdb..c5f2c1e 100644
--- a/events/src/main/resources/templates/index.html
+++ b/events/src/main/resources/templates/index.html
@@ -1,5 +1,5 @@
-
+
ReactJS + Spring Data REST
diff --git a/events/webpack.config.js b/events/webpack.config.js
new file mode 100644
index 0000000..8f4b525
--- /dev/null
+++ b/events/webpack.config.js
@@ -0,0 +1,31 @@
+var path = require('path');
+
+module.exports = {
+ entry: './src/main/js/app.js',
+ devtool: 'sourcemaps',
+ 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'
+ },
+ module: {
+ rules: [
+ {
+ test: path.join(__dirname, '.'),
+ exclude: /(node_modules)/,
+ use: [{
+ loader: 'babel-loader',
+ options: {
+ presets: ["@babel/preset-env", "@babel/preset-react"]
+ }
+ }]
+ }
+ ]
+ }
+};
\ No newline at end of file
diff --git a/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java b/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/hypermedia/.mvn/wrapper/maven-wrapper.jar b/hypermedia/.mvn/wrapper/maven-wrapper.jar
index 5fd4d50..2cc7d4a 100644
Binary files a/hypermedia/.mvn/wrapper/maven-wrapper.jar and b/hypermedia/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/hypermedia/.mvn/wrapper/maven-wrapper.properties b/hypermedia/.mvn/wrapper/maven-wrapper.properties
index eb91947..642d572 100644
--- a/hypermedia/.mvn/wrapper/maven-wrapper.properties
+++ b/hypermedia/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/hypermedia/README.adoc b/hypermedia/README.adoc
index fad6cfc..ce29225 100644
--- a/hypermedia/README.adoc
+++ b/hypermedia/README.adoc
@@ -2,50 +2,53 @@
= Part 2 - Hypermedia Controls
:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master
-In the <>, you found out how to stand up a backend payroll service to store employee data using Spring Data REST. A key feature it lacked was using the hypermedia controls and navigation by links. Instead, it hard coded the path to find data.
+In the <>, you found out how to create a backend payroll service to store employee data by using Spring Data REST. A key feature it lacked was using the hypermedia controls and navigation by links. Instead, it hard-coded the path to find data.
-Feel free to {sourcedir}/hypermedia[grab the code] from this repository and follow along. This section is based on the previous section's app with extra things added.
+Feel free to {sourcedir}/hypermedia[grab the code] from this repository and follow along. This section is based on the previous section's application, with extra things added.
-== In the beginning there was data...and then there was REST
+== In the Beginning, There Was Data...and Then There Was REST
-[quote, Roy T. Fielding, http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven]
+[quote, Roy T. Fielding, https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven]
I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC....What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?
-So, what exactly ARE hypermedia controls, i.e. hypertext, and how can you use them? To find out, let's take a step back and look at the core mission of REST.
+So, what exactly ARE hypermedia controls (that is, hypertext) and how can you use them? To find out, we take a step back and look at the core mission of REST.
-The concept of REST was to borrow ideas that made the web so successful and apply them to APIs. Despite the web's vast size, dynamic nature, and low rate that clients, i.e. browsers, are updated, the web is an amazing success. Roy Fielding sought to use some of its constraints and features and see if that would afford similar expansion of API production and consumption.
+The concept of REST was to borrow ideas that made the web so successful and apply them to APIs. Despite the web's vast size, dynamic nature, and low rate at which clients (that is, browsers) are updated, the web is an amazing success. Roy Fielding sought to use some of its constraints and features and see if that would afford similar expansion of API production and consumption.
-One of the constraints is to limit the number of verbs. For REST, the primary ones are GET, POST, PUT, DELETE, and PATCH. There are others, but we won't get into them here.
+One of the constraints is to limit the number of verbs. For REST, the primary ones are GET, POST, PUT, DELETE, and PATCH. There are others, but we will not get into them here.
-* GET - fetch the state of a resource without altering the system
-* POST - create a new resource without saying where
-* PUT - replace an existing resource, overwriting whatever else is already there (if anything)
-* DELETE - remove an existing resource
-* PATCH - alter an existing resource partially
+* GET: Fetches the state of a resource without altering the system
+* POST: Creates a new resource without saying where
+* PUT: Replaces an existing resource, overwriting whatever else (if anything) is already there
+* DELETE: Removes an existing resource
+* PATCH: Alters an existing resource (partially rather than creating a new resource)
-These are standardized HTTP verbs with well written specs. By picking up and using already coined HTTP operations, we don't have to invent a new language and educate the industry.
+These are standardized HTTP verbs with well known specifications. By picking up and using already coined HTTP operations, we need not invent a new language and educate the industry.
-Another constraint of REST is to use media types to define the format of data. Instead of everyone writing their own dialect for the exchange of information, it would be prudent to develop some media types. One of the most popular ones to be accepted is HAL, media type application/hal+json. It is Spring Data REST's default media type. A keen value is that there is no centralized, single media type for REST. Instead, people can develop media types and plug them in. Try them out. As different needs become available, the industry can flexibly move.
+Another constraint of REST is to use media types to define the format of data. Instead of everyone writing their own dialect for the exchange of information, it would be prudent to develop some media types. One of the most popular ones to be accepted is HAL, media type `application/hal+json`. It is Spring Data REST's default media type. A key value is that there is no centralized, single media type for REST. Instead, people can develop media types and plug them in and try them out. As different needs become available, the industry can flexibly move.
-A key feature of REST is to include links to relevant resources. For example, if you were looking at an order, a RESTful API would include a link to the related customer, links to the catalog of items, and perhaps a link to the store from which the order was placed. In this section, you will introduce paging, and see how to also use navigational paging links.
+A key feature of REST is to include links to relevant resources. For example, if you were looking at an order, a RESTful API would include a link to the related customer, links to the catalog of items, and perhaps a link to the store from which the order was placed. In this section, you will introduce paging and see how to also use navigational paging links.
-== Turning on paging from the backend
+== Turning on Paging from the Backend
-To get underway with using frontend hypermedia controls, you need to turn on some extra controls. Spring Data REST provides paging support. To use it, just tweak the repository definition:
+To get underway with using frontend hypermedia controls, you need to turn on some extra controls. Spring Data REST provides paging support. To use it, tweak the repository definition as follows:
.src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
+====
[source,java]
----
include::src/main/java/com/greglturnquist/payroll/EmployeeRepository.java[tag=code]
----
+====
-Your interface now extends `PagingAndSortingRepository` which adds extra options to set page size, and also adds navigational links to hop from page to page. The rest of the backend is the same (exception for some https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[extra pre-loaded data] to make things interesting).
+Your interface now extends `PagingAndSortingRepository`, which adds extra options to set page size and adds navigational links to hop from page to page. The rest of the backend is the same (except for some https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[extra pre-loaded data] to make things interesting).
-Restart the application (`./mvnw spring-boot:run`) and see how it works.
+Restart the application (`./mvnw spring-boot:run`) and see how it works. Then run the following command (shown with its output) to see the paging in action:
+====
----
-$ curl localhost:8080/api/employees?size=2
+$ curl "localhost:8080/api/employees?size=2"
{
"_links" : {
"first" : {
@@ -90,11 +93,13 @@ $ curl localhost:8080/api/employees?size=2
}
}
----
+====
-The default page size is 20, so to see it in action, `?size=2` applied. As expected, only two employees are listed. In addition, there is also a *first*, *next*, and *last* link. There is also the *self* link, free of context _including page parameters_.
+The default page size is 20, but we do not have that much data. So, to see it in action, we set `?size=2`. As expected, only two employees are listed. In addition, there are also `first`, `next`, and `last` links. There is also the `self` link, which is free of context, _including page parameters_.
-If you navigate to the *next* link, you'll then see a *prev* link as well:
+If you navigate to the `next` link, you'll see a `prev` link as well. The following command (shown with its output) does so:
+====
----
$ curl "http://localhost:8080/api/employees?page=1&size=2"
{
@@ -117,58 +122,66 @@ $ curl "http://localhost:8080/api/employees?page=1&size=2"
},
...
----
+====
-NOTE: When using "&" in URL query parameters, the command line thinks it's a line break. Wrap the whole URL with quotation marks to bypass that.
+NOTE: When using `&` in URL query parameters, the command line thinks it is a line break. Wrap the whole URL with quotation marks to avoid that problem.
-That looks neat, but it will be even better when you update the frontend to take advantage of that.
+That looks neat, but it will be even better when you update the frontend to take advantage of it.
-== Navigating by relationship
+== Navigating by Relationship
-That's it! No more changes are needed on the backend to start using the hypermedia controls Spring Data REST provides out of the box. You can switch to working on the frontend. (That's part of the beauty of Spring Data REST. No messy controller updates!)
+No more changes are needed on the backend to start using the hypermedia controls Spring Data REST provides out of the box. You can switch to working on the frontend. (That is part of the beauty of Spring Data REST: No messy controller updates!)
-NOTE: It's important to point out, this application isn't "Spring Data REST-specific." Instead, it uses http://stateless.co/hal_specification.html[HAL], https://tools.ietf.org/html/rfc6570[URI Templates], and other standards. That's how using rest.js is a snap: that library comes with HAL support.
+NOTE: It is important to point out that this application is not "`Spring Data REST-specific.`" Instead, it uses http://stateless.co/hal_specification.html[HAL], https://tools.ietf.org/html/rfc6570[URI Templates], and other standards. That is why using rest.js is a snap: That library comes with HAL support.
-In the previous section, you hardcoded the path to `/api/employees`. Instead, the ONLY path you should hardcode is the root.
+In the previous section, you hardcoded the path to `/api/employees`. Instead, the ONLY path you should hardcode is the root, as follows
+====
[source,javascript]
----
...
var root = '/api';
...
----
+====
-With a handy little https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/resources/static/follow.js[`follow()` function], you can now start from the root and navigate to where you need!
+With a handy little https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/js/follow.js[`follow()` function], you can now start from the root and navigate to where you want, as follows:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=follow-1]
+include::src/main/js/app.js[tag=follow-1]
----
+====
-In the previous section, the loading was done directly inside `componentDidMount()`. In this section, we are making it possible to reload the entire list of employees when the page size is updated. To do so, we have moved things into `loadFromServer()`.
+In the previous section, the loading was done directly inside `componentDidMount()`. In this section, we are making it possible to reload the entire list of employees when the page size is updated. To do so, we have moved things into `loadFromServer()`, as follows:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=follow-2]
+include::src/main/js/app.js[tag=follow-2]
----
+====
-`loadFromServer` is very similar the previous section, but instead if uses `follow()`:
+`loadFromServer` is very similar to the previous section. However, it uses `follow()`:
-* The first argument to the follow() function is the `client` object used to make REST calls.
+* The first argument to the `follow()` function is the `client` object used to make REST calls.
* The second argument is the root URI to start from.
* The third argument is an array of relationships to navigate along. Each one can be a string or an object.
-The array of relationships can be as simple as `["employees"]`, meaning when the first call is made, look in *_links* for the relationship (or *rel*) named *employees*. Find its *href* and navigate to it. If there is another relationship in the array, rinse and repeat.
+The array of relationships can be as simple as `["employees"]`, meaning when the first call is made, look in `_links` for the relationship (or `rel`) named `employees`. Find its `href` and navigate to it. If there is another relationship in the array, repeat the process.
-Sometimes, a rel by itself isn't enough. In this fragment of code, it also plugs in a query parameter of *?size=*. There are other options that can be supplied, as you'll see further along.
+Sometimes, a `rel` by itself is not enough. In this fragment of code, it also plugs in a query parameter of `?size=`. There are other options that can be supplied, as you will see later.
-== Grabbing JSON Schema metadata
+== Grabbing JSON Schema Metadata
-After navigating to *employees* with the size-based query, the *employeeCollection* is at your fingertips. In the previous section, we called it day and displayed that data inside ``. Today, you are performing another call to grab some http://json-schema.org/[JSON Schema metadata] found at `/api/profile/employees/`.
+After navigating to `employees` with the size-based query, the `employeeCollection` is available. In the previous section, we displayed that data inside ``. In this section, you are performing another call to grab some https://json-schema.org/[JSON Schema metadata] found at `/api/profile/employees/`.
-You can see the data yourself:
+You can see the data yourself by running the following `curl` command (shown with its output):
+====
----
-$ curl http://localhost:8080/api/profile/employees -H 'Accept:application/schema+json'
+$ curl http://localhost:8080/api/profile/employees -H "Accept:application/schema+json"
{
"title" : "Employee",
"properties" : {
@@ -190,71 +203,75 @@ $ curl http://localhost:8080/api/profile/employees -H 'Accept:application/schema
},
"definitions" : { },
"type" : "object",
- "$schema" : "http://json-schema.org/draft-04/schema#"
+ "$schema" : "https://json-schema.org/draft-04/schema#"
}
----
+====
-NOTE: The default form of metadata at /profile/employees is ALPS. In this case, though, you are using content negotation to fetch JSON Schema.
+NOTE: The default form of metadata at `/profile/employees` is http://alps.io[ALPS]. In this case, though, you are using content negotiation to fetch JSON Schema.
-By capturing this information in the`` component's state, you can make good use of it later on when building input forms.
+By capturing this information in the`` component's state, you can make good use of it later when building input forms.
[[creating-new-records]]
-== Creating new records
+== Creating New Records
-Equipped with this metadata, you can now add some extra controls to the UI. Create a new React component, ``.
+Equipped with this metadata, you can now add some extra controls to the UI. You can start by creating a new React component ``, as follows:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=create-dialog]
+include::src/main/js/app.js[tag=create-dialog]
----
+====
-This new component has both a `handleSubmit()` function as well as the expected `render()` function.
+This new component has both a `handleSubmit()` function and the expected `render()` function.
-Let's dig into these functions in reverse order, and first look at the `render()` function.
+We dig into these functions in reverse order, looking first at the `render()` function.
[[hypermedia-rendering]]
=== Rendering
-Your code maps over the JSON Schema data found in the *attributes* property and converts it into an array of `` elements.
+Your code maps over the JSON Schema data found in the `attributes` property and converts it into an array of `` elements.
-* *key* is again needed by React to distinguish between multiple child nodes.
-* It's a simple text-based entry field.
-* *placeholder* is where we can show the user with field is which.
-* You may used to having a *name* attribute, but it's not necessary. With React, *ref* is the mechanism to grab a particular DOM node (as you'll soon see).
+* `key` is again needed by React to distinguish between multiple child nodes.
+* It is a simple text-based entry field.
+* `placeholder` lets us show the user with field is which.
+* You may be used to having a `name` attribute, but it is not necessary. With React, `ref` is the mechanism for grabbing a particular DOM node (as you will soon see).
This represents the dynamic nature of the component, driven by loading data from the server.
-Inside this component's top-level `
` is an anchor tag and another `
`. The anchor tag is the button to open the dialog. And the nested `
` is the hidden dialog itself. In this example, you are use pure HTML5 and CSS3. No JavaScript at all! You can https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/resources/static/main.css[see the CSS code] used to show/hide the dialog. We won't dive into that here.
+Inside this component's top-level `
` is an anchor tag and another `
`. The anchor tag is the button to open the dialog. And the nested `
` is the hidden dialog itself. In this example, you are using pure HTML5 and CSS3. No JavaScript at all! You can https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/resources/static/main.css[see the CSS code] used to show and hide the dialog. We will not dive into that here.
Nestled inside `
` is a form where your dynamic list of input fields are injected followed by the *Create* button. That button has an `onClick={this.handleSubmit}` event handler. This is the React way of registering an event handler.
-NOTE: React doesn't create a fistful of event handlers on every DOM element. Instead, it has a https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation[much more performant and sophisticated] solution. The point being you don't have to manage that infrastructure and can instead focus on writing functional code.
+NOTE: React does not create event handlers on every DOM element. Instead, it has a https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation[much more performant and sophisticated] solution. You need not manage that infrastructure and can instead focus on writing functional code.
-=== Handling user input
+=== Handling User Input
-The `handleSubmit()` function first stops the event from bubbling further up the hierarchy. It then uses the same JSON Schema attribute property to find each `` using `React.findDOMNode(this.refs[attribute])`.
+The `handleSubmit()` function first stops the event from bubbling further up the hierarchy. It then uses the same JSON Schema attribute property to find each ``, by using `React.findDOMNode(this.refs[attribute])`.
-`this.refs` is a way to reach out and grab a particular React component by name. In that sense, you are ONLY getting the virtual DOM component. To grab the actual DOM element you need to use `React.findDOMNode()`.
+`this.refs` is a way to reach out and grab a particular React component by name. Note that you are getting ONLY the virtual DOM component. To grab the actual DOM element, you need to use `React.findDOMNode()`.
-After iterating over every input and building up the `newEmployee` object, we invoke a callback to `onCreate()` the new employee. This function is up top inside `App.onCreate` and was provided to this React component as another property. Look at how that top-level function operates:
+After iterating over every input and building up the `newEmployee` object, we invoke a callback to `onCreate()` for the new employee record. This function is inside `App.onCreate` and was provided to this React component as another property. Look at how that top-level function operates:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=create]
+include::src/main/js/app.js[tag=create]
----
+====
-Once again, use the `follow()` function to navigate to the *employees* resource where POST operations are performed. In this case, there was no need to apply any parameters, so the string-based array of rels is fine. In this situation, the POST call is returned. This allows the next `then()` clause to handle processing the outcome of the POST.
-
-New records are typically added to the end of the dataset. Since you are looking at a certain page, it's logical to expect the new employee record to not be on the current page. To handle this, you need to fetch a new batch of data with the same page size applied. That promise is returned for the final clause inside `done()`.
+Once again, we use the `follow()` function to navigate to the `employees` resource where POST operations are performed. In this case, there was no need to apply any parameters, so the string-based array of `rel` instance is fine. In this situation, the `POST` call is returned. This allows the next `then()` clause to handle processing the outcome of the `POST`.
-Since the user probably wants to see the newly created employee, you can then use the hypermedia controls and navigate to the *last* entry.
+New records are typically added to the end of the dataset. Since you are looking at a certain page, it is logical to expect the new employee record to not be on the current page. To handle this, you need to fetch a new batch of data with the same page size applied. That promise is returned for the final clause inside `done()`.
-This introduces the concept of paging in our UI. Let's tackle that next!
+Since the user probably wants to see the newly created employee, you can then use the hypermedia controls and navigate to the `last` entry.
[[NOTE]]
-====
-First time using a promise-based API? https://promisesaplus.com/[Promises] are a way to kick of asynchronous operations and then register a function to respond when the task is done. Promises are designed to be chained together to avoid "callback hell". Look at the following flow:
+=====
+First time using a promise-based API? https://promisesaplus.com/[Promises] are a way to kick off asynchronous operations and then register a function to respond when the task is done. Promises are designed to be chained together to avoid "`callback hell`". Look at the following flow:
+====
[source,javascript]
----
when.promise(async_func_call())
@@ -268,124 +285,138 @@ when.promise(async_func_call())
/* process the previous then() and wrap things up */
});
----
+====
For more details, check out http://know.cujojs.com/tutorials/promises/consuming-promises[this tutorial on promises].
-The secret thing to remember with promises is that `then()` functions _need_ to return something, whether it's a value or another promise. `done()` functions do NOT return anything, and you don't chain anything after it. In case you haven't noticed yet, `client` (which is an instance of `rest` from rest.js) as well as the `follow` function return promises.
-====
+The secret thing to remember with promises is that `then()` functions _need_ to return something, whether it is a value or another promise. `done()` functions do NOT return anything, and you do not chain anything after one. In case you have not yet noticed, `client` (which is an instance of `rest` from rest.js) and the `follow` function return promises.
+=====
-== Paging through data
+== Paging Through Data
-You set up paging on the backend and have already starting taking advantage of it when creating new employees.
+You have set up paging on the backend and have already starting taking advantage of it when creating new employees.
-In <>, you used the page controls to jump to the *last* page. It would be really handy to dynamically apply it to the UI and let the user navigate as desired. Adjusting the controls dynamically based on available navigation links would be great.
+In <>, you used the page controls to jump to the `last` page. It would be really handy to dynamically apply it to the UI and let the user navigate as desired. Adjusting the controls dynamically, based on available navigation links, would be great.
-First, let's check out the `onNavigate()` function you used.
+First, let's check out the `onNavigate()` function you used:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=navigate]
+include::src/main/js/app.js[tag=navigate]
----
+====
This is defined at the top, inside `App.onNavigate`. Again, this is to allow managing the state of the UI in the top component. After passing `onNavigate()` down to the `` React component, the following handlers are coded up to handle clicking on some buttons:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=handle-nav]
+include::src/main/js/app.js[tag=handle-nav]
----
+====
-Each of these functions intercepts the default event and stops it from bubbling up. Then it invokes the `onNavigate()` function with the proper hypermedia link.
+Each of these functions intercepts the default event and stops it from bubbling up. Then it invokes the `onNavigate()` function with the proper hypermedia link.
-Now conditionally display the controls based on which links appear in the hypermedia links in `EmployeeList.render`:
+Now you can conditionally display the controls based on which links appear in the hypermedia links in `EmployeeList.render`:
+====
[source,javascript,indent=0]
----
-include::src/main/resources/static/app.js[tag=employee-list-render]
+include::src/main/js/app.js[tag=employee-list-render]
----
+====
-As in the previous section, it still transforms `this.props.employees` into an array of `` components. Then it builds up an array of `navLinks`, an array of HTML buttons.
+As in the previous section, it still transforms `this.props.employees` into an array of `` components. Then it builds up an array of `navLinks` as an array of HTML buttons.
-NOTE: Because React is based on XML, you can't put "<" inside the `