diff --git a/restful-notes-spring-data-rest/.gitignore b/restful-notes-spring-data-rest/.gitignore
new file mode 100644
index 0000000..d11829c
--- /dev/null
+++ b/restful-notes-spring-data-rest/.gitignore
@@ -0,0 +1,4 @@
+.classpath
+.project
+.settings/
+target/
diff --git a/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar b/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..bf82ff0
Binary files /dev/null and b/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties b/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..3d64498
--- /dev/null
+++ b/restful-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
diff --git a/restful-notes-spring-data-rest/README.md b/restful-notes-spring-data-rest/README.md
new file mode 100644
index 0000000..8b705df
--- /dev/null
+++ b/restful-notes-spring-data-rest/README.md
@@ -0,0 +1,17 @@
+# Restful Notes Spring Data REST Sample
+A RESTful web service for creating and storing notes.
+It uses hypermedia to describe the relationships between resources and to allow navigation between them.
+Demonstrates using Spring REST Docs with MockMvc and Spring Data REST.
+
+
+
+## Building and Running the Sample
+You will need Java 17 to build and run the sample.
+It is built using Maven:
+
+```
+./mvnw verify
+```
+
+As part of the build, files named `build/docs/asciidoc/api-guide.html` and `build/docs/asciidoc/getting-started-guide.html` are created.
+They are produced using Asciidoctor and include snippets generated by the sample's tests using Spring REST Docs.
diff --git a/restful-notes-spring-data-rest/mvnw b/restful-notes-spring-data-rest/mvnw
new file mode 100755
index 0000000..63fcc8f
--- /dev/null
+++ b/restful-notes-spring-data-rest/mvnw
@@ -0,0 +1,234 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+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
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, 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
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+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)
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ wdir=$(cd "$wdir/.."; pwd)
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# 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
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS
+
diff --git a/restful-notes-spring-data-rest/mvnw.cmd b/restful-notes-spring-data-rest/mvnw.cmd
new file mode 100644
index 0000000..66e928b
--- /dev/null
+++ b/restful-notes-spring-data-rest/mvnw.cmd
@@ -0,0 +1,145 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@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 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
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@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_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
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@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'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %*
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+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.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/restful-notes-spring-data-rest/pom.xml b/restful-notes-spring-data-rest/pom.xml
new file mode 100644
index 0000000..c9c84a6
--- /dev/null
+++ b/restful-notes-spring-data-rest/pom.xml
@@ -0,0 +1,135 @@
+
+
+ 4.0.0
+
+ com.example
+ restful-notes-spring-data-rest
+ 0.0.1-SNAPSHOT
+ jar
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.0.0-M4
+
+
+
+
+ UTF-8
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/*Documentation.java
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 2.2.2
+
+
+ generate-docs
+ prepare-package
+
+ process-asciidoc
+
+
+ html
+ book
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-asciidoctor
+ ${spring-restdocs.version}
+
+
+
+
+ maven-resources-plugin
+
+
+ copy-resources
+ prepare-package
+
+ copy-resources
+
+
+ ${project.build.outputDirectory}/static/docs
+
+
+ ${project.build.directory}/generated-docs
+
+
+
+
+
+
+
+
+
+
+
+ spring-milestones
+ Spring milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+ spring-milestones
+ Spring milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
diff --git a/restful-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/restful-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc
new file mode 100644
index 0000000..8cb8178
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc
@@ -0,0 +1,228 @@
+= RESTful Notes API Guide
+Andy Wilkinson;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:toc: left
+:toclevels: 4
+:sectlinks:
+:operation-curl-request-title: Example request
+:operation-http-response-title: Example response
+
+[[overview]]
+= Overview
+
+[[overview_http_verbs]]
+== HTTP verbs
+
+RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
+use of HTTP verbs.
+
+|===
+| Verb | Usage
+
+| `GET`
+| Used to retrieve a resource
+
+| `POST`
+| Used to create a new resource
+
+| `PATCH`
+| Used to update an existing resource, including partial updates
+
+| `DELETE`
+| Used to delete an existing resource
+|===
+
+[[overview_http_status_codes]]
+== HTTP status codes
+
+RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
+use of HTTP status codes.
+
+|===
+| Status code | Usage
+
+| `200 OK`
+| The request completed successfully
+
+| `201 Created`
+| A new resource has been created successfully. The resource's URI is available from the response's
+`Location` header
+
+| `204 No Content`
+| An update to an existing resource has been applied successfully
+
+| `400 Bad Request`
+| The request was malformed. The response body will include an error providing further information
+
+| `404 Not Found`
+| The requested resource did not exist
+|===
+
+[[overview_errors]]
+== Errors
+
+Whenever an error response (status code >= 400) is returned, the body will contain a JSON object
+that describes the problem. The error object has the following structure:
+
+include::{snippets}/error-example/response-fields.adoc[]
+
+For example, a request that attempts to apply a non-existent tag to a note will produce a
+`400 Bad Request` response:
+
+include::{snippets}/error-example/http-response.adoc[]
+
+[[overview_hypermedia]]
+== Hypermedia
+
+RESTful Notes uses hypermedia and resources include links to other resources in their
+responses. Responses are in https://github.com/mikekelly/hal_specification[Hypertext
+Application Language (HAL)] format. Links can be found beneath the `_links` key. Users of
+the API should not create URIs themselves, instead they should use the above-described
+links to navigate from resource to resource.
+
+[[resources]]
+= Resources
+
+
+
+[[resources_index]]
+== Index
+
+The index provides the entry point into the service.
+
+
+
+[[resources_index_access]]
+=== Accessing the index
+
+A `GET` request is used to access the index
+
+operation::index-example[snippets='response-fields,http-response,links']
+
+
+
+[[resources_notes]]
+== Notes
+
+The Notes resources is used to create and list notes
+
+
+
+[[resources_notes_list]]
+=== Listing notes
+
+A `GET` request will list all of the service's notes.
+
+operation::notes-list-example[snippets='response-fields,curl-request,http-response,links']
+
+
+
+[[resources_notes_create]]
+=== Creating a note
+
+A `POST` request is used to create a note.
+
+operation::notes-create-example[snippets='request-fields,curl-request,http-response']
+
+
+
+[[resources_tags]]
+== Tags
+
+The Tags resource is used to create and list tags.
+
+
+
+[[resources_tags_list]]
+=== Listing tags
+
+A `GET` request will list all of the service's tags.
+
+operation::tags-list-example[snippets='response-fields,curl-request,http-response,links']
+
+
+
+[[resources_tags_create]]
+=== Creating a tag
+
+A `POST` request is used to create a note
+
+operation::tags-create-example[snippets='request-fields,curl-request,http-response']
+
+
+
+[[resources_note]]
+== Note
+
+The Note resource is used to retrieve, update, and delete individual notes
+
+
+
+[[resources_note_links]]
+=== Links
+
+include::{snippets}/note-get-example/links.adoc[]
+
+
+
+[[resources_note_retrieve]]
+=== Retrieve a note
+
+A `GET` request will retrieve the details of a note
+
+operation::note-get-example[snippets='response-fields,curl-request,http-response']
+
+
+
+[[resources_note_update]]
+=== Update a note
+
+A `PATCH` request is used to update a note
+
+==== Request structure
+
+include::{snippets}/note-update-example/request-fields.adoc[]
+
+To leave an attribute of a note unchanged, any of the above may be omitted from the request.
+
+==== Example request
+
+include::{snippets}/note-update-example/curl-request.adoc[]
+
+==== Example response
+
+include::{snippets}/note-update-example/http-response.adoc[]
+
+
+
+[[resources_tag]]
+== Tag
+
+The Tag resource is used to retrieve, update, and delete individual tags
+
+
+
+[[resources_tag_links]]
+=== Links
+
+include::{snippets}/tag-get-example/links.adoc[]
+
+
+
+[[resources_tag_retrieve]]
+=== Retrieve a tag
+
+A `GET` request will retrieve the details of a tag
+
+operation::tag-get-example[snippets='response-fields,curl-request,http-response']
+
+
+
+[[resources_tag_update]]
+=== Update a tag
+
+A `PATCH` request is used to update a tag
+
+operation::tag-update-example[snippets='request-fields,curl-request,http-response']
diff --git a/restful-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/restful-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc
new file mode 100644
index 0000000..c1af376
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc
@@ -0,0 +1,175 @@
+= RESTful Notes Getting Started Guide
+Andy Wilkinson;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:toc: left
+:toclevels: 4
+:sectlinks:
+
+[[introduction]]
+= Introduction
+
+RESTful Notes is a RESTful web service for creating and storing notes. It uses hypermedia
+to describe the relationships between resources and to allow navigation between them.
+
+
+
+[[getting_started_running_the_service]]
+== Running the service
+RESTful Notes is written using https://projects.spring.io/spring-boot[Spring Boot] which
+makes it easy to get it up and running so that you can start exploring the REST API.
+
+The first step is to clone the Git repository:
+
+[source,bash]
+----
+$ git clone https://github.com/spring-projects/spring-restdocs-samples
+----
+
+Once the clone is complete, you're ready to get the service up and running:
+
+[source,bash]
+----
+$ cd restful-notes-spring-data-rest
+$ ./mvnw clean package
+$ java -jar target/*.jar
+----
+
+You can check that the service is up and running by executing a simple request using
+cURL:
+
+include::{snippets}/index/1/curl-request.adoc[]
+
+This request should yield the following response in the
+https://github.com/mikekelly/hal_specification[Hypertext Application Language (HAL)]
+format:
+
+include::{snippets}/index/1/http-response.adoc[]
+
+Note the `_links` in the JSON response. They are key to navigating the API.
+
+
+
+[[getting_started_creating_a_note]]
+== Creating a note
+Now that you've started the service and verified that it works, the next step is to use
+it to create a new note. As you saw above, the URI for working with notes is included as
+a link when you perform a `GET` request against the root of the service:
+
+include::{snippets}/index/1/http-response.adoc[]
+
+To create a note, you need to execute a `POST` request to this URI including a JSON
+payload containing the title and body of the note:
+
+include::{snippets}/creating-a-note/1/curl-request.adoc[]
+
+The response from this request should have a status code of `201 Created` and contain a
+`Location` header whose value is the URI of the newly created note:
+
+include::{snippets}/creating-a-note/1/http-response.adoc[]
+
+To work with the newly created note you use the URI in the `Location` header. For example,
+you can access the note's details by performing a `GET` request:
+
+include::{snippets}/creating-a-note/2/curl-request.adoc[]
+
+This request will produce a response with the note's details in its body:
+
+include::{snippets}/creating-a-note/2/http-response.adoc[]
+
+Note the `tags` link which we'll make use of later.
+
+
+
+[[getting_started_creating_a_tag]]
+== Creating a tag
+To make a note easier to find, it can be associated with any number of tags. To be able
+to tag a note, you must first create the tag.
+
+Referring back to the response for the service's index, the URI for working with tags is
+include as a link:
+
+include::{snippets}/index/1/http-response.adoc[]
+
+To create a tag you need to execute a `POST` request to this URI, including a JSON
+payload containing the name of the tag:
+
+include::{snippets}/creating-a-note/3/curl-request.adoc[]
+
+The response from this request should have a status code of `201 Created` and contain a
+`Location` header whose value is the URI of the newly created tag:
+
+include::{snippets}/creating-a-note/3/http-response.adoc[]
+
+To work with the newly created tag you use the URI in the `Location` header. For example
+you can access the tag's details by performing a `GET` request:
+
+include::{snippets}/creating-a-note/4/curl-request.adoc[]
+
+This request will produce a response with the tag's details in its body:
+
+include::{snippets}/creating-a-note/4/http-response.adoc[]
+
+
+
+[[getting_started_tagging_a_note]]
+== Tagging a note
+A tag isn't particularly useful until it's been associated with one or more notes. There
+are two ways to tag a note: when the note is first created or by updating an existing
+note. We'll look at both of these in turn.
+
+
+
+[[getting_started_tagging_a_note_creating]]
+=== Creating a tagged note
+The process is largely the same as we saw before, but this time, in addition to providing
+a title and body for the note, we'll also provide the tag that we want to be associated
+with it.
+
+Once again we execute a `POST` request. However, this time, in an array named tags, we
+include the URI of the tag we just created:
+
+include::{snippets}/creating-a-note/5/curl-request.adoc[]
+
+Once again, the response's `Location` header tells us the URI of the newly created note:
+
+include::{snippets}/creating-a-note/5/http-response.adoc[]
+
+As before, a `GET` request executed against this URI will retrieve the note's details:
+
+include::{snippets}/creating-a-note/6/curl-request.adoc[]
+include::{snippets}/creating-a-note/6/http-response.adoc[]
+
+To verify that the tag has been associated with the note, we can perform a `GET` request
+against the URI from the `tags` link:
+
+include::{snippets}/creating-a-note/7/curl-request.adoc[]
+
+The response embeds information about the tag that we've just associated with the note:
+
+include::{snippets}/creating-a-note/7/http-response.adoc[]
+
+
+
+[[getting_started_tagging_a_note_existing]]
+=== Tagging an existing note
+An existing note can be tagged by executing a `PATCH` request against the note's URI with
+a body that contains the array of tags to be associated with the note. We'll used the
+URI of the untagged note that we created earlier:
+
+include::{snippets}/creating-a-note/8/curl-request.adoc[]
+
+This request should produce a `204 No Content` response:
+
+include::{snippets}/creating-a-note/8/http-response.adoc[]
+
+When we first created this note, we noted the tags link included in its details:
+
+include::{snippets}/creating-a-note/2/http-response.adoc[]
+
+We can use that link now and execute a `GET` request to see that the note now has a
+single tag:
+
+include::{snippets}/creating-a-note/9/curl-request.adoc[]
+include::{snippets}/creating-a-note/9/http-response.adoc[]
diff --git a/restful-notes-spring-data-rest/src/main/java/com/example/notes/Note.java b/restful-notes-spring-data-rest/src/main/java/com/example/notes/Note.java
new file mode 100644
index 0000000..6f83259
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/java/com/example/notes/Note.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import java.util.List;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToMany;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+@Entity
+public class Note {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private long id;
+
+ private String title;
+
+ private String body;
+
+ @ManyToMany
+ private List tags;
+
+ @JsonIgnore
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void setTags(List tags) {
+ this.tags = tags;
+ }
+}
diff --git a/restful-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java b/restful-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java
new file mode 100644
index 0000000..c99dacc
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface NoteRepository extends CrudRepository {
+
+}
diff --git a/restful-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/restful-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java
new file mode 100644
index 0000000..f9670e5
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class RestNotesSpringDataRest {
+
+ public static void main(String[] args) {
+ SpringApplication.run(RestNotesSpringDataRest.class, args);
+ }
+
+}
diff --git a/restful-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java b/restful-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java
new file mode 100644
index 0000000..317851f
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import java.util.List;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToMany;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+@Entity
+public class Tag {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private long id;
+
+ private String name;
+
+ @ManyToMany(mappedBy = "tags")
+ private List notes;
+
+ @JsonIgnore
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getNotes() {
+ return notes;
+ }
+
+ public void setNotes(List notes) {
+ this.notes = notes;
+ }
+}
diff --git a/restful-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java b/restful-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java
new file mode 100644
index 0000000..9fa5430
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface TagRepository extends CrudRepository {
+
+}
diff --git a/restful-notes-spring-data-rest/src/main/resources/application.properties b/restful-notes-spring-data-rest/src/main/resources/application.properties
new file mode 100644
index 0000000..8e06a82
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.jackson.serialization.indent_output: true
\ No newline at end of file
diff --git a/restful-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/restful-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java
new file mode 100644
index 0000000..baa879d
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.servlet.RequestDispatcher;
+
+@SpringBootTest
+@ExtendWith(RestDocumentationExtension.class)
+class ApiDocumentation {
+
+ @Autowired
+ private NoteRepository noteRepository;
+
+ @Autowired
+ private TagRepository tagRepository;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ public void setUp(RestDocumentationContextProvider restDocumentation) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+ .apply(documentationConfiguration(restDocumentation)).build();
+ }
+
+ @Test
+ void errorExample() throws Exception {
+ this.mockMvc
+ .perform(get("/error")
+ .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400)
+ .requestAttr(RequestDispatcher.ERROR_REQUEST_URI,
+ "/notes")
+ .requestAttr(RequestDispatcher.ERROR_MESSAGE,
+ "The tag 'http://localhost:8080/tags/123' does not exist"))
+ .andDo(print()).andExpect(status().isBadRequest())
+ .andExpect(jsonPath("error", is("Bad Request")))
+ .andExpect(jsonPath("timestamp", is(notNullValue())))
+ .andExpect(jsonPath("status", is(400)))
+ .andExpect(jsonPath("path", is(notNullValue())))
+ .andDo(document("error-example",
+ responseFields(
+ fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
+ fieldWithPath("path").description("The path to which the request was made"),
+ fieldWithPath("status").description("The HTTP status code, e.g. `400`"),
+ fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))));
+ }
+
+ @Test
+ void indexExample() throws Exception {
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andDo(document("index-example",
+ links(
+ linkWithRel("notes").description("The <>"),
+ linkWithRel("tags").description("The <>"),
+ linkWithRel("profile").description("The ALPS profile for the service")),
+ responseFields(
+ subsectionWithPath("_links").description("<> to other resources"))));
+
+ }
+
+ @Test
+ void notesListExample() throws Exception {
+ this.noteRepository.deleteAll();
+
+ createNote("REST maturity model",
+ "https://martinfowler.com/articles/richardsonMaturityModel.html");
+ createNote("Hypertext Application Language (HAL)",
+ "https://github.com/mikekelly/hal_specification");
+ createNote("Application-Level Profile Semantics (ALPS)", "https://github.com/alps-io/spec");
+
+ this.mockMvc.perform(get("/notes"))
+ .andExpect(status().isOk())
+ .andDo(document("notes-list-example",
+ links(
+ linkWithRel("self").description("Canonical link for this resource"),
+ linkWithRel("profile").description("The ALPS profile for this resource")),
+ responseFields(
+ subsectionWithPath("_embedded.notes").description("An array of <>"),
+ subsectionWithPath("_links").description("<> to other resources"))));
+ }
+
+ @Test
+ void notesCreateExample() throws Exception {
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ Map note = new HashMap();
+ note.put("title", "REST maturity model");
+ note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html");
+ note.put("tags", Arrays.asList(tagLocation));
+
+ this.mockMvc.perform(
+ post("/notes").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(note))).andExpect(
+ status().isCreated())
+ .andDo(document("notes-create-example",
+ requestFields(
+ fieldWithPath("title").description("The title of the note"),
+ fieldWithPath("body").description("The body of the note"),
+ fieldWithPath("tags").description("An array of tag resource URIs"))));
+ }
+
+ @Test
+ void noteGetExample() throws Exception {
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ Map note = new HashMap();
+ note.put("title", "REST maturity model");
+ note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html");
+ note.put("tags", Arrays.asList(tagLocation));
+
+ String noteLocation = this.mockMvc
+ .perform(
+ post("/notes").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(note)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ this.mockMvc.perform(get(noteLocation))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("title", is(note.get("title"))))
+ .andExpect(jsonPath("body", is(note.get("body"))))
+ .andExpect(jsonPath("_links.self.href", is(noteLocation)))
+ .andExpect(jsonPath("_links.tags", is(notNullValue())))
+ .andDo(print())
+ .andDo(document("note-get-example",
+ links(
+ linkWithRel("self").description("Canonical link for this <>"),
+ linkWithRel("note").description("This <>"),
+ linkWithRel("tags").description("This note's tags")),
+ responseFields(
+ fieldWithPath("title").description("The title of the note"),
+ fieldWithPath("body").description("The body of the note"),
+ subsectionWithPath("_links").description("<> to other resources"))));
+ }
+
+ @Test
+ void tagsListExample() throws Exception {
+ this.noteRepository.deleteAll();
+ this.tagRepository.deleteAll();
+
+ createTag("REST");
+ createTag("Hypermedia");
+ createTag("HTTP");
+
+ this.mockMvc.perform(get("/tags"))
+ .andExpect(status().isOk())
+ .andDo(document("tags-list-example",
+ links(
+ linkWithRel("self").description("Canonical link for this resource"),
+ linkWithRel("profile").description("The ALPS profile for this resource")),
+ responseFields(
+ subsectionWithPath("_embedded.tags").description("An array of <>"),
+ subsectionWithPath("_links").description("<> to other resources"))));
+ }
+
+ @Test
+ void tagsCreateExample() throws Exception {
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ this.mockMvc.perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated())
+ .andDo(document("tags-create-example",
+ requestFields(
+ fieldWithPath("name").description("The name of the tag"))));
+ }
+
+ @Test
+ void noteUpdateExample() throws Exception {
+ Map note = new HashMap();
+ note.put("title", "REST maturity model");
+ note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html");
+
+ String noteLocation = this.mockMvc
+ .perform(
+ post("/notes").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(note)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
+ .andExpect(jsonPath("title", is(note.get("title"))))
+ .andExpect(jsonPath("body", is(note.get("body"))))
+ .andExpect(jsonPath("_links.self.href", is(noteLocation)))
+ .andExpect(jsonPath("_links.tags", is(notNullValue())));
+
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ Map noteUpdate = new HashMap();
+ noteUpdate.put("tags", Arrays.asList(tagLocation));
+
+ this.mockMvc.perform(
+ patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(noteUpdate)))
+ .andExpect(status().isNoContent())
+ .andDo(document("note-update-example",
+ requestFields(
+ fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(),
+ fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(),
+ fieldWithPath("tags").description("An array of tag resource URIs").optional())));
+ }
+
+ @Test
+ void tagGetExample() throws Exception {
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ this.mockMvc.perform(get(tagLocation))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("name", is(tag.get("name"))))
+ .andDo(document("tag-get-example",
+ links(
+ linkWithRel("self").description("Canonical link for this <>"),
+ linkWithRel("tag").description("This <>"),
+ linkWithRel("notes").description("The notes that have this tag")),
+ responseFields(
+ fieldWithPath("name").description("The name of the tag"),
+ subsectionWithPath("_links").description("<> to other resources"))));
+ }
+
+ @Test
+ void tagUpdateExample() throws Exception {
+ Map tag = new HashMap();
+ tag.put("name", "REST");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated()).andReturn().getResponse()
+ .getHeader("Location");
+
+ Map tagUpdate = new HashMap();
+ tagUpdate.put("name", "RESTful");
+
+ this.mockMvc.perform(
+ patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
+ this.objectMapper.writeValueAsString(tagUpdate)))
+ .andExpect(status().isNoContent())
+ .andDo(document("tag-update-example",
+ requestFields(
+ fieldWithPath("name").description("The name of the tag"))));
+ }
+
+ private void createNote(String title, String body) {
+ Note note = new Note();
+ note.setTitle(title);
+ note.setBody(body);
+
+ this.noteRepository.save(note);
+ }
+
+ private void createTag(String name) {
+ Tag tag = new Tag();
+ tag.setName(name);
+ this.tagRepository.save(tag);
+ }
+}
diff --git a/restful-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/restful-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java
new file mode 100644
index 0000000..32a3d23
--- /dev/null
+++ b/restful-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2014-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.notes;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.JsonPath;
+
+@SpringBootTest
+@ExtendWith(RestDocumentationExtension.class)
+class GettingStartedDocumentation {
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setUp(RestDocumentationContextProvider restDocumentation) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+ .apply(documentationConfiguration(restDocumentation))
+ .alwaysDo(document("{method-name}/{step}/"))
+ .build();
+ }
+
+ @Test
+ void index() throws Exception {
+ this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("_links.notes", is(notNullValue())))
+ .andExpect(jsonPath("_links.tags", is(notNullValue())));
+ }
+
+ @Test
+ void creatingANote() throws JsonProcessingException, Exception {
+ String noteLocation = createNote();
+ MvcResult note = getNote(noteLocation);
+
+ String tagLocation = createTag();
+ getTag(tagLocation);
+
+ String taggedNoteLocation = createTaggedNote(tagLocation);
+ MvcResult taggedNote = getNote(taggedNoteLocation);
+ getTags(getLink(taggedNote, "tags"));
+
+ tagExistingNote(noteLocation, tagLocation);
+ getTags(getLink(note, "tags"));
+ }
+
+ private String createNote() throws Exception {
+ Map note = new HashMap();
+ note.put("title", "Note creation with cURL");
+ note.put("body", "An example of how to create a note using cURL");
+
+ String noteLocation = this.mockMvc
+ .perform(
+ post("/notes").contentType(MediaTypes.HAL_JSON).content(
+ objectMapper.writeValueAsString(note)))
+ .andExpect(status().isCreated())
+ .andExpect(header().string("Location", notNullValue()))
+ .andReturn().getResponse().getHeader("Location");
+ return noteLocation;
+ }
+
+ private MvcResult getNote(String noteLocation) throws Exception {
+ return this.mockMvc.perform(get(noteLocation))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("title", is(notNullValue())))
+ .andExpect(jsonPath("body", is(notNullValue())))
+ .andExpect(jsonPath("_links.tags", is(notNullValue())))
+ .andReturn();
+ }
+
+ private String createTag() throws Exception, JsonProcessingException {
+ Map tag = new HashMap();
+ tag.put("name", "getting-started");
+
+ String tagLocation = this.mockMvc
+ .perform(
+ post("/tags").contentType(MediaTypes.HAL_JSON).content(
+ objectMapper.writeValueAsString(tag)))
+ .andExpect(status().isCreated())
+ .andExpect(header().string("Location", notNullValue()))
+ .andReturn().getResponse().getHeader("Location");
+ return tagLocation;
+ }
+
+ private void getTag(String tagLocation) throws Exception {
+ this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
+ .andExpect(jsonPath("name", is(notNullValue())))
+ .andExpect(jsonPath("_links.notes", is(notNullValue())));
+ }
+
+ private String createTaggedNote(String tag) throws Exception {
+ Map note = new HashMap();
+ note.put("title", "Tagged note creation with cURL");
+ note.put("body", "An example of how to create a tagged note using cURL");
+ note.put("tags", Arrays.asList(tag));
+
+ String noteLocation = this.mockMvc
+ .perform(
+ post("/notes").contentType(MediaTypes.HAL_JSON).content(
+ objectMapper.writeValueAsString(note)))
+ .andExpect(status().isCreated())
+ .andExpect(header().string("Location", notNullValue()))
+ .andReturn().getResponse().getHeader("Location");
+ return noteLocation;
+ }
+
+ private void getTags(String noteTagsLocation) throws Exception {
+ this.mockMvc.perform(get(noteTagsLocation))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("_embedded.tags", hasSize(1)));
+ }
+
+ private void tagExistingNote(String noteLocation, String tagLocation) throws Exception {
+ Map update = new HashMap();
+ update.put("tags", Arrays.asList(tagLocation));
+
+ this.mockMvc.perform(
+ patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
+ objectMapper.writeValueAsString(update)))
+ .andExpect(status().isNoContent());
+ }
+
+ private String getLink(MvcResult result, String rel)
+ throws UnsupportedEncodingException {
+ return JsonPath.parse(result.getResponse().getContentAsString()).read(
+ "_links." + rel + ".href");
+ }
+
+}