Add RESTful Notes Spring Data REST sample

This commit is contained in:
Andy Wilkinson
2022-07-26 12:43:30 +01:00
parent 58cf479ca1
commit 91b9c5c118
17 changed files with 1698 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
.classpath
.project
.settings/
target/

Binary file not shown.

View File

@@ -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

View File

@@ -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.

234
restful-notes-spring-data-rest/mvnw vendored Executable file
View File

@@ -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

145
restful-notes-spring-data-rest/mvnw.cmd vendored Normal file
View File

@@ -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%

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>restful-notes-spring-data-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-M4</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Documentation.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.2</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/generated-docs</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

View File

@@ -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']

View File

@@ -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[]

View File

@@ -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<Tag> 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<Tag> getTags() {
return tags;
}
public void setTags(List<Tag> tags) {
this.tags = tags;
}
}

View File

@@ -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<Note, Long> {
}

View File

@@ -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);
}
}

View File

@@ -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<Note> 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<Note> getNotes() {
return notes;
}
public void setNotes(List<Note> notes) {
this.notes = notes;
}
}

View File

@@ -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<Tag, Long> {
}

View File

@@ -0,0 +1 @@
spring.jackson.serialization.indent_output: true

View File

@@ -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 <<resources_notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources_tags,Tags resource>>"),
linkWithRel("profile").description("The ALPS profile for the service")),
responseFields(
subsectionWithPath("_links").description("<<resources_index_access_links,Links>> 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 <<resources_note, Note resources>>"),
subsectionWithPath("_links").description("<<resources_tags_list_links, Links>> to other resources"))));
}
@Test
void notesCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
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<String, Object> note = new HashMap<String, Object>();
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<String, String> tag = new HashMap<String, String>();
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<String, Object> note = new HashMap<String, Object>();
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 <<resources_note,note>>"),
linkWithRel("note").description("This <<resources_note,note>>"),
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("<<resources_note_links,Links>> 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 <<resources_tag,Tag resources>>"),
subsectionWithPath("_links").description("<<resources_tags_list_links, Links>> to other resources"))));
}
@Test
void tagsCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
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<String, Object> note = new HashMap<String, Object>();
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<String, String> tag = new HashMap<String, String>();
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<String, Object> noteUpdate = new HashMap<String, Object>();
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<String, String> tag = new HashMap<String, String>();
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 <<resources_tag,tag>>"),
linkWithRel("tag").description("This <<resources_tag,tag>>"),
linkWithRel("notes").description("The notes that have this tag")),
responseFields(
fieldWithPath("name").description("The name of the tag"),
subsectionWithPath("_links").description("<<resources_tag_links,Links>> to other resources"))));
}
@Test
void tagUpdateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
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<String, Object> tagUpdate = new HashMap<String, Object>();
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);
}
}

View File

@@ -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<String, String> note = new HashMap<String, String>();
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<String, String> tag = new HashMap<String, String>();
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<String, Object> note = new HashMap<String, Object>();
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<String, Object> update = new HashMap<String, Object>();
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");
}
}