├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── .travis.yml
├── LICENSE
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── github
│ │ └── sbouclier
│ │ └── javarestbooks
│ │ ├── JavaRestBooksApplication.java
│ │ ├── controller
│ │ ├── BookController.java
│ │ └── BookControllerAdvice.java
│ │ ├── domain
│ │ ├── Author.java
│ │ └── Book.java
│ │ ├── exception
│ │ ├── BookIsbnAlreadyExistsException.java
│ │ └── BookNotFoundException.java
│ │ └── repository
│ │ └── BookRepository.java
└── resources
│ ├── application.properties
│ └── import.sql
└── test
└── java
└── com
└── github
└── sbouclier
└── javarestbooks
├── JavaRestBooksApplicationTests.java
├── controller
└── BookControllerTest.java
└── domain
├── AuthorTest.java
└── BookTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.ear
17 | *.zip
18 | *.tar.gz
19 | *.rar
20 |
21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
22 | hs_err_pid*
23 |
24 | # IntelliJ IDEA #
25 | .idea
26 | *.iws
27 | *.iml
28 | *.ipr
29 |
30 | # Mac #
31 | .DS_Store
32 |
33 | target/
34 | !.mvn/wrapper/maven-wrapper.jar
35 |
36 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbouclier/java-rest-books/ea2c6f8159dbf61692680f1b67b9cf9e629ed662/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | script: mvn test
3 | after_success:
4 | - mvn clean test jacoco:report coveralls:report
5 | jdk:
6 | - oraclejdk8
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Stéphane Bouclier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/sbouclier/java-rest-books)
2 | [](https://coveralls.io/github/sbouclier/java-rest-books?branch=master)
3 |
4 | # java-rest-books
5 |
6 | Sample code of DZone article: https://dzone.com/articles/leverage-http-status-codes-to-build-a-rest-service
7 |
8 | ## Requirements
9 | - JDK 8
10 | - Maven 3.x
11 |
12 | ## Setup
13 |
14 | Clone the repository:
15 | ```bash
16 | git clone https://github.com/sbouclier/java-rest-books.git
17 | ```
18 |
19 | Go inside the folder:
20 | ```bash
21 | cd java-rest-books/
22 | ```
23 |
24 | Run the application:
25 | ```bash
26 | mvn clean install spring-boot:run
27 | ```
28 |
29 | Open your browser an go to http://localhost:8080/api/books to see some books.
30 |
31 | ## API methods
32 |
33 | ### Create book
34 |
35 | ```bash
36 | curl -X POST --header 'Content-Type: application/json' --header 'Accept: */*' -d '{ \
37 | "authors": [ \
38 | { \
39 | "firstName": "John", \
40 | "lastName": "Doe" \
41 | } \
42 | ], \
43 | "description": "desc", \
44 | "isbn": "123-1234567890", \
45 | "publisher": "My publisher", \
46 | "title": "My book" \
47 | }' 'http://localhost:8080/api/books'
48 | ```
49 |
50 | ### Get a book
51 |
52 | ```bash
53 | curl -X GET --header 'Accept: application/json' 'http://localhost:8080/api/books/978-0321356680'
54 | ```
55 |
56 | ### Update book
57 |
58 | ```bash
59 | curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \
60 | "authors": [ \
61 | { \
62 | "firstName": "John", \
63 | "lastName": "Doe" \
64 | } \
65 | ], \
66 | "description": "new desc", \
67 | "isbn": "978-1617292545", \
68 | "publisher": "new publisher", \
69 | "title": "new title" \
70 | }' 'http://localhost:8080/api/books/978-1617292545'
71 | ```
72 |
73 | ### Update a book's description
74 |
75 | ```bash
76 | curl -X PATCH --header 'Content-Type: application/json' --header 'Accept: application/json' -d 'new description' 'http://localhost:8080/api/books/978-1491900864'
77 | ```
78 |
79 | ### Delete a book
80 |
81 | ```bash
82 | curl -X DELETE --header 'Accept: */*' 'http://localhost:8080/api/books/978-1617292545'
83 | ```
84 |
85 | ### Get all books
86 |
87 | ```bash
88 | curl -X GET --header 'Accept: application/json' 'http://localhost:8080/api/books?sort=id&order=asc'
89 | ```
90 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven2 Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Migwn, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | # TODO classpath?
118 | fi
119 |
120 | if [ -z "$JAVA_HOME" ]; then
121 | javaExecutable="`which javac`"
122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
123 | # readlink(1) is not available as standard on Solaris 10.
124 | readLink=`which readlink`
125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
126 | if $darwin ; then
127 | javaHome="`dirname \"$javaExecutable\"`"
128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
129 | else
130 | javaExecutable="`readlink -f \"$javaExecutable\"`"
131 | fi
132 | javaHome="`dirname \"$javaExecutable\"`"
133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
134 | JAVA_HOME="$javaHome"
135 | export JAVA_HOME
136 | fi
137 | fi
138 | fi
139 |
140 | if [ -z "$JAVACMD" ] ; then
141 | if [ -n "$JAVA_HOME" ] ; then
142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
143 | # IBM's JDK on AIX uses strange locations for the executables
144 | JAVACMD="$JAVA_HOME/jre/sh/java"
145 | else
146 | JAVACMD="$JAVA_HOME/bin/java"
147 | fi
148 | else
149 | JAVACMD="`which java`"
150 | fi
151 | fi
152 |
153 | if [ ! -x "$JAVACMD" ] ; then
154 | echo "Error: JAVA_HOME is not defined correctly." >&2
155 | echo " We cannot execute $JAVACMD" >&2
156 | exit 1
157 | fi
158 |
159 | if [ -z "$JAVA_HOME" ] ; then
160 | echo "Warning: JAVA_HOME environment variable is not set."
161 | fi
162 |
163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
164 |
165 | # traverses directory structure from process work directory to filesystem root
166 | # first directory with .mvn subdirectory is considered project base directory
167 | find_maven_basedir() {
168 |
169 | if [ -z "$1" ]
170 | then
171 | echo "Path not specified to find_maven_basedir"
172 | return 1
173 | fi
174 |
175 | basedir="$1"
176 | wdir="$1"
177 | while [ "$wdir" != '/' ] ; do
178 | if [ -d "$wdir"/.mvn ] ; then
179 | basedir=$wdir
180 | break
181 | fi
182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
183 | if [ -d "${wdir}" ]; then
184 | wdir=`cd "$wdir/.."; pwd`
185 | fi
186 | # end of workaround
187 | done
188 | echo "${basedir}"
189 | }
190 |
191 | # concatenates all lines of a file
192 | concat_lines() {
193 | if [ -f "$1" ]; then
194 | echo "$(tr -s '\n' ' ' < "$1")"
195 | fi
196 | }
197 |
198 | BASE_DIR=`find_maven_basedir "$(pwd)"`
199 | if [ -z "$BASE_DIR" ]; then
200 | exit 1;
201 | fi
202 |
203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
204 | echo $MAVEN_PROJECTBASEDIR
205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
206 |
207 | # For Cygwin, switch paths to Windows format before running java
208 | if $cygwin; then
209 | [ -n "$M2_HOME" ] &&
210 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
211 | [ -n "$JAVA_HOME" ] &&
212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
213 | [ -n "$CLASSPATH" ] &&
214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
215 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
217 | fi
218 |
219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
220 |
221 | exec "$JAVACMD" \
222 | $MAVEN_OPTS \
223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
226 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven2 Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
40 |
41 | @REM set %HOME% to equivalent of $HOME
42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
43 |
44 | @REM Execute a user defined script before this one
45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
49 | :skipRcPre
50 |
51 | @setlocal
52 |
53 | set ERROR_CODE=0
54 |
55 | @REM To isolate internal variables from possible post scripts, we use another setlocal
56 | @setlocal
57 |
58 | @REM ==== START VALIDATION ====
59 | if not "%JAVA_HOME%" == "" goto OkJHome
60 |
61 | echo.
62 | echo Error: JAVA_HOME not found in your environment. >&2
63 | echo Please set the JAVA_HOME variable in your environment to match the >&2
64 | echo location of your Java installation. >&2
65 | echo.
66 | goto error
67 |
68 | :OkJHome
69 | if exist "%JAVA_HOME%\bin\java.exe" goto init
70 |
71 | echo.
72 | echo Error: JAVA_HOME is set to an invalid directory. >&2
73 | echo JAVA_HOME = "%JAVA_HOME%" >&2
74 | echo Please set the JAVA_HOME variable in your environment to match the >&2
75 | echo location of your Java installation. >&2
76 | echo.
77 | goto error
78 |
79 | @REM ==== END VALIDATION ====
80 |
81 | :init
82 |
83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
84 | @REM Fallback to current working directory if not found.
85 |
86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
88 |
89 | set EXEC_DIR=%CD%
90 | set WDIR=%EXEC_DIR%
91 | :findBaseDir
92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
93 | cd ..
94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
95 | set WDIR=%CD%
96 | goto findBaseDir
97 |
98 | :baseDirFound
99 | set MAVEN_PROJECTBASEDIR=%WDIR%
100 | cd "%EXEC_DIR%"
101 | goto endDetectBaseDir
102 |
103 | :baseDirNotFound
104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
105 | cd "%EXEC_DIR%"
106 |
107 | :endDetectBaseDir
108 |
109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
110 |
111 | @setlocal EnableExtensions EnableDelayedExpansion
112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
114 |
115 | :endReadAdditionalConfig
116 |
117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
118 |
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
123 | if ERRORLEVEL 1 goto error
124 | goto end
125 |
126 | :error
127 | set ERROR_CODE=1
128 |
129 | :end
130 | @endlocal & set ERROR_CODE=%ERROR_CODE%
131 |
132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
136 | :skipRcPost
137 |
138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
140 |
141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
142 |
143 | exit /B %ERROR_CODE%
144 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.github.sbouclier
7 | java-rest-books
8 | 1.0-SNAPSHOT
9 | jar
10 |
11 | java-rest-books
12 | https://github.com/sbouclier/java-rest-books
13 |
14 |
15 | https://github.com/sbouclier/java-rest-books
16 | scm:git:ssh://git@github.com/sbouclier/java-rest-books.git
17 | scm:git:ssh://git@github.com/sbouclier/java-rest-books.git
18 | HEAD
19 |
20 |
21 | https://travis-ci.org/sbouclier/java-rest-books/
22 | Travis
23 |
24 |
25 |
26 | org.springframework.boot
27 | spring-boot-starter-parent
28 | 2.0.0.M4
29 |
30 |
31 |
32 |
33 | UTF-8
34 | UTF-8
35 | yyyy-MM-dd'T'HH:mm:ss
36 | 1.8
37 |
38 |
39 |
40 |
41 | org.springframework.boot
42 | spring-boot-starter-data-jpa
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-starter-hateoas
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-starter-web
51 |
52 |
53 | com.h2database
54 | h2
55 |
56 |
57 | org.springframework.boot
58 | spring-boot-starter-test
59 | test
60 |
61 |
62 | org.apache.commons
63 | commons-lang3
64 | 3.6
65 |
66 |
67 |
68 |
69 |
70 |
71 | org.apache.maven.plugins
72 | maven-compiler-plugin
73 | 3.2
74 |
75 | ${java.version}
76 | ${java.version}
77 |
78 |
79 |
80 | org.springframework.boot
81 | spring-boot-maven-plugin
82 |
83 |
84 |
85 |
86 | org.jacoco
87 | jacoco-maven-plugin
88 | 0.7.9
89 |
90 |
91 | jacoco-initialize
92 |
93 | prepare-agent
94 |
95 |
96 |
97 | jacoco-site
98 | package
99 |
100 | report
101 |
102 |
103 |
104 |
105 |
106 | org.eluder.coveralls
107 | coveralls-maven-plugin
108 | 4.3.0
109 |
110 | ${maven.build.timestamp.format}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | spring-snapshots
119 | Spring Snapshots
120 | https://repo.spring.io/snapshot
121 |
122 | true
123 |
124 |
125 |
126 | spring-milestones
127 | Spring Milestones
128 | https://repo.spring.io/milestone
129 |
130 | false
131 |
132 |
133 |
134 |
135 |
136 |
137 | spring-snapshots
138 | Spring Snapshots
139 | https://repo.spring.io/snapshot
140 |
141 | true
142 |
143 |
144 |
145 | spring-milestones
146 | Spring Milestones
147 | https://repo.spring.io/milestone
148 |
149 | false
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/JavaRestBooksApplication.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class JavaRestBooksApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(JavaRestBooksApplication.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/controller/BookController.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.controller;
2 |
3 | import com.github.sbouclier.javarestbooks.domain.Book;
4 | import com.github.sbouclier.javarestbooks.exception.BookIsbnAlreadyExistsException;
5 | import com.github.sbouclier.javarestbooks.exception.BookNotFoundException;
6 | import com.github.sbouclier.javarestbooks.repository.BookRepository;
7 | import org.springframework.data.domain.Page;
8 | import org.springframework.data.domain.PageRequest;
9 | import org.springframework.data.domain.Pageable;
10 | import org.springframework.data.domain.Sort;
11 | import org.springframework.data.web.PageableDefault;
12 | import org.springframework.http.HttpHeaders;
13 | import org.springframework.http.HttpStatus;
14 | import org.springframework.http.ResponseEntity;
15 | import org.springframework.web.bind.annotation.*;
16 | import org.springframework.web.util.UriComponentsBuilder;
17 |
18 | import javax.validation.Valid;
19 | import java.util.List;
20 |
21 | import static org.springframework.web.util.UriComponentsBuilder.fromUriString;
22 |
23 | /**
24 | * Book controller
25 | *
26 | * @author Stéphane Bouclier
27 | *
28 | */
29 | @RestController
30 | @RequestMapping(value = "/api/books")
31 | public class BookController {
32 |
33 | private static final int MAX_PAGE_SIZE = 50;
34 |
35 | private final BookRepository bookRepository;
36 |
37 | public BookController(BookRepository bookRepository) {
38 | this.bookRepository = bookRepository;
39 | }
40 |
41 | @PostMapping
42 | public ResponseEntity> createBook(@Valid @RequestBody Book book, UriComponentsBuilder ucBuilder) {
43 | if (bookRepository.findByIsbn(book.getIsbn()).isPresent()) {
44 | throw new BookIsbnAlreadyExistsException(book.getIsbn());
45 | }
46 | bookRepository.save(book);
47 |
48 | HttpHeaders headers = new HttpHeaders();
49 | headers.setLocation(ucBuilder.path("/api/books/{isbn}").buildAndExpand(book.getIsbn()).toUri());
50 | return new ResponseEntity<>(headers, HttpStatus.CREATED);
51 | }
52 |
53 | @GetMapping("/{isbn}")
54 | public ResponseEntity getBook(@PathVariable("isbn") String isbn) {
55 | return bookRepository.findByIsbn(isbn)
56 | .map(book -> new ResponseEntity<>(book, HttpStatus.OK))
57 | .orElseThrow(() -> new BookNotFoundException(isbn));
58 | }
59 |
60 | @GetMapping
61 | public ResponseEntity> getAllBooks(
62 | @PageableDefault(size = MAX_PAGE_SIZE) Pageable pageable,
63 | @RequestParam(required = false, defaultValue = "id") String sort,
64 | @RequestParam(required = false, defaultValue = "asc") String order) {
65 | final PageRequest pr = PageRequest.of(
66 | pageable.getPageNumber(), pageable.getPageSize(),
67 | Sort.by("asc" .equals(order) ? Sort.Direction.ASC : Sort.Direction.DESC, sort)
68 | );
69 |
70 | Page booksPage = bookRepository.findAll(pr);
71 |
72 | if (booksPage.getContent().isEmpty()) {
73 | return new ResponseEntity(HttpStatus.NO_CONTENT);
74 | } else {
75 | long totalBooks = booksPage.getTotalElements();
76 | int nbPageBooks = booksPage.getNumberOfElements();
77 |
78 | HttpHeaders headers = new HttpHeaders();
79 | headers.add("X-Total-Count", String.valueOf(totalBooks));
80 |
81 | if (nbPageBooks < totalBooks) {
82 | headers.add("first", buildPageUri(PageRequest.of(0, booksPage.getSize())));
83 | headers.add("last", buildPageUri(PageRequest.of(booksPage.getTotalPages() - 1, booksPage.getSize())));
84 |
85 | if (booksPage.hasNext()) {
86 | headers.add("next", buildPageUri(booksPage.nextPageable()));
87 | }
88 |
89 | if (booksPage.hasPrevious()) {
90 | headers.add("prev", buildPageUri(booksPage.previousPageable()));
91 | }
92 |
93 | return new ResponseEntity<>(booksPage.getContent(), headers, HttpStatus.PARTIAL_CONTENT);
94 | } else {
95 | return new ResponseEntity(booksPage.getContent(), headers, HttpStatus.OK);
96 | }
97 | }
98 | }
99 |
100 | @PutMapping("/{isbn}")
101 | public ResponseEntity updateBook(@PathVariable("isbn") String isbn, @Valid @RequestBody Book book) {
102 | return bookRepository.findByIsbn(isbn)
103 | .map(bookToUpdate -> {
104 | bookToUpdate.setIsbn(book.getIsbn());
105 | bookToUpdate.setTitle(book.getTitle());
106 | bookToUpdate.setDescription(book.getDescription());
107 | bookToUpdate.setAuthors(book.getAuthors());
108 | bookToUpdate.setPublisher(book.getPublisher());
109 | bookRepository.save(bookToUpdate);
110 |
111 | return new ResponseEntity<>(bookToUpdate, HttpStatus.OK);
112 | })
113 | .orElseThrow(() -> new BookNotFoundException(isbn));
114 | }
115 |
116 | @PatchMapping("/{isbn}")
117 | public ResponseEntity updateBookDescription(@PathVariable("isbn") String isbn, @RequestBody String description) {
118 | return bookRepository.findByIsbn(isbn)
119 | .map(book -> {
120 | book.setDescription(description);
121 | bookRepository.save(book);
122 |
123 | return new ResponseEntity<>(book, HttpStatus.OK);
124 | })
125 | .orElseThrow(() -> new BookNotFoundException(isbn));
126 | }
127 |
128 | @DeleteMapping("/{isbn}")
129 | public ResponseEntity> deleteBook(@PathVariable("isbn") String isbn) {
130 | return bookRepository.findByIsbn(isbn)
131 | .map(book -> {
132 | bookRepository.delete(book);
133 | return new ResponseEntity(HttpStatus.NO_CONTENT);
134 | })
135 | .orElseThrow(() -> new BookNotFoundException(isbn));
136 | }
137 |
138 | private String buildPageUri(Pageable page) {
139 | return fromUriString("/api/books")
140 | .query("page={page}&size={size}")
141 | .buildAndExpand(page.getPageNumber(), page.getPageSize())
142 | .toUriString();
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/controller/BookControllerAdvice.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.controller;
2 |
3 | import com.github.sbouclier.javarestbooks.exception.BookIsbnAlreadyExistsException;
4 | import com.github.sbouclier.javarestbooks.exception.BookNotFoundException;
5 | import org.springframework.hateoas.VndErrors;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.web.bind.annotation.*;
8 |
9 | @ControllerAdvice
10 | @RequestMapping(produces = "application/vnd.error")
11 | public class BookControllerAdvice {
12 |
13 | @ResponseBody
14 | @ExceptionHandler(BookNotFoundException.class)
15 | @ResponseStatus(HttpStatus.NOT_FOUND)
16 | VndErrors bookNotFoundExceptionHandler(BookNotFoundException ex) {
17 | return new VndErrors("error", ex.getMessage());
18 | }
19 |
20 | @ResponseBody
21 | @ExceptionHandler(BookIsbnAlreadyExistsException.class)
22 | @ResponseStatus(HttpStatus.CONFLICT)
23 | VndErrors bookIsbnAlreadyExistsExceptionHandler(BookIsbnAlreadyExistsException ex) {
24 | return new VndErrors("error", ex.getMessage());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/domain/Author.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.domain;
2 |
3 | import org.apache.commons.lang3.builder.ToStringBuilder;
4 | import org.apache.commons.lang3.builder.ToStringStyle;
5 |
6 | import javax.persistence.Embeddable;
7 |
8 | /**
9 | * Author embeddable entity
10 | *
11 | * @author Stéphane Bouclier
12 | *
13 | */
14 | @Embeddable
15 | public class Author {
16 |
17 | private String firstName;
18 |
19 | private String lastName;
20 |
21 | private Author() {
22 | // Default constructor for Jackson
23 | }
24 |
25 | public Author(String firstName, String lastName) {
26 | this.firstName = firstName;
27 | this.lastName = lastName;
28 | }
29 |
30 | public String getFirstName() {
31 | return firstName;
32 | }
33 |
34 | public String getLastName() {
35 | return lastName;
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
41 | .append("firstName", firstName)
42 | .append("lastName", lastName)
43 | .toString();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/domain/Book.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.domain;
2 |
3 | import org.apache.commons.lang3.builder.ToStringBuilder;
4 | import org.apache.commons.lang3.builder.ToStringStyle;
5 | import org.hibernate.validator.constraints.NotBlank;
6 | import org.hibernate.validator.constraints.NotEmpty;
7 |
8 | import javax.persistence.*;
9 | import java.util.HashSet;
10 | import java.util.Set;
11 |
12 | /**
13 | * Book entity
14 | *
15 | * @author Stéphane Bouclier
16 | *
17 | */
18 | @Entity
19 | @Table(uniqueConstraints = { @UniqueConstraint(name = "uk_book_isbn", columnNames = "isbn") })
20 | public class Book {
21 |
22 | @Id
23 | @GeneratedValue(strategy = GenerationType.IDENTITY)
24 | private Long id;
25 |
26 | @NotBlank
27 | private String isbn;
28 |
29 | @NotBlank
30 | private String title;
31 |
32 | private String description;
33 |
34 | @ElementCollection
35 | @NotEmpty
36 | private Set authors;
37 |
38 | @NotBlank
39 | private String publisher;
40 |
41 | // ----------------
42 | // - CONSTRUCTORS -
43 | // ----------------
44 |
45 | private Book() {
46 | // Default constructor for Jackson
47 | }
48 |
49 | public Book(String isbn, String title, Set authors, String publisher) {
50 | this.isbn = isbn;
51 | this.title = title;
52 | this.authors = authors;
53 | this.publisher = publisher;
54 | }
55 |
56 | public Book(String isbn, String title, String publisher) {
57 | this(isbn, title, new HashSet<>(), publisher);
58 | }
59 |
60 | // -----------
61 | // - METHODS -
62 | // -----------
63 |
64 | public void addAuthor(Author author) {
65 | this.authors.add(author);
66 | }
67 |
68 | // -------------
69 | // - TO STRING -
70 | // -------------
71 |
72 | @Override
73 | public String toString() {
74 | return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
75 | .append("id", id)
76 | .append("isbn", isbn)
77 | .append("title", title)
78 | .append("description", description)
79 | .append("authors", authors)
80 | .append("publisher", publisher)
81 | .toString();
82 | }
83 |
84 | // -------------------
85 | // - SETTERS/GETTERS -
86 | // -------------------
87 |
88 | public Long getId() {
89 | return id;
90 | }
91 |
92 | public void setId(Long id) {
93 | this.id = id;
94 | }
95 |
96 | public String getIsbn() {
97 | return isbn;
98 | }
99 |
100 | public void setIsbn(String isbn) {
101 | this.isbn = isbn;
102 | }
103 |
104 | public String getTitle() {
105 | return title;
106 | }
107 |
108 | public void setTitle(String title) {
109 | this.title = title;
110 | }
111 |
112 | public String getDescription() {
113 | return description;
114 | }
115 |
116 | public void setDescription(String description) {
117 | this.description = description;
118 | }
119 |
120 | public Set getAuthors() {
121 | return authors;
122 | }
123 |
124 | public void setAuthors(Set authors) {
125 | this.authors = authors;
126 | }
127 |
128 | public String getPublisher() {
129 | return publisher;
130 | }
131 |
132 | public void setPublisher(String publisher) {
133 | this.publisher = publisher;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/exception/BookIsbnAlreadyExistsException.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.exception;
2 |
3 | /**
4 | * BookIsbnAlreadyExists exception
5 | *
6 | * @author Stéphane Bouclier
7 | *
8 | */
9 | public class BookIsbnAlreadyExistsException extends RuntimeException {
10 |
11 | public BookIsbnAlreadyExistsException(String isbn) {
12 | super("book already exists for ISBN: '" + isbn + "'");
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/exception/BookNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.exception;
2 |
3 | /**
4 | * BookNotFound exception
5 | *
6 | * @author Stéphane Bouclier
7 | *
8 | */
9 | public class BookNotFoundException extends RuntimeException {
10 |
11 | public BookNotFoundException(String isbn) {
12 | super("could not find book with ISBN: '" + isbn + "'");
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sbouclier/javarestbooks/repository/BookRepository.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.repository;
2 |
3 | import com.github.sbouclier.javarestbooks.domain.Book;
4 | import org.springframework.data.repository.PagingAndSortingRepository;
5 |
6 | import java.util.Optional;
7 |
8 | /**
9 | * Book repository
10 | *
11 | * @author Stéphane Bouclier
12 | *
13 | */
14 | public interface BookRepository extends PagingAndSortingRepository {
15 | Optional findByIsbn(String isbn);
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # H2
2 | spring.h2.console.enabled=true
3 | spring.h2.console.path=/h2
4 |
5 | # Datasource
6 | spring.datasource.url=jdbc:h2:mem:book
7 | spring.datasource.username=sa
8 | spring.datasource.password=
9 | spring.datasource.driver-class-name=org.h2.Driver
--------------------------------------------------------------------------------
/src/main/resources/import.sql:
--------------------------------------------------------------------------------
1 | -- books
2 | insert into book(isbn,title,publisher) values ('978-0321356680','Effective Java','Addison Wesley');
3 | insert into book(isbn,title,publisher) values ('978-1617292545','Spring Boot in Action','Manning Publications');
4 | insert into book(isbn,title,publisher) values ('978-1491900864','Java 8 Pocket Guide','O''Reilly');
5 | insert into book(isbn,title,publisher) values ('978-0321349606','Java Concurrency in Practice','Addison Wesley');
6 |
7 | -- authors
8 | insert into book_authors(book_id,first_name,last_name) values (1,'Joshua', 'Blosh');
9 | insert into book_authors(book_id,first_name,last_name) values (2,'Craig', 'Walls');
10 | insert into book_authors(book_id,first_name,last_name) values (3,'Robert', 'Liguori');
11 | insert into book_authors(book_id,first_name,last_name) values (3,'Patricia', 'Liguori');
12 | insert into book_authors(book_id,first_name,last_name) values (4,'Brian', 'Goetz');
13 | insert into book_authors(book_id,first_name,last_name) values (4,'Joshua', 'Blosh');
14 | insert into book_authors(book_id,first_name,last_name) values (4,'Joseph', 'Bowbeer');
15 | insert into book_authors(book_id,first_name,last_name) values (4,'Tim', 'Peierls');
--------------------------------------------------------------------------------
/src/test/java/com/github/sbouclier/javarestbooks/JavaRestBooksApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks;
2 |
3 | import org.assertj.core.util.Arrays;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.test.context.junit4.SpringRunner;
8 |
9 | @RunWith(SpringRunner.class)
10 | @SpringBootTest
11 | public class JavaRestBooksApplicationTests {
12 |
13 | @Test
14 | public void contextLoads() {
15 | JavaRestBooksApplication.main(Arrays.array());
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/java/com/github/sbouclier/javarestbooks/controller/BookControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.controller;
2 |
3 | import com.github.sbouclier.javarestbooks.JavaRestBooksApplication;
4 | import com.github.sbouclier.javarestbooks.domain.Author;
5 | import com.github.sbouclier.javarestbooks.domain.Book;
6 | import org.junit.Assert;
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
11 | import org.springframework.boot.test.context.SpringBootTest;
12 | import org.springframework.http.MediaType;
13 | import org.springframework.http.converter.HttpMessageConverter;
14 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
15 | import org.springframework.mock.http.MockHttpOutputMessage;
16 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
17 | import org.springframework.test.web.servlet.MockMvc;
18 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
19 | import org.springframework.transaction.annotation.Transactional;
20 |
21 | import java.io.IOException;
22 | import java.util.Arrays;
23 |
24 | import static org.hamcrest.CoreMatchers.containsString;
25 | import static org.hamcrest.Matchers.contains;
26 | import static org.hamcrest.Matchers.hasSize;
27 | import static org.hamcrest.Matchers.nullValue;
28 | import static org.hamcrest.core.Is.is;
29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
30 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
31 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
32 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
34 |
35 | /**
36 | * BookController test
37 | *
38 | * @author Stéphane Bouclier
39 | *
40 | */
41 | @RunWith(SpringJUnit4ClassRunner.class)
42 | @SpringBootTest(classes = JavaRestBooksApplication.class)
43 | @AutoConfigureMockMvc
44 | @Transactional
45 | public class BookControllerTest {
46 |
47 | @Autowired
48 | private MockMvc mockMvc;
49 |
50 | private HttpMessageConverter mappingJackson2HttpMessageConverter;
51 |
52 | @Autowired
53 | void setConverters(HttpMessageConverter>[] converters) {
54 | this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream()
55 | .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
56 | Assert.assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter);
57 | }
58 |
59 | @SuppressWarnings("unchecked")
60 | protected String json(Object o) throws IOException {
61 | MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
62 | this.mappingJackson2HttpMessageConverter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
63 | return mockHttpOutputMessage.getBodyAsString();
64 | }
65 |
66 | // ---------- create book ----------
67 |
68 | @Test
69 | public void should_create_valid_book_and_return_created_status() throws Exception {
70 | Book book = new Book("123-1234567890","My new book","Publisher");
71 | book.addAuthor(new Author("John","Doe"));
72 |
73 | mockMvc.perform(post("/api/books")
74 | .contentType(MediaType.APPLICATION_JSON)
75 | .content(json(book)))
76 | .andExpect(status().isCreated())
77 | .andExpect(header().string("Location", is("http://localhost/api/books/123-1234567890")))
78 | .andExpect(content().string(""))
79 | .andDo(MockMvcResultHandlers.print());
80 | }
81 |
82 | @Test
83 | public void should_not_create_invalid_content_book_and_return_bad_request_status() throws Exception {
84 | Book book = new Book(null,"My new book","Publisher");
85 |
86 | mockMvc.perform(post("/api/books")
87 | .contentType(MediaType.APPLICATION_JSON)
88 | .content(json(book)))
89 | .andExpect(status().isBadRequest())
90 | .andDo(MockMvcResultHandlers.print());
91 | }
92 |
93 | @Test
94 | public void should_not_create_existing_book_and_return_conflict_status() throws Exception {
95 | Book book = new Book("978-0321356680","My new book","Publisher");
96 | book.addAuthor(new Author("John","Doe"));
97 |
98 | mockMvc.perform(post("/api/books")
99 | .contentType(MediaType.APPLICATION_JSON)
100 | .content(json(book)))
101 | .andExpect(status().isConflict())
102 | .andDo(MockMvcResultHandlers.print());
103 | }
104 |
105 | @Test
106 | public void should_not_allow_others_http_methods() throws Exception {
107 | Book book = new Book("123-1234567890","My new book","Publisher");
108 | book.addAuthor(new Author("John","Doe"));
109 |
110 | mockMvc.perform(put("/api/books")
111 | .contentType(MediaType.APPLICATION_JSON)
112 | .content(json(book)))
113 | .andExpect(status().isMethodNotAllowed())
114 | .andExpect(content().string(""))
115 | .andDo(MockMvcResultHandlers.print());
116 | }
117 |
118 | // ---------- get book ----------
119 |
120 | @Test
121 | public void should_get_valid_book_with_ok_status() throws Exception {
122 | mockMvc.perform(get("/api/books/978-0321356680").contentType(MediaType.APPLICATION_JSON))
123 | .andExpect(status().isOk())
124 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
125 | .andExpect(jsonPath("$.id", is(1)))
126 | .andExpect(jsonPath("$.title", is("Effective Java")))
127 | .andExpect(jsonPath("$.publisher", is("Addison Wesley")))
128 | .andDo(MockMvcResultHandlers.print());
129 | }
130 |
131 | @Test
132 | public void should_no_get_unknown_book_with_not_found_status() throws Exception {
133 | mockMvc.perform(get("/api/books/000-1234567890").contentType(MediaType.APPLICATION_JSON))
134 | .andExpect(status().isNotFound())
135 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
136 | .andExpect(jsonPath("$[0].logref", is("error")))
137 | .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
138 | .andDo(MockMvcResultHandlers.print());
139 | }
140 |
141 | // ---------- get books ----------
142 |
143 | @Test
144 | public void should_get_all_books_with_ok_status() throws Exception {
145 | mockMvc.perform(get("/api/books").contentType(MediaType.APPLICATION_JSON))
146 | .andExpect(status().isOk())
147 | .andExpect(header().string("X-Total-Count", is("4")))
148 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
149 | .andExpect(jsonPath("$", hasSize(4)))
150 | .andExpect(jsonPath("$[*].id", contains(1,2,3,4))) // sorted by id asc by default
151 | .andDo(MockMvcResultHandlers.print());
152 | }
153 |
154 | @Test
155 | public void should_get_first_page_paginated_books() throws Exception {
156 | mockMvc.perform(get("/api/books?page=0&size=2").contentType(MediaType.APPLICATION_JSON))
157 | .andExpect(status().isPartialContent())
158 | .andExpect(header().string("X-Total-Count", is("4")))
159 | .andExpect(header().string("first", is("/api/books?page=0&size=2")))
160 | .andExpect(header().string("last", is("/api/books?page=1&size=2")))
161 | .andExpect(header().string("prev", is(nullValue())))
162 | .andExpect(header().string("next", is("/api/books?page=1&size=2")))
163 | .andExpect(header().string("X-Total-Count", is("4")))
164 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
165 | .andExpect(jsonPath("$", hasSize(2)))
166 | .andExpect(jsonPath("$[*].id", contains(1,2))) // sorted by id asc by default
167 | .andDo(MockMvcResultHandlers.print());
168 | }
169 |
170 | @Test
171 | public void should_get_last_page_paginated_books() throws Exception {
172 | mockMvc.perform(get("/api/books?page=1&size=2").contentType(MediaType.APPLICATION_JSON))
173 | .andExpect(status().isPartialContent())
174 | .andExpect(header().string("X-Total-Count", is("4")))
175 | .andExpect(header().string("first", is("/api/books?page=0&size=2")))
176 | .andExpect(header().string("last", is("/api/books?page=1&size=2")))
177 | .andExpect(header().string("prev", is("/api/books?page=0&size=2")))
178 | .andExpect(header().string("next", is(nullValue())))
179 | .andExpect(jsonPath("$", hasSize(2)))
180 | .andExpect(jsonPath("$[*].id", contains(3,4))) // sorted by id asc by default
181 | .andDo(MockMvcResultHandlers.print());
182 | }
183 |
184 | @Test
185 | public void should_sort_books() throws Exception {
186 | mockMvc.perform(get("/api/books?sort=title&order=desc").contentType(MediaType.APPLICATION_JSON))
187 | .andExpect(status().isOk())
188 | .andExpect(header().string("X-Total-Count", is("4")))
189 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
190 | .andExpect(jsonPath("$", hasSize(4)))
191 | .andExpect(jsonPath("$[*].id", contains(2,4,3,1)))
192 | .andDo(MockMvcResultHandlers.print());
193 | }
194 |
195 | @Test
196 | public void should_not_get_books_for_bad_pagination() throws Exception {
197 | mockMvc.perform(get("/api/books?page=999").contentType(MediaType.APPLICATION_JSON))
198 | .andExpect(status().isNoContent())
199 | .andDo(MockMvcResultHandlers.print());
200 | }
201 |
202 | // ---------- update book ----------
203 |
204 | @Test
205 | public void should_update_valid_book_and_return_ok_status() throws Exception {
206 | Book book = new Book("978-0321356680","Book updated","Publisher");
207 | book.setDescription("New description");
208 |
209 | Author author = new Author("John","Doe");
210 | book.addAuthor(author);
211 |
212 | mockMvc.perform(put("/api/books/978-0321356680")
213 | .contentType(MediaType.APPLICATION_JSON)
214 | .content(json(book)))
215 | .andExpect(status().isOk())
216 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
217 | .andExpect(jsonPath("$.id", is(1)))
218 | .andExpect(jsonPath("$.title", is("Book updated")))
219 | .andExpect(jsonPath("$.description", is("New description")))
220 | .andExpect(jsonPath("$.publisher", is("Publisher")))
221 | .andExpect(jsonPath("$.authors[0].firstName", is("John")))
222 | .andExpect(jsonPath("$.authors[0].lastName", is("Doe")))
223 | .andDo(MockMvcResultHandlers.print());
224 | }
225 |
226 | @Test
227 | public void should_not_update_unknown_book_and_return_not_found_status() throws Exception {
228 | Book book = new Book("978-0321356680","Book updated","Publisher");
229 | book.setDescription("New description");
230 |
231 | Author author = new Author("John","Doe");
232 | book.addAuthor(author);
233 |
234 | mockMvc.perform(put("/api/books/000-1234567890")
235 | .contentType(MediaType.APPLICATION_JSON)
236 | .content(json(book)))
237 | .andExpect(status().isNotFound())
238 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
239 | .andExpect(jsonPath("$[0].logref", is("error")))
240 | .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
241 | .andDo(MockMvcResultHandlers.print());
242 | }
243 |
244 | // ---------- update book's description ----------
245 |
246 | @Test
247 | public void should_update_existing_book_description_and_return_ok_status() throws Exception {
248 | mockMvc.perform(patch("/api/books/978-0321356680")
249 | .contentType(MediaType.APPLICATION_JSON)
250 | .content("new description"))
251 | .andExpect(status().isOk())
252 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
253 | .andExpect(jsonPath("$.title", is("Effective Java")))
254 | .andExpect(jsonPath("$.description", is("new description")))
255 | .andDo(MockMvcResultHandlers.print());
256 | }
257 |
258 | @Test
259 | public void should_not_update_description_of_unknown_book_and_return_not_found_status() throws Exception {
260 | mockMvc.perform(patch("/api/books/000-1234567890")
261 | .contentType(MediaType.APPLICATION_JSON)
262 | .content("new description"))
263 | .andExpect(status().isNotFound())
264 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
265 | .andExpect(jsonPath("$[0].logref", is("error")))
266 | .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
267 | .andDo(MockMvcResultHandlers.print());
268 | }
269 |
270 | // ---------- delete book ----------
271 |
272 | @Test
273 | public void should_delete_existing_book_and_return_no_content_status() throws Exception {
274 | mockMvc.perform(delete("/api/books/978-0321356680")
275 | .contentType(MediaType.APPLICATION_JSON))
276 | .andExpect(status().isNoContent())
277 | .andExpect(content().string(""))
278 | .andDo(MockMvcResultHandlers.print());
279 | }
280 |
281 | @Test
282 | public void should_not_delete_unknown_book_and_return_not_found_status() throws Exception {
283 | mockMvc.perform(delete("/api/books/000-1234567890")
284 | .contentType(MediaType.APPLICATION_JSON))
285 | .andExpect(status().isNotFound())
286 | .andExpect(jsonPath("$[0].logref", is("error")))
287 | .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
288 | .andDo(MockMvcResultHandlers.print());
289 | }
290 | }
--------------------------------------------------------------------------------
/src/test/java/com/github/sbouclier/javarestbooks/domain/AuthorTest.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.domain;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.hamcrest.MatcherAssert.assertThat;
6 | import static org.hamcrest.Matchers.is;
7 |
8 | /**
9 | * Author test
10 | *
11 | * @author Stéphane Bouclier
12 | *
13 | */
14 | public class AuthorTest {
15 |
16 | @Test
17 | public void should_return_to_string() {
18 |
19 | // Given
20 | final Author author = new Author("John", "Doe");
21 |
22 | // When
23 | final String toString = author.toString();
24 |
25 | // Then
26 | assertThat(toString, is("Author[firstName=John,lastName=Doe]"));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/test/java/com/github/sbouclier/javarestbooks/domain/BookTest.java:
--------------------------------------------------------------------------------
1 | package com.github.sbouclier.javarestbooks.domain;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.hamcrest.MatcherAssert.assertThat;
6 | import static org.hamcrest.Matchers.is;
7 |
8 | /**
9 | * Book test
10 | *
11 | * @author Stéphane Bouclier
12 | *
13 | */
14 | public class BookTest {
15 |
16 | @Test
17 | public void should_return_to_string() {
18 |
19 | // Given
20 | final Author author = new Author("John", "Doe");
21 | final Book book = new Book("isbn", "title", "publisher");
22 | book.addAuthor(author);
23 |
24 | StringBuilder expectedString = new StringBuilder("Book[id=,isbn=isbn,title=title,description=,");
25 | expectedString.append("authors=[Author[firstName=John,lastName=Doe]],publisher=publisher]");
26 |
27 | // When
28 | final String toString = book.toString();
29 |
30 | // Then
31 | assertThat(toString, is(expectedString.toString()));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------