├── README.md ├── gamification ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── microservices │ │ │ └── book │ │ │ ├── GamificationApplication.java │ │ │ └── gamification │ │ │ ├── client │ │ │ ├── MultiplicationResultAttemptClient.java │ │ │ ├── MultiplicationResultAttemptClientImpl.java │ │ │ ├── MultiplicationResultAttemptDeserializer.java │ │ │ └── dto │ │ │ │ └── MultiplicationResultAttempt.java │ │ │ ├── configuration │ │ │ ├── RabbitMQConfiguration.java │ │ │ └── RestClientConfiguration.java │ │ │ ├── controller │ │ │ ├── LeaderBoardController.java │ │ │ └── UserStatsController.java │ │ │ ├── domain │ │ │ ├── Badge.java │ │ │ ├── BadgeCard.java │ │ │ ├── GameStats.java │ │ │ ├── LeaderBoardRow.java │ │ │ └── ScoreCard.java │ │ │ ├── event │ │ │ ├── EventHandler.java │ │ │ └── MultiplicationSolvedEvent.java │ │ │ ├── repository │ │ │ ├── BadgeCardRepository.java │ │ │ └── ScoreCardRepository.java │ │ │ └── service │ │ │ ├── GameService.java │ │ │ ├── GameServiceImpl.java │ │ │ ├── LeaderBoardService.java │ │ │ └── LeaderBoardServiceImpl.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── microservices │ └── book │ └── gamification │ ├── controller │ ├── LeaderBoardControllerTest.java │ └── UserStatsControllerTest.java │ ├── event │ └── EventHandlerTest.java │ └── service │ ├── GameServiceImplTest.java │ └── LeaderBoardServiceImplTest.java └── social-multiplication ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml ├── social-multiplication └── README.md └── src ├── main ├── java │ └── microservices │ │ └── book │ │ ├── SocialMultiplicationApplication.java │ │ └── multiplication │ │ ├── configuration │ │ └── RabbitMQConfiguration.java │ │ ├── controller │ │ ├── MultiplicationController.java │ │ └── MultiplicationResultAttemptController.java │ │ ├── domain │ │ ├── Multiplication.java │ │ ├── MultiplicationResultAttempt.java │ │ └── User.java │ │ ├── event │ │ ├── EventDispatcher.java │ │ └── MultiplicationSolvedEvent.java │ │ ├── repository │ │ ├── MultiplicationRepository.java │ │ ├── MultiplicationResultAttemptRepository.java │ │ └── UserRepository.java │ │ └── service │ │ ├── MultiplicationService.java │ │ ├── MultiplicationServiceImpl.java │ │ ├── RandomGeneratorService.java │ │ └── RandomGeneratorServiceImpl.java └── resources │ ├── application.properties │ └── static │ ├── index.html │ ├── multiplication-client.js │ └── styles.css └── test └── java └── microservices └── book └── multiplication ├── controller ├── MultiplicationControllerTest.java └── MultiplicationResultAttemptControllerTest.java └── service ├── MultiplicationServiceImplTest.java └── RandomGeneratorServiceImplTest.java /README.md: -------------------------------------------------------------------------------- 1 | # Learn Microservices with Spring Boot - v5 2 | 3 | This project contains the version 5 of the application that is developed under the scope of the book *Learn Microservices with Spring Boot*. You can get a copy of the book on [Amazon](http://amzn.to/2FSB2ME) or [Apress](http://www.apress.com/book/9781484231647). 4 | 5 | The book shows you how to evolve a simple Spring Boot application to become a full Microservices Architecture, using Spring Cloud Eureka, Ribbon, Zuul and Hystrix to implement Service Discovery, Load Balancing, the API Gateway pattern and a Circuit Breaker. Besides, you'll learn how to implement End-to-End tests with Cucumber, an Event-Driven system and the best practices when building Microservices. 6 | -------------------------------------------------------------------------------- /gamification/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /gamification/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microservices-practical/microservices-v5/b5d42cf7be3ae03fceca8cb62b6e530f05a38a3c/gamification/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /gamification/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip 2 | -------------------------------------------------------------------------------- /gamification/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 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /gamification/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 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %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% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /gamification/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | microservices.book 7 | gamification-v5 8 | 0.5.0-SNAPSHOT 9 | jar 10 | 11 | gamification-v5 12 | Social Multiplication App - Gamification (Microservices - the Practical Way book) 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.7.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-amqp 36 | 37 | 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | 1.16.12 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-data-jpa 48 | 49 | 50 | 51 | com.h2database 52 | h2 53 | runtime 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/GamificationApplication.java: -------------------------------------------------------------------------------- 1 | package microservices.book; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class GamificationApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(GamificationApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/client/MultiplicationResultAttemptClient.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.client; 2 | 3 | import microservices.book.gamification.client.dto.MultiplicationResultAttempt; 4 | 5 | /** 6 | * This interface allows us to connect to the Multiplication microservice. 7 | * Note that it's agnostic to the way of communication. 8 | */ 9 | public interface MultiplicationResultAttemptClient { 10 | 11 | MultiplicationResultAttempt retrieveMultiplicationResultAttemptbyId(final Long multiplicationId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/client/MultiplicationResultAttemptClientImpl.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.client; 2 | 3 | import microservices.book.gamification.client.dto.MultiplicationResultAttempt; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | /** 10 | * This implementation of MultiplicationResultAttemptClient interface connects to 11 | * the Multiplication microservice via REST. 12 | */ 13 | @Component 14 | class MultiplicationResultAttemptClientImpl implements MultiplicationResultAttemptClient { 15 | 16 | private final RestTemplate restTemplate; 17 | private final String multiplicationHost; 18 | 19 | @Autowired 20 | public MultiplicationResultAttemptClientImpl(final RestTemplate restTemplate, 21 | @Value("${multiplicationHost}") final String multiplicationHost) { 22 | this.restTemplate = restTemplate; 23 | this.multiplicationHost = multiplicationHost; 24 | } 25 | 26 | @Override 27 | public MultiplicationResultAttempt retrieveMultiplicationResultAttemptbyId(final Long multiplicationResultAttemptId) { 28 | return restTemplate.getForObject( 29 | multiplicationHost + "/results/" + multiplicationResultAttemptId, 30 | MultiplicationResultAttempt.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/client/MultiplicationResultAttemptDeserializer.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.client; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.core.ObjectCodec; 6 | import com.fasterxml.jackson.databind.DeserializationContext; 7 | import com.fasterxml.jackson.databind.JsonDeserializer; 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import microservices.book.gamification.client.dto.MultiplicationResultAttempt; 10 | 11 | import java.io.IOException; 12 | 13 | /** 14 | * Deserializes an attempt coming from the Multiplication microservice 15 | * into the Gamification's representation of an attempt. 16 | */ 17 | public class MultiplicationResultAttemptDeserializer 18 | extends JsonDeserializer { 19 | 20 | @Override 21 | public MultiplicationResultAttempt deserialize(JsonParser jsonParser, 22 | DeserializationContext deserializationContext) 23 | throws IOException, JsonProcessingException { 24 | ObjectCodec oc = jsonParser.getCodec(); 25 | JsonNode node = oc.readTree(jsonParser); 26 | return new MultiplicationResultAttempt(node.get("user").get("alias").asText(), 27 | node.get("multiplication").get("factorA").asInt(), 28 | node.get("multiplication").get("factorB").asInt(), 29 | node.get("resultAttempt").asInt(), 30 | node.get("correct").asBoolean()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/client/dto/MultiplicationResultAttempt.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.client.dto; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.ToString; 8 | import microservices.book.gamification.client.MultiplicationResultAttemptDeserializer; 9 | 10 | /** 11 | * Identifies the attempt from a user to solve a multiplication. 12 | */ 13 | @RequiredArgsConstructor 14 | @Getter 15 | @ToString 16 | @EqualsAndHashCode 17 | @JsonDeserialize(using = MultiplicationResultAttemptDeserializer.class) 18 | public final class MultiplicationResultAttempt { 19 | 20 | private final String userAlias; 21 | 22 | private final int multiplicationFactorA; 23 | private final int multiplicationFactorB; 24 | private final int resultAttempt; 25 | 26 | private final boolean correct; 27 | 28 | // Empty constructor for JSON/JPA 29 | MultiplicationResultAttempt() { 30 | userAlias = null; 31 | multiplicationFactorA = -1; 32 | multiplicationFactorB = -1; 33 | resultAttempt = -1; 34 | correct = false; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/configuration/RabbitMQConfiguration.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.configuration; 2 | 3 | import org.springframework.amqp.core.Binding; 4 | import org.springframework.amqp.core.BindingBuilder; 5 | import org.springframework.amqp.core.Queue; 6 | import org.springframework.amqp.core.TopicExchange; 7 | import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer; 8 | import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.messaging.converter.MappingJackson2MessageConverter; 13 | import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; 14 | 15 | /** 16 | * Configures RabbitMQ to use events in our application. 17 | */ 18 | @Configuration 19 | public class RabbitMQConfiguration implements RabbitListenerConfigurer { 20 | 21 | @Bean 22 | public TopicExchange multiplicationExchange(@Value("${multiplication.exchange}") final String exchangeName) { 23 | return new TopicExchange(exchangeName); 24 | } 25 | 26 | @Bean 27 | public Queue gamificationMultiplicationQueue(@Value("${multiplication.queue}") final String queueName) { 28 | return new Queue(queueName, true); 29 | } 30 | 31 | @Bean 32 | Binding binding(final Queue queue, final TopicExchange exchange, 33 | @Value("${multiplication.anything.routing-key}") final String routingKey) { 34 | return BindingBuilder.bind(queue).to(exchange).with(routingKey); 35 | } 36 | 37 | @Bean 38 | public MappingJackson2MessageConverter consumerJackson2MessageConverter() { 39 | return new MappingJackson2MessageConverter(); 40 | } 41 | 42 | @Bean 43 | public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() { 44 | DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); 45 | factory.setMessageConverter(consumerJackson2MessageConverter()); 46 | return factory; 47 | } 48 | 49 | @Override 50 | public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) { 51 | registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/configuration/RestClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.configuration; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | /** 9 | * Configures the REST client in our application 10 | */ 11 | @Configuration 12 | public class RestClientConfiguration { 13 | 14 | @Bean 15 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 16 | return builder.build(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/controller/LeaderBoardController.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.controller; 2 | 3 | import microservices.book.gamification.domain.LeaderBoardRow; 4 | import microservices.book.gamification.service.LeaderBoardService; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * This class implements a REST API for the Gamification LeaderBoard service. 13 | */ 14 | @RestController 15 | @RequestMapping("/leaders") 16 | class LeaderBoardController { 17 | 18 | private final LeaderBoardService leaderBoardService; 19 | 20 | public LeaderBoardController(final LeaderBoardService leaderBoardService) { 21 | this.leaderBoardService = leaderBoardService; 22 | } 23 | 24 | @GetMapping 25 | public List getLeaderBoard() { 26 | return leaderBoardService.getCurrentLeaderBoard(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/controller/UserStatsController.java: -------------------------------------------------------------------------------- 1 | 2 | package microservices.book.gamification.controller; 3 | 4 | import microservices.book.gamification.domain.GameStats; 5 | import microservices.book.gamification.service.GameService; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | /** 12 | * This class implements a REST API for the Gamification User Statistics service. 13 | */ 14 | @RestController 15 | @RequestMapping("/stats") 16 | class UserStatsController { 17 | 18 | private final GameService gameService; 19 | 20 | public UserStatsController(final GameService gameService) { 21 | this.gameService = gameService; 22 | } 23 | 24 | @GetMapping 25 | public GameStats getStatsForUser(@RequestParam("userId") final Long userId) { 26 | return gameService.retrieveStatsForUser(userId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/domain/Badge.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.domain; 2 | 3 | /** 4 | * Enumeration with the different types of Badges that a user can win. 5 | */ 6 | public enum Badge { 7 | 8 | // Badges depending on score 9 | BRONZE_MULTIPLICATOR, 10 | SILVER_MULTIPLICATOR, 11 | GOLD_MULTIPLICATOR, 12 | 13 | // Other badges won for different conditions 14 | FIRST_ATTEMPT, 15 | FIRST_WON, 16 | LUCKY_NUMBER 17 | 18 | } 19 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/domain/BadgeCard.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | /** 14 | * This class links a Badge to a User. Contains also a timestamp with the moment in which the user got it. 15 | */ 16 | @RequiredArgsConstructor 17 | @Getter 18 | @ToString 19 | @EqualsAndHashCode 20 | @Entity 21 | public final class BadgeCard { 22 | 23 | @Id 24 | @GeneratedValue 25 | @Column(name = "BADGE_ID") 26 | private final Long badgeId; 27 | 28 | private final Long userId; 29 | private final long badgeTimestamp; 30 | private final Badge badge; 31 | 32 | // Empty constructor for JSON / JPA 33 | public BadgeCard() { 34 | this(null, null, 0, null); 35 | } 36 | 37 | public BadgeCard(final Long userId, final Badge badge) { 38 | this(null, userId, System.currentTimeMillis(), badge); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/domain/GameStats.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * This object contains the result of one or many iterations of the game. 14 | * It may contain any combination of {@link ScoreCard} objects and {@link BadgeCard} objects. 15 | * 16 | * It can be used as a delta (as a single game iteration) or to represent the total amount of score / badges. 17 | */ 18 | @RequiredArgsConstructor 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | public final class GameStats { 23 | 24 | private final Long userId; 25 | private final int score; 26 | private final List badges; 27 | 28 | // Empty constructor for JSON / JPA 29 | public GameStats() { 30 | this.userId = 0L; 31 | this.score = 0; 32 | this.badges = new ArrayList<>(); 33 | } 34 | 35 | /** 36 | * Factory method to build an empty instance (zero points and no badges) 37 | * @param userId the user's id 38 | * @return a {@link GameStats} object with zero score and no badges 39 | */ 40 | public static GameStats emptyStats(final Long userId) { 41 | return new GameStats(userId, 0, Collections.emptyList()); 42 | } 43 | 44 | /** 45 | * @return an unmodifiable view of the badge cards list 46 | */ 47 | public List getBadges() { 48 | return Collections.unmodifiableList(badges); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/domain/LeaderBoardRow.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | /** 9 | * Represents a line in our Leaderboard: it links a user to a total score. 10 | */ 11 | @RequiredArgsConstructor 12 | @Getter 13 | @ToString 14 | @EqualsAndHashCode 15 | public final class LeaderBoardRow { 16 | 17 | private final Long userId; 18 | private final Long totalScore; 19 | 20 | // Empty constructor for JSON / JPA 21 | public LeaderBoardRow() { 22 | this(0L, 0L); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/domain/ScoreCard.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | /** 14 | * This class represents the Score linked to an attempt in the game, 15 | * with an associated user and the timestamp in which the score 16 | * is registered. 17 | */ 18 | @RequiredArgsConstructor 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | @Entity 23 | public final class ScoreCard { 24 | 25 | // The default score assigned to this card, if not specified. 26 | public static final int DEFAULT_SCORE = 10; 27 | 28 | @Id 29 | @GeneratedValue 30 | @Column(name = "CARD_ID") 31 | private final Long cardId; 32 | 33 | @Column(name = "USER_ID") 34 | private final Long userId; 35 | 36 | @Column(name = "ATTEMPT_ID") 37 | private final Long attemptId; 38 | 39 | @Column(name = "SCORE_TS") 40 | private final long scoreTimestamp; 41 | 42 | @Column(name = "SCORE") 43 | private final int score; 44 | 45 | // Empty constructor for JSON / JPA 46 | public ScoreCard() { 47 | this(null, null, null, 0, 0); 48 | } 49 | 50 | public ScoreCard(final Long userId, final Long attemptId) { 51 | this(null, userId, attemptId, System.currentTimeMillis(), DEFAULT_SCORE); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/event/EventHandler.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.event; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import microservices.book.gamification.service.GameService; 5 | import org.springframework.amqp.AmqpRejectAndDontRequeueException; 6 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * This class receives the events and triggers the associated 11 | * business logic. 12 | */ 13 | @Slf4j 14 | @Component 15 | class EventHandler { 16 | 17 | private GameService gameService; 18 | 19 | EventHandler(final GameService gameService) { 20 | this.gameService = gameService; 21 | } 22 | 23 | @RabbitListener(queues = "${multiplication.queue}") 24 | void handleMultiplicationSolved(final MultiplicationSolvedEvent event) { 25 | log.info("Multiplication Solved Event received: {}", event.getMultiplicationResultAttemptId()); 26 | try { 27 | gameService.newAttemptForUser(event.getUserId(), 28 | event.getMultiplicationResultAttemptId(), 29 | event.isCorrect()); 30 | } catch (final Exception e) { 31 | log.error("Error when trying to process MultiplicationSolvedEvent", e); 32 | // Avoids the event to be re-queued and reprocessed. 33 | throw new AmqpRejectAndDontRequeueException(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/event/MultiplicationSolvedEvent.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.event; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * Event received when a multiplication has been solved in the system. 12 | * Provides some context information about the multiplication. 13 | */ 14 | @RequiredArgsConstructor 15 | @Getter 16 | @ToString 17 | @EqualsAndHashCode 18 | class MultiplicationSolvedEvent implements Serializable { 19 | 20 | private final Long multiplicationResultAttemptId; 21 | private final Long userId; 22 | private final boolean correct; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/repository/BadgeCardRepository.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.repository; 2 | 3 | import microservices.book.gamification.domain.BadgeCard; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Handles data operations with BadgeCards 10 | */ 11 | public interface BadgeCardRepository extends CrudRepository { 12 | 13 | /** 14 | * Retrieves all BadgeCards for a given user. 15 | * @param userId the id of the user to look for BadgeCards 16 | * @return the list of BadgeCards, sorted by most recent. 17 | */ 18 | List findByUserIdOrderByBadgeTimestampDesc(final Long userId); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/repository/ScoreCardRepository.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.repository; 2 | 3 | import microservices.book.gamification.domain.LeaderBoardRow; 4 | import microservices.book.gamification.domain.ScoreCard; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | import org.springframework.data.repository.query.Param; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Handles CRUD operations with ScoreCards 13 | */ 14 | public interface ScoreCardRepository extends CrudRepository { 15 | 16 | /** 17 | * Gets the total score for a given user, being the sum of the scores of all his ScoreCards. 18 | * @param userId the id of the user for which the total score should be retrieved 19 | * @return the total score for the given user 20 | */ 21 | @Query("SELECT SUM(s.score) FROM microservices.book.gamification.domain.ScoreCard s WHERE s.userId = :userId GROUP BY s.userId") 22 | int getTotalScoreForUser(@Param("userId") final Long userId); 23 | 24 | /** 25 | * Retrieves a list of {@link LeaderBoardRow}s representing the Leader Board of users and their total score. 26 | * @return the leader board, sorted by highest score first. 27 | */ 28 | @Query("SELECT NEW microservices.book.gamification.domain.LeaderBoardRow(s.userId, SUM(s.score)) " + 29 | "FROM microservices.book.gamification.domain.ScoreCard s " + 30 | "GROUP BY s.userId ORDER BY SUM(s.score) DESC") 31 | List findFirst10(); 32 | 33 | /** 34 | * Retrieves all the ScoreCards for a given user, identified by his user id. 35 | * @param userId the id of the user 36 | * @return a list containing all the ScoreCards for the given user, sorted by most recent. 37 | */ 38 | List findByUserIdOrderByScoreTimestampDesc(final Long userId); 39 | } 40 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/service/GameService.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import microservices.book.gamification.domain.GameStats; 4 | 5 | /** 6 | * This service includes the main logic for gamifying the system. 7 | */ 8 | public interface GameService { 9 | 10 | /** 11 | * Process a new attempt from a given user. 12 | * 13 | * @param userId the user's unique id 14 | * @param attemptId the attempt id, can be used to retrieve extra data if needed 15 | * @param correct indicates if the attempt was correct 16 | * 17 | * @return a {@link GameStats} object containing the new score and badge cards obtained 18 | */ 19 | GameStats newAttemptForUser(Long userId, Long attemptId, boolean correct); 20 | 21 | /** 22 | * Gets the game statistics for a given user 23 | * @param userId the user 24 | * @return the total statistics for that user 25 | */ 26 | GameStats retrieveStatsForUser(Long userId); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/service/GameServiceImpl.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import microservices.book.gamification.client.MultiplicationResultAttemptClient; 5 | import microservices.book.gamification.client.dto.MultiplicationResultAttempt; 6 | import microservices.book.gamification.domain.*; 7 | import microservices.book.gamification.repository.BadgeCardRepository; 8 | import microservices.book.gamification.repository.ScoreCardRepository; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * @author moises.macero 18 | */ 19 | @Service 20 | @Slf4j 21 | class GameServiceImpl implements GameService { 22 | 23 | public static final int LUCKY_NUMBER = 42; 24 | 25 | private ScoreCardRepository scoreCardRepository; 26 | private BadgeCardRepository badgeCardRepository; 27 | private MultiplicationResultAttemptClient attemptClient; 28 | 29 | GameServiceImpl(ScoreCardRepository scoreCardRepository, 30 | BadgeCardRepository badgeCardRepository, 31 | MultiplicationResultAttemptClient attemptClient) { 32 | this.scoreCardRepository = scoreCardRepository; 33 | this.badgeCardRepository = badgeCardRepository; 34 | this.attemptClient = attemptClient; 35 | } 36 | 37 | @Override 38 | public GameStats newAttemptForUser(final Long userId, 39 | final Long attemptId, 40 | final boolean correct) { 41 | // For the first version we'll give points only if it's correct 42 | if(correct) { 43 | ScoreCard scoreCard = new ScoreCard(userId, attemptId); 44 | scoreCardRepository.save(scoreCard); 45 | log.info("User with id {} scored {} points for attempt id {}", 46 | userId, scoreCard.getScore(), attemptId); 47 | List badgeCards = processForBadges(userId, attemptId); 48 | return new GameStats(userId, scoreCard.getScore(), 49 | badgeCards.stream().map(BadgeCard::getBadge) 50 | .collect(Collectors.toList())); 51 | } 52 | return GameStats.emptyStats(userId); 53 | } 54 | 55 | /** 56 | * Checks the total score and the different score cards obtained 57 | * to give new badges in case their conditions are met. 58 | */ 59 | private List processForBadges(final Long userId, 60 | final Long attemptId) { 61 | List badgeCards = new ArrayList<>(); 62 | 63 | int totalScore = scoreCardRepository.getTotalScoreForUser(userId); 64 | log.info("New score for user {} is {}", userId, totalScore); 65 | 66 | List scoreCardList = scoreCardRepository 67 | .findByUserIdOrderByScoreTimestampDesc(userId); 68 | List badgeCardList = badgeCardRepository 69 | .findByUserIdOrderByBadgeTimestampDesc(userId); 70 | 71 | // Badges depending on score 72 | checkAndGiveBadgeBasedOnScore(badgeCardList, 73 | Badge.BRONZE_MULTIPLICATOR, totalScore, 100, userId) 74 | .ifPresent(badgeCards::add); 75 | checkAndGiveBadgeBasedOnScore(badgeCardList, 76 | Badge.SILVER_MULTIPLICATOR, totalScore, 500, userId) 77 | .ifPresent(badgeCards::add); 78 | checkAndGiveBadgeBasedOnScore(badgeCardList, 79 | Badge.GOLD_MULTIPLICATOR, totalScore, 999, userId) 80 | .ifPresent(badgeCards::add); 81 | 82 | // First won badge 83 | if(scoreCardList.size() == 1 && 84 | !containsBadge(badgeCardList, Badge.FIRST_WON)) { 85 | BadgeCard firstWonBadge = giveBadgeToUser(Badge.FIRST_WON, userId); 86 | badgeCards.add(firstWonBadge); 87 | } 88 | 89 | // Lucky number badge 90 | MultiplicationResultAttempt attempt = attemptClient 91 | .retrieveMultiplicationResultAttemptbyId(attemptId); 92 | if(!containsBadge(badgeCardList, Badge.LUCKY_NUMBER) && 93 | (LUCKY_NUMBER == attempt.getMultiplicationFactorA() || 94 | LUCKY_NUMBER == attempt.getMultiplicationFactorB())) { 95 | BadgeCard luckyNumberBadge = giveBadgeToUser( 96 | Badge.LUCKY_NUMBER, userId); 97 | badgeCards.add(luckyNumberBadge); 98 | } 99 | 100 | return badgeCards; 101 | } 102 | 103 | @Override 104 | public GameStats retrieveStatsForUser(final Long userId) { 105 | int score = scoreCardRepository.getTotalScoreForUser(userId); 106 | List badgeCards = badgeCardRepository 107 | .findByUserIdOrderByBadgeTimestampDesc(userId); 108 | return new GameStats(userId, score, badgeCards.stream() 109 | .map(BadgeCard::getBadge).collect(Collectors.toList())); 110 | } 111 | 112 | /** 113 | * Convenience method to check the current score against 114 | * the different thresholds to gain badges. 115 | * It also assigns badge to user if the conditions are met. 116 | */ 117 | private Optional checkAndGiveBadgeBasedOnScore( 118 | final List badgeCards, final Badge badge, 119 | final int score, final int scoreThreshold, final Long userId) { 120 | if(score >= scoreThreshold && !containsBadge(badgeCards, badge)) { 121 | return Optional.of(giveBadgeToUser(badge, userId)); 122 | } 123 | return Optional.empty(); 124 | } 125 | 126 | /** 127 | * Checks if the passed list of badges includes the one being checked 128 | */ 129 | private boolean containsBadge(final List badgeCards, 130 | final Badge badge) { 131 | return badgeCards.stream().anyMatch(b -> b.getBadge().equals(badge)); 132 | } 133 | 134 | /** 135 | * Assigns a new badge to the given user 136 | */ 137 | private BadgeCard giveBadgeToUser(final Badge badge, final Long userId) { 138 | BadgeCard badgeCard = new BadgeCard(userId, badge); 139 | badgeCardRepository.save(badgeCard); 140 | log.info("User with id {} won a new badge: {}", userId, badge); 141 | return badgeCard; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/service/LeaderBoardService.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import microservices.book.gamification.domain.LeaderBoardRow; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Provides methods to access the LeaderBoard with users and scores. 9 | */ 10 | public interface LeaderBoardService { 11 | 12 | /** 13 | * Retrieves the current leader board with the top score users 14 | * @return the users with the highest score 15 | */ 16 | List getCurrentLeaderBoard(); 17 | } 18 | -------------------------------------------------------------------------------- /gamification/src/main/java/microservices/book/gamification/service/LeaderBoardServiceImpl.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import microservices.book.gamification.domain.LeaderBoardRow; 4 | import microservices.book.gamification.repository.ScoreCardRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | 9 | @Service 10 | class LeaderBoardServiceImpl implements LeaderBoardService { 11 | 12 | private ScoreCardRepository scoreCardRepository; 13 | 14 | LeaderBoardServiceImpl(ScoreCardRepository scoreCardRepository) { 15 | this.scoreCardRepository = scoreCardRepository; 16 | } 17 | 18 | @Override 19 | public List getCurrentLeaderBoard() { 20 | return scoreCardRepository.findFirst10(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gamification/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 2 | 3 | # Gives us access to the H2 database web console 4 | spring.h2.console.enabled=true 5 | # Generates the database *only* if it's not there yet 6 | spring.jpa.hibernate.ddl-auto=update 7 | # Creates the database in a file 8 | spring.datasource.url=jdbc:h2:file:~/gamification;DB_CLOSE_ON_EXIT=FALSE; 9 | # For educational purposes we will show the SQL in console 10 | spring.jpa.properties.hibernate.show_sql=true 11 | 12 | ## RabbitMQ configuration 13 | multiplication.exchange=multiplication_exchange 14 | multiplication.solved.key=multiplication.solved 15 | multiplication.queue=gamification_multiplication_queue 16 | multiplication.anything.routing-key=multiplication.* 17 | 18 | # REST client settings 19 | multiplicationHost=http://localhost:8080 -------------------------------------------------------------------------------- /gamification/src/test/java/microservices/book/gamification/controller/LeaderBoardControllerTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import microservices.book.gamification.domain.LeaderBoardRow; 5 | import microservices.book.gamification.service.LeaderBoardService; 6 | import org.junit.Before; 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.WebMvcTest; 11 | import org.springframework.boot.test.json.JacksonTester; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 26 | 27 | /** 28 | * @author moises.macero 29 | */ 30 | @RunWith(SpringRunner.class) 31 | @WebMvcTest(LeaderBoardController.class) 32 | public class LeaderBoardControllerTest { 33 | 34 | @MockBean 35 | private LeaderBoardService leaderBoardService; 36 | 37 | @Autowired 38 | private MockMvc mvc; 39 | 40 | private JacksonTester> json; 41 | 42 | @Before 43 | public void setup() { 44 | JacksonTester.initFields(this, new ObjectMapper()); 45 | } 46 | 47 | @Test 48 | public void getLeaderBoardTest() throws Exception{ 49 | // given 50 | LeaderBoardRow leaderBoardRow1 = new LeaderBoardRow(1L, 500L); 51 | LeaderBoardRow leaderBoardRow2 = new LeaderBoardRow(2L, 400L); 52 | List leaderBoard = new ArrayList<>(); 53 | Collections.addAll(leaderBoard, leaderBoardRow1, leaderBoardRow2); 54 | given(leaderBoardService.getCurrentLeaderBoard()).willReturn(leaderBoard); 55 | 56 | // when 57 | MockHttpServletResponse response = mvc.perform( 58 | get("/leaders") 59 | .accept(MediaType.APPLICATION_JSON)) 60 | .andReturn().getResponse(); 61 | 62 | // then 63 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 64 | assertThat(response.getContentAsString()) 65 | .isEqualTo(json.write(leaderBoard).getJson()); 66 | } 67 | } -------------------------------------------------------------------------------- /gamification/src/test/java/microservices/book/gamification/controller/UserStatsControllerTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import microservices.book.gamification.domain.Badge; 5 | import microservices.book.gamification.domain.GameStats; 6 | import microservices.book.gamification.service.GameService; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 12 | import org.springframework.boot.test.json.JacksonTester; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.mock.web.MockHttpServletResponse; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | 20 | import java.util.Collections; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.mockito.BDDMockito.given; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 25 | 26 | /** 27 | * @author moises.macero 28 | */ 29 | @RunWith(SpringRunner.class) 30 | @WebMvcTest(UserStatsController.class) 31 | public class UserStatsControllerTest { 32 | 33 | @MockBean 34 | private GameService gameService; 35 | 36 | @Autowired 37 | private MockMvc mvc; 38 | 39 | private JacksonTester json; 40 | 41 | @Before 42 | public void setup() { 43 | JacksonTester.initFields(this, new ObjectMapper()); 44 | } 45 | 46 | @Test 47 | public void getUserStatsTest() throws Exception{ 48 | // given 49 | GameStats gameStats = new GameStats(1L, 2000, Collections.singletonList(Badge.GOLD_MULTIPLICATOR)); 50 | given(gameService.retrieveStatsForUser(1L)).willReturn(gameStats); 51 | 52 | // when 53 | MockHttpServletResponse response = mvc.perform( 54 | get("/stats?userId=1") 55 | .accept(MediaType.APPLICATION_JSON)) 56 | .andReturn().getResponse(); 57 | 58 | // then 59 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 60 | assertThat(response.getContentAsString()) 61 | .isEqualTo(json.write(gameStats).getJson()); 62 | } 63 | } -------------------------------------------------------------------------------- /gamification/src/test/java/microservices/book/gamification/event/EventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.event; 2 | 3 | import microservices.book.gamification.domain.GameStats; 4 | import microservices.book.gamification.service.GameService; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | 10 | import static org.mockito.BDDMockito.given; 11 | import static org.mockito.Mockito.verify; 12 | 13 | /** 14 | * @author moises.macero 15 | */ 16 | public class EventHandlerTest { 17 | 18 | private EventHandler eventHandler; 19 | 20 | @Mock 21 | private GameService gameService; 22 | 23 | @Before 24 | public void setUp() { 25 | MockitoAnnotations.initMocks(this); 26 | eventHandler = new EventHandler(gameService); 27 | } 28 | 29 | @Test 30 | public void multiplicationAttemptReceivedTest() { 31 | // given 32 | Long userId = 1L; 33 | Long attemptId = 8L; 34 | boolean correct = true; 35 | GameStats gameStatsExpected = new GameStats(); 36 | MultiplicationSolvedEvent event = new MultiplicationSolvedEvent(attemptId, userId, correct); 37 | given(gameService.newAttemptForUser(userId, attemptId, correct)).willReturn(gameStatsExpected); 38 | 39 | // when 40 | eventHandler.handleMultiplicationSolved(event); 41 | 42 | // then 43 | verify(gameService).newAttemptForUser(userId, attemptId, correct); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /gamification/src/test/java/microservices/book/gamification/service/GameServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import microservices.book.gamification.client.MultiplicationResultAttemptClient; 4 | import microservices.book.gamification.client.dto.MultiplicationResultAttempt; 5 | import microservices.book.gamification.domain.*; 6 | import microservices.book.gamification.repository.BadgeCardRepository; 7 | import microservices.book.gamification.repository.ScoreCardRepository; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.BDDMockito.given; 20 | import static org.mockito.Matchers.anyLong; 21 | 22 | /** 23 | * @author moises.macero 24 | */ 25 | public class GameServiceImplTest { 26 | 27 | private GameServiceImpl gameService; 28 | 29 | @Mock 30 | private ScoreCardRepository scoreCardRepository; 31 | 32 | @Mock 33 | private BadgeCardRepository badgeCardRepository; 34 | 35 | @Mock 36 | private MultiplicationResultAttemptClient multiplicationClient; 37 | 38 | @Before 39 | public void setUp() { 40 | // With this call to initMocks we tell Mockito to process the annotations 41 | MockitoAnnotations.initMocks(this); 42 | gameService = new GameServiceImpl(scoreCardRepository, badgeCardRepository, multiplicationClient); 43 | 44 | // Common given - attempt does not contain a lucky number by default 45 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 46 | "john_doe", 20, 70, 1400, true); 47 | given(multiplicationClient.retrieveMultiplicationResultAttemptbyId(anyLong())) 48 | .willReturn(attempt); 49 | } 50 | 51 | @Test 52 | public void processFirstCorrectAttemptTest() { 53 | // given 54 | Long userId = 1L; 55 | Long attemptId = 8L; 56 | int totalScore = 10; 57 | ScoreCard scoreCard = new ScoreCard(userId, attemptId); 58 | given(scoreCardRepository.getTotalScoreForUser(userId)) 59 | .willReturn(totalScore); 60 | // this repository will return the just-won score card 61 | given(scoreCardRepository.findByUserIdOrderByScoreTimestampDesc(userId)) 62 | .willReturn(Collections.singletonList(scoreCard)); 63 | given(badgeCardRepository.findByUserIdOrderByBadgeTimestampDesc(userId)) 64 | .willReturn(Collections.emptyList()); 65 | 66 | 67 | // when 68 | GameStats iteration = gameService.newAttemptForUser(userId, attemptId, true); 69 | 70 | // assert - should score one card and win the badge FIRST_WON 71 | assertThat(iteration.getScore()).isEqualTo(scoreCard.getScore()); 72 | assertThat(iteration.getBadges()).containsOnly(Badge.FIRST_WON); 73 | } 74 | 75 | @Test 76 | public void processCorrectAttemptForScoreBadgeTest() { 77 | // given 78 | Long userId = 1L; 79 | Long attemptId = 29L; 80 | int totalScore = 100; 81 | BadgeCard firstWonBadge = new BadgeCard(userId, Badge.FIRST_WON); 82 | given(scoreCardRepository.getTotalScoreForUser(userId)) 83 | .willReturn(totalScore); 84 | // this repository will return the just-won score card 85 | given(scoreCardRepository.findByUserIdOrderByScoreTimestampDesc(userId)) 86 | .willReturn(createNScoreCards(10, userId)); 87 | // the first won badge is already there 88 | given(badgeCardRepository.findByUserIdOrderByBadgeTimestampDesc(userId)) 89 | .willReturn(Collections.singletonList(firstWonBadge)); 90 | 91 | 92 | // when 93 | GameStats iteration = gameService.newAttemptForUser(userId, attemptId, true); 94 | 95 | // assert - should score one card and win the badge BRONZE 96 | assertThat(iteration.getScore()).isEqualTo(ScoreCard.DEFAULT_SCORE); 97 | assertThat(iteration.getBadges()).containsOnly(Badge.BRONZE_MULTIPLICATOR); 98 | } 99 | 100 | @Test 101 | public void processCorrectAttemptForLuckyNumberBadgeTest() { 102 | // given 103 | Long userId = 1L; 104 | Long attemptId = 29L; 105 | int totalScore = 10; 106 | BadgeCard firstWonBadge = new BadgeCard(userId, Badge.FIRST_WON); 107 | given(scoreCardRepository.getTotalScoreForUser(userId)) 108 | .willReturn(totalScore); 109 | // this repository will return the just-won score card 110 | given(scoreCardRepository.findByUserIdOrderByScoreTimestampDesc(userId)) 111 | .willReturn(createNScoreCards(1, userId)); 112 | // the first won badge is already there 113 | given(badgeCardRepository.findByUserIdOrderByBadgeTimestampDesc(userId)) 114 | .willReturn(Collections.singletonList(firstWonBadge)); 115 | // the attempt includes the lucky number 116 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 117 | "john_doe", 42, 10, 420, true); 118 | given(multiplicationClient.retrieveMultiplicationResultAttemptbyId(attemptId)) 119 | .willReturn(attempt); 120 | 121 | // when 122 | GameStats iteration = gameService.newAttemptForUser(userId, attemptId, true); 123 | 124 | // assert - should score one card and win the badge LUCKY NUMBER 125 | assertThat(iteration.getScore()).isEqualTo(ScoreCard.DEFAULT_SCORE); 126 | assertThat(iteration.getBadges()).containsOnly(Badge.LUCKY_NUMBER); 127 | } 128 | 129 | @Test 130 | public void processWrongAttemptTest() { 131 | // given 132 | Long userId = 1L; 133 | Long attemptId = 8L; 134 | int totalScore = 10; 135 | ScoreCard scoreCard = new ScoreCard(userId, attemptId); 136 | given(scoreCardRepository.getTotalScoreForUser(userId)) 137 | .willReturn(totalScore); 138 | // this repository will return the just-won score card 139 | given(scoreCardRepository.findByUserIdOrderByScoreTimestampDesc(userId)) 140 | .willReturn(Collections.singletonList(scoreCard)); 141 | given(badgeCardRepository.findByUserIdOrderByBadgeTimestampDesc(userId)) 142 | .willReturn(Collections.emptyList()); 143 | 144 | 145 | // when 146 | GameStats iteration = gameService.newAttemptForUser(userId, attemptId, false); 147 | 148 | // assert - shouldn't score anything 149 | assertThat(iteration.getScore()).isEqualTo(0); 150 | assertThat(iteration.getBadges()).isEmpty(); 151 | } 152 | 153 | @Test 154 | public void retrieveStatsForUserTest() { 155 | // given 156 | Long userId = 1L; 157 | int totalScore = 1000; 158 | BadgeCard badgeCard = new BadgeCard(userId, Badge.SILVER_MULTIPLICATOR); 159 | given(scoreCardRepository.getTotalScoreForUser(userId)) 160 | .willReturn(totalScore); 161 | given(badgeCardRepository.findByUserIdOrderByBadgeTimestampDesc(userId)) 162 | .willReturn(Collections.singletonList(badgeCard)); 163 | 164 | // when 165 | GameStats stats = gameService.retrieveStatsForUser(userId); 166 | 167 | // assert - should score one card and win the badge FIRST_WON 168 | assertThat(stats.getScore()).isEqualTo(totalScore); 169 | assertThat(stats.getBadges()).containsOnly(Badge.SILVER_MULTIPLICATOR); 170 | } 171 | 172 | private List createNScoreCards(int n, Long userId) { 173 | return IntStream.range(0, n) 174 | .mapToObj(i -> new ScoreCard(userId, (long)i)) 175 | .collect(Collectors.toList()); 176 | } 177 | } -------------------------------------------------------------------------------- /gamification/src/test/java/microservices/book/gamification/service/LeaderBoardServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.gamification.service; 2 | 3 | import microservices.book.gamification.domain.LeaderBoardRow; 4 | import microservices.book.gamification.repository.ScoreCardRepository; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.mockito.BDDMockito.given; 15 | 16 | /** 17 | * @author moises.macero 18 | */ 19 | public class LeaderBoardServiceImplTest { 20 | 21 | private LeaderBoardServiceImpl leaderBoardService; 22 | 23 | @Mock 24 | private ScoreCardRepository scoreCardRepository; 25 | 26 | @Before 27 | public void setUp() { 28 | MockitoAnnotations.initMocks(this); 29 | leaderBoardService = new LeaderBoardServiceImpl(scoreCardRepository); 30 | } 31 | 32 | @Test 33 | public void retrieveLeaderBoardTest() { 34 | // given 35 | Long userId = 1L; 36 | LeaderBoardRow leaderRow1 = new LeaderBoardRow(userId, 300L); 37 | List expectedLeaderBoard = Collections.singletonList(leaderRow1); 38 | given(scoreCardRepository.findFirst10()).willReturn(expectedLeaderBoard); 39 | 40 | // when 41 | List leaderBoard = leaderBoardService.getCurrentLeaderBoard(); 42 | 43 | // then 44 | assertThat(leaderBoard).isEqualTo(expectedLeaderBoard); 45 | } 46 | } -------------------------------------------------------------------------------- /social-multiplication/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /social-multiplication/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microservices-practical/microservices-v5/b5d42cf7be3ae03fceca8cb62b6e530f05a38a3c/social-multiplication/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /social-multiplication/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip 2 | -------------------------------------------------------------------------------- /social-multiplication/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 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /social-multiplication/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 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %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% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /social-multiplication/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | microservices.book 7 | social-multiplication-v5 8 | 0.5.0-SNAPSHOT 9 | jar 10 | 11 | social-multiplication-v5 12 | Social Multiplication App (Learn Microservices with Spring Boot) 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.7.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-amqp 36 | 37 | 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | 1.16.18 43 | provided 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-jpa 49 | 50 | 51 | 52 | com.h2database 53 | h2 54 | runtime 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-maven-plugin 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /social-multiplication/social-multiplication/README.md: -------------------------------------------------------------------------------- 1 | # microservices-v5 2 | 3 | This project contains the version 5 of the application that is developed under the scope of the book *Learn Microservices with Spring Boot*. You can get a copy of the book on [Apress](http://www.apress.com/gp/book/9781484231647). 4 | 5 | ## About this version 6 | 7 | From this version onwards we have a new microservice: **Gamification**. Our **Multiplication** microservice 8 | is now using Spring AMQP and RabbitMQ to send an event when we receive an attempt. 9 | 10 | On the other hand we have a new project, located in the `gamification` folder. This microservice subscribes to 11 | the event sent by Multiplication, and react to it assigning points - and potentially badges, to our users. 12 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/SocialMultiplicationApplication.java: -------------------------------------------------------------------------------- 1 | package microservices.book; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SocialMultiplicationApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SocialMultiplicationApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/configuration/RabbitMQConfiguration.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.configuration; 2 | 3 | import org.springframework.amqp.core.TopicExchange; 4 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 5 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 6 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * Configures RabbitMQ to use events in our application. 13 | */ 14 | @Configuration 15 | public class RabbitMQConfiguration { 16 | 17 | @Bean 18 | public TopicExchange multiplicationExchange(@Value("${multiplication.exchange}") final String exchangeName) { 19 | return new TopicExchange(exchangeName); 20 | } 21 | 22 | @Bean 23 | public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) { 24 | final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); 25 | rabbitTemplate.setMessageConverter(producerJackson2MessageConverter()); 26 | return rabbitTemplate; 27 | } 28 | 29 | @Bean 30 | public Jackson2JsonMessageConverter producerJackson2MessageConverter() { 31 | return new Jackson2JsonMessageConverter(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/controller/MultiplicationController.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.controller; 2 | 3 | import microservices.book.multiplication.domain.Multiplication; 4 | import microservices.book.multiplication.service.MultiplicationService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | /** 11 | * This class implements a REST API for our Multiplication application. 12 | */ 13 | @RestController 14 | @RequestMapping("/multiplications") 15 | final class MultiplicationController { 16 | 17 | private final MultiplicationService multiplicationService; 18 | 19 | @Autowired 20 | public MultiplicationController(final MultiplicationService multiplicationService) { 21 | this.multiplicationService = multiplicationService; 22 | } 23 | 24 | @GetMapping("/random") 25 | Multiplication getRandomMultiplication() { 26 | return multiplicationService.createRandomMultiplication(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/controller/MultiplicationResultAttemptController.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.controller; 2 | 3 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 4 | import microservices.book.multiplication.service.MultiplicationService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * This class provides a REST API to POST the attempts from users. 13 | */ 14 | @RestController 15 | @RequestMapping("/results") 16 | final class MultiplicationResultAttemptController { 17 | 18 | private final MultiplicationService multiplicationService; 19 | 20 | @Autowired 21 | MultiplicationResultAttemptController(final MultiplicationService multiplicationService) { 22 | this.multiplicationService = multiplicationService; 23 | } 24 | 25 | @PostMapping 26 | ResponseEntity postResult(@RequestBody MultiplicationResultAttempt multiplicationResultAttempt) { 27 | boolean isCorrect = multiplicationService.checkAttempt(multiplicationResultAttempt); 28 | MultiplicationResultAttempt attemptCopy = new MultiplicationResultAttempt( 29 | multiplicationResultAttempt.getUser(), 30 | multiplicationResultAttempt.getMultiplication(), 31 | multiplicationResultAttempt.getResultAttempt(), 32 | isCorrect 33 | ); 34 | return ResponseEntity.ok(attemptCopy); 35 | } 36 | 37 | @GetMapping 38 | ResponseEntity> getStatistics(@RequestParam("alias") String alias) { 39 | return ResponseEntity.ok( 40 | multiplicationService.getStatsForUser(alias) 41 | ); 42 | } 43 | 44 | @GetMapping("/{resultId}") 45 | ResponseEntity getResultById(final @PathVariable("resultId") Long resultId) { 46 | return ResponseEntity.ok( 47 | multiplicationService.getResultById(resultId) 48 | ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/domain/Multiplication.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | /** 14 | * This class represents a Multiplication (a * b). 15 | */ 16 | @RequiredArgsConstructor 17 | @Getter 18 | @ToString 19 | @EqualsAndHashCode 20 | @Entity 21 | public final class Multiplication { 22 | 23 | @Id 24 | @GeneratedValue 25 | @Column(name = "MULTIPLICATION_ID") 26 | private Long id; 27 | 28 | // Both factors 29 | private final int factorA; 30 | private final int factorB; 31 | 32 | // Empty constructor for JSON/JPA 33 | Multiplication() { 34 | this(0, 0); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/domain/MultiplicationResultAttempt.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.*; 9 | 10 | /** 11 | * Identifies the attempt from a {@link User} to solve a 12 | * {@link Multiplication}. 13 | */ 14 | @RequiredArgsConstructor 15 | @Getter 16 | @ToString 17 | @EqualsAndHashCode 18 | @Entity 19 | public final class MultiplicationResultAttempt { 20 | 21 | @Id 22 | @GeneratedValue 23 | private Long id; 24 | 25 | @ManyToOne(cascade = CascadeType.PERSIST) 26 | @JoinColumn(name = "USER_ID") 27 | private final User user; 28 | 29 | @ManyToOne(cascade = CascadeType.PERSIST) 30 | @JoinColumn(name = "MULTIPLICATION_ID") 31 | private final Multiplication multiplication; 32 | private final int resultAttempt; 33 | 34 | private final boolean correct; 35 | 36 | // Empty constructor for JSON/JPA 37 | MultiplicationResultAttempt() { 38 | user = null; 39 | multiplication = null; 40 | resultAttempt = -1; 41 | correct = false; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/domain/User.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | /** 14 | * Stores information to identify the user. 15 | */ 16 | @RequiredArgsConstructor 17 | @Getter 18 | @ToString 19 | @EqualsAndHashCode 20 | @Entity 21 | public final class User { 22 | 23 | @Id 24 | @GeneratedValue 25 | @Column(name = "USER_ID") 26 | private Long id; 27 | 28 | private final String alias; 29 | 30 | // Empty constructor for JSON/JPA 31 | protected User() { 32 | alias = null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/event/EventDispatcher.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.event; 2 | 3 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Handles the communication with the Event Bus. 10 | */ 11 | @Component 12 | public class EventDispatcher { 13 | 14 | private RabbitTemplate rabbitTemplate; 15 | 16 | // The exchange to use to send anything related to Multiplication 17 | private String multiplicationExchange; 18 | 19 | // The routing key to use to send this particular event 20 | private String multiplicationSolvedRoutingKey; 21 | 22 | @Autowired 23 | EventDispatcher(final RabbitTemplate rabbitTemplate, 24 | @Value("${multiplication.exchange}") final String multiplicationExchange, 25 | @Value("${multiplication.solved.key}") final String multiplicationSolvedRoutingKey) { 26 | this.rabbitTemplate = rabbitTemplate; 27 | this.multiplicationExchange = multiplicationExchange; 28 | this.multiplicationSolvedRoutingKey = multiplicationSolvedRoutingKey; 29 | } 30 | 31 | public void send(final MultiplicationSolvedEvent multiplicationSolvedEvent) { 32 | rabbitTemplate.convertAndSend( 33 | multiplicationExchange, 34 | multiplicationSolvedRoutingKey, 35 | multiplicationSolvedEvent); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/event/MultiplicationSolvedEvent.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.event; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * Event that models the fact that a {@link microservices.book.multiplication.domain.Multiplication} 12 | * has been solved in the system. Provides some context information about the multiplication. 13 | */ 14 | @RequiredArgsConstructor 15 | @Getter 16 | @ToString 17 | @EqualsAndHashCode 18 | public class MultiplicationSolvedEvent implements Serializable { 19 | 20 | private final Long multiplicationResultAttemptId; 21 | private final Long userId; 22 | private final boolean correct; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/repository/MultiplicationRepository.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.repository; 2 | 3 | import microservices.book.multiplication.domain.Multiplication; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | /** 7 | * This interface allows us to save and retrieve Multiplications 8 | */ 9 | public interface MultiplicationRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/repository/MultiplicationResultAttemptRepository.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.repository; 2 | 3 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * This interface allow us to store and retrieve attempts 10 | */ 11 | public interface MultiplicationResultAttemptRepository 12 | extends CrudRepository { 13 | 14 | /** 15 | * @return the latest 5 attempts for a given user, identified by their alias. 16 | */ 17 | List findTop5ByUserAliasOrderByIdDesc(String userAlias); 18 | } 19 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.repository; 2 | 3 | import microservices.book.multiplication.domain.User; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * This interface allows us to save and retrieve Users 10 | */ 11 | public interface UserRepository extends CrudRepository { 12 | 13 | Optional findByAlias(final String alias); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/service/MultiplicationService.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | import microservices.book.multiplication.domain.Multiplication; 4 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 5 | 6 | import java.util.List; 7 | 8 | public interface MultiplicationService { 9 | 10 | /** 11 | * Creates a Multiplication object with two randomly-generated factors 12 | * between 11 and 99. 13 | * 14 | * @return a Multiplication object with random factors 15 | */ 16 | Multiplication createRandomMultiplication(); 17 | 18 | /** 19 | * @return true if the attempt matches the result of the 20 | * multiplication, false otherwise. 21 | */ 22 | boolean checkAttempt(final MultiplicationResultAttempt resultAttempt); 23 | 24 | /** 25 | * Gets the statistics for a given user. 26 | * 27 | * @param userAlias the user's alias 28 | * @return a list of {@link MultiplicationResultAttempt} objects, being the past attempts of the user. 29 | */ 30 | List getStatsForUser(final String userAlias); 31 | 32 | /** 33 | * Gets an attempt by its id 34 | * 35 | * @param resultId the identifier of the attempt 36 | * @return the {@link MultiplicationResultAttempt} object matching the id, otherwise null. 37 | */ 38 | MultiplicationResultAttempt getResultById(final Long resultId); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/service/MultiplicationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | import microservices.book.multiplication.domain.Multiplication; 4 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 5 | import microservices.book.multiplication.domain.User; 6 | import microservices.book.multiplication.event.EventDispatcher; 7 | import microservices.book.multiplication.event.MultiplicationSolvedEvent; 8 | import microservices.book.multiplication.repository.MultiplicationResultAttemptRepository; 9 | import microservices.book.multiplication.repository.UserRepository; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.util.Assert; 13 | 14 | import javax.transaction.Transactional; 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | @Service 19 | class MultiplicationServiceImpl implements MultiplicationService { 20 | 21 | private RandomGeneratorService randomGeneratorService; 22 | private MultiplicationResultAttemptRepository attemptRepository; 23 | private UserRepository userRepository; 24 | private EventDispatcher eventDispatcher; 25 | 26 | @Autowired 27 | public MultiplicationServiceImpl(final RandomGeneratorService randomGeneratorService, 28 | final MultiplicationResultAttemptRepository attemptRepository, 29 | final UserRepository userRepository, 30 | final EventDispatcher eventDispatcher) { 31 | this.randomGeneratorService = randomGeneratorService; 32 | this.attemptRepository = attemptRepository; 33 | this.userRepository = userRepository; 34 | this.eventDispatcher = eventDispatcher; 35 | } 36 | 37 | @Override 38 | public Multiplication createRandomMultiplication() { 39 | int factorA = randomGeneratorService.generateRandomFactor(); 40 | int factorB = randomGeneratorService.generateRandomFactor(); 41 | return new Multiplication(factorA, factorB); 42 | } 43 | 44 | @Transactional 45 | @Override 46 | public boolean checkAttempt(final MultiplicationResultAttempt attempt) { 47 | // Check if the user already exists for that alias 48 | Optional user = userRepository.findByAlias(attempt.getUser().getAlias()); 49 | 50 | // Avoids 'hack' attempts 51 | Assert.isTrue(!attempt.isCorrect(), "You can't send an attempt marked as correct!!"); 52 | 53 | // Check if the attempt is correct 54 | boolean isCorrect = attempt.getResultAttempt() == 55 | attempt.getMultiplication().getFactorA() * 56 | attempt.getMultiplication().getFactorB(); 57 | 58 | MultiplicationResultAttempt checkedAttempt = new MultiplicationResultAttempt( 59 | user.orElse(attempt.getUser()), 60 | attempt.getMultiplication(), 61 | attempt.getResultAttempt(), 62 | isCorrect 63 | ); 64 | 65 | // Stores the attempt 66 | attemptRepository.save(checkedAttempt); 67 | 68 | // Communicates the result via Event 69 | eventDispatcher.send( 70 | new MultiplicationSolvedEvent(checkedAttempt.getId(), 71 | checkedAttempt.getUser().getId(), 72 | checkedAttempt.isCorrect()) 73 | ); 74 | 75 | return isCorrect; 76 | } 77 | 78 | @Override 79 | public List getStatsForUser(final String userAlias) { 80 | return attemptRepository.findTop5ByUserAliasOrderByIdDesc(userAlias); 81 | } 82 | 83 | @Override 84 | public MultiplicationResultAttempt getResultById(final Long resultId) { 85 | return attemptRepository.findOne(resultId); 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/service/RandomGeneratorService.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | public interface RandomGeneratorService { 4 | 5 | /** 6 | * @return a randomly-generated factor. It's always a number between 11 and 99. 7 | */ 8 | int generateRandomFactor(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /social-multiplication/src/main/java/microservices/book/multiplication/service/RandomGeneratorServiceImpl.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import java.util.Random; 5 | 6 | @Service 7 | final class RandomGeneratorServiceImpl implements RandomGeneratorService { 8 | 9 | final static int MINIMUM_FACTOR = 11; 10 | final static int MAXIMUM_FACTOR = 99; 11 | 12 | @Override 13 | public int generateRandomFactor() { 14 | return new Random().nextInt((MAXIMUM_FACTOR - MINIMUM_FACTOR) + 1) + MINIMUM_FACTOR; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /social-multiplication/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Gives us access to the H2 database web console 2 | spring.h2.console.enabled=true 3 | # Generates the database *only* if it's not there yet 4 | spring.jpa.hibernate.ddl-auto=update 5 | # Creates the database in a file 6 | spring.datasource.url=jdbc:h2:file:~/social-multiplication;DB_CLOSE_ON_EXIT=FALSE; 7 | # For educational purposes we will show the SQL in console 8 | spring.jpa.properties.hibernate.show_sql=true 9 | 10 | ## RabbitMQ configuration 11 | multiplication.exchange=multiplication_exchange 12 | multiplication.solved.key=multiplication.solved -------------------------------------------------------------------------------- /social-multiplication/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Multiplication v1 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Welcome to Social Multiplication

13 |

This is your challenge for today:

14 |

15 | x = 16 |

17 |

18 |

19 | Result?
20 | Your alias:
21 | 22 |
23 |

24 |

25 |

Stats

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
Attempt IDMultiplicationYou enteredCorrect?
35 |
36 | 37 | -------------------------------------------------------------------------------- /social-multiplication/src/main/resources/static/multiplication-client.js: -------------------------------------------------------------------------------- 1 | function updateMultiplication() { 2 | $.ajax({ 3 | url: "http://localhost:8080/multiplications/random" 4 | }).then(function(data) { 5 | // Cleans the form 6 | $("#attempt-form").find( "input[name='result-attempt']" ).val(""); 7 | $("#attempt-form").find( "input[name='user-alias']" ).val(""); 8 | // Gets a random challenge from API and loads the data in the HTML 9 | $('.multiplication-a').empty().append(data.factorA); 10 | $('.multiplication-b').empty().append(data.factorB); 11 | }); 12 | } 13 | 14 | function updateStats(alias) { 15 | $.ajax({ 16 | url: "http://localhost:8080/results?alias=" + alias, 17 | }).then(function(data) { 18 | $('#stats-body').empty(); 19 | data.forEach(function(row) { 20 | $('#stats-body').append('' + row.id + '' + 21 | '' + row.multiplication.factorA + ' x ' + row.multiplication.factorB + '' + 22 | '' + row.resultAttempt + '' + 23 | '' + (row.correct === true ? 'YES' : 'NO') + ''); 24 | }); 25 | }); 26 | } 27 | 28 | $(document).ready(function() { 29 | 30 | updateMultiplication(); 31 | 32 | $("#attempt-form").submit(function( event ) { 33 | 34 | // Don't submit the form normally 35 | event.preventDefault(); 36 | 37 | // Get some values from elements on the page 38 | var a = $('.multiplication-a').text(); 39 | var b = $('.multiplication-b').text(); 40 | var $form = $( this ), 41 | attempt = $form.find( "input[name='result-attempt']" ).val(), 42 | userAlias = $form.find( "input[name='user-alias']" ).val(); 43 | 44 | // Compose the data in the format that the API is expecting 45 | var data = { user: { alias: userAlias}, multiplication: {factorA: a, factorB: b}, resultAttempt: attempt}; 46 | 47 | // Send the data using post 48 | $.ajax({ 49 | url: '/results', 50 | type: 'POST', 51 | data: JSON.stringify(data), 52 | contentType: "application/json; charset=utf-8", 53 | dataType: "json", 54 | async: false, 55 | success: function(result){ 56 | if(result.correct) { 57 | $('.result-message').empty().append("The result is correct! Congratulations!"); 58 | } else { 59 | $('.result-message').empty().append("Ooops that's not correct! But keep trying!"); 60 | } 61 | } 62 | }); 63 | 64 | updateMultiplication(); 65 | 66 | updateStats(userAlias); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /social-multiplication/src/main/resources/static/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | html { 6 | display: table; 7 | margin: auto; 8 | } 9 | 10 | body { 11 | display: table-cell; 12 | vertical-align: middle; 13 | } -------------------------------------------------------------------------------- /social-multiplication/src/test/java/microservices/book/multiplication/controller/MultiplicationControllerTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import microservices.book.multiplication.domain.Multiplication; 5 | import microservices.book.multiplication.service.MultiplicationService; 6 | import org.junit.Before; 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.WebMvcTest; 11 | import org.springframework.boot.test.json.JacksonTester; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.mockito.BDDMockito.given; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | 23 | @RunWith(SpringRunner.class) 24 | @WebMvcTest(MultiplicationController.class) 25 | public class MultiplicationControllerTest { 26 | 27 | @MockBean 28 | private MultiplicationService multiplicationService; 29 | 30 | @Autowired 31 | private MockMvc mvc; 32 | 33 | // This object will be magically initialized by the initFields method below. 34 | private JacksonTester json; 35 | 36 | @Before 37 | public void setup() { 38 | JacksonTester.initFields(this, new ObjectMapper()); 39 | } 40 | 41 | @Test 42 | public void getRandomMultiplicationTest() throws Exception{ 43 | // given 44 | given(multiplicationService.createRandomMultiplication()) 45 | .willReturn(new Multiplication(70, 20)); 46 | 47 | // when 48 | MockHttpServletResponse response = mvc.perform( 49 | get("/multiplications/random") 50 | .accept(MediaType.APPLICATION_JSON)) 51 | .andReturn().getResponse(); 52 | 53 | // then 54 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 55 | assertThat(response.getContentAsString()) 56 | .isEqualTo(json.write(new Multiplication(70, 20)).getJson()); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /social-multiplication/src/test/java/microservices/book/multiplication/controller/MultiplicationResultAttemptControllerTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import microservices.book.multiplication.domain.Multiplication; 5 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 6 | import microservices.book.multiplication.domain.User; 7 | import microservices.book.multiplication.service.MultiplicationService; 8 | import org.assertj.core.util.Lists; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 14 | import org.springframework.boot.test.json.JacksonTester; 15 | import org.springframework.boot.test.mock.mockito.MockBean; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.mock.web.MockHttpServletResponse; 19 | import org.springframework.test.context.junit4.SpringRunner; 20 | import org.springframework.test.web.servlet.MockMvc; 21 | 22 | import java.util.List; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.mockito.BDDMockito.given; 26 | import static org.mockito.Matchers.any; 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 28 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 29 | 30 | @RunWith(SpringRunner.class) 31 | @WebMvcTest(MultiplicationResultAttemptController.class) 32 | public class MultiplicationResultAttemptControllerTest { 33 | 34 | @MockBean 35 | private MultiplicationService multiplicationService; 36 | 37 | @Autowired 38 | private MockMvc mvc; 39 | 40 | // These objects will be magically initialized by the initFields method below. 41 | private JacksonTester jsonResultAttempt; 42 | private JacksonTester> jsonResultAttemptList; 43 | 44 | @Before 45 | public void setup() { 46 | JacksonTester.initFields(this, new ObjectMapper()); 47 | } 48 | 49 | @Test 50 | public void postResultReturnCorrect() throws Exception { 51 | genericParameterizedTest(true); 52 | } 53 | 54 | @Test 55 | public void postResultReturnNotCorrect() throws Exception { 56 | genericParameterizedTest(false); 57 | } 58 | 59 | void genericParameterizedTest(final boolean correct) throws Exception { 60 | // given (remember we're not testing here the service itself) 61 | given(multiplicationService 62 | .checkAttempt(any(MultiplicationResultAttempt.class))) 63 | .willReturn(correct); 64 | User user = new User("john"); 65 | Multiplication multiplication = new Multiplication(50, 70); 66 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 67 | user, multiplication, 3500, correct); 68 | 69 | // when 70 | MockHttpServletResponse response = mvc.perform( 71 | post("/results").contentType(MediaType.APPLICATION_JSON) 72 | .content(jsonResultAttempt.write(attempt).getJson())) 73 | .andReturn().getResponse(); 74 | 75 | // then 76 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 77 | assertThat(response.getContentAsString()).isEqualTo( 78 | jsonResultAttempt.write( 79 | new MultiplicationResultAttempt(attempt.getUser(), 80 | attempt.getMultiplication(), 81 | attempt.getResultAttempt(), 82 | correct) 83 | ).getJson()); 84 | } 85 | 86 | @Test 87 | public void getUserStats() throws Exception { 88 | // given 89 | User user = new User("john_doe"); 90 | Multiplication multiplication = new Multiplication(50, 70); 91 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 92 | user, multiplication, 3500, true); 93 | List recentAttempts = Lists.newArrayList(attempt, attempt); 94 | given(multiplicationService 95 | .getStatsForUser("john_doe")) 96 | .willReturn(recentAttempts); 97 | 98 | // when 99 | MockHttpServletResponse response = mvc.perform( 100 | get("/results").param("alias", "john_doe")) 101 | .andReturn().getResponse(); 102 | 103 | // then 104 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 105 | assertThat(response.getContentAsString()).isEqualTo( 106 | jsonResultAttemptList.write( 107 | recentAttempts 108 | ).getJson()); 109 | } 110 | 111 | @Test 112 | public void getResultByIdTest() throws Exception { 113 | // given 114 | User user = new User("john_doe"); 115 | Multiplication multiplication = new Multiplication(50, 70); 116 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 117 | user, multiplication, 3500, true); 118 | given(multiplicationService.getResultById(4L)).willReturn(attempt); 119 | 120 | // when 121 | MockHttpServletResponse response = mvc.perform( 122 | get("/results/4")) 123 | .andReturn().getResponse(); 124 | 125 | // then 126 | assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); 127 | assertThat(response.getContentAsString()).isEqualTo( 128 | jsonResultAttempt.write(attempt).getJson()); 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /social-multiplication/src/test/java/microservices/book/multiplication/service/MultiplicationServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | import microservices.book.multiplication.domain.Multiplication; 4 | import microservices.book.multiplication.domain.MultiplicationResultAttempt; 5 | import microservices.book.multiplication.domain.User; 6 | import microservices.book.multiplication.event.EventDispatcher; 7 | import microservices.book.multiplication.event.MultiplicationSolvedEvent; 8 | import microservices.book.multiplication.repository.MultiplicationResultAttemptRepository; 9 | import microservices.book.multiplication.repository.UserRepository; 10 | import org.assertj.core.util.Lists; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.mockito.BDDMockito.given; 21 | import static org.mockito.Matchers.eq; 22 | import static org.mockito.Mockito.verify; 23 | 24 | public class MultiplicationServiceImplTest { 25 | 26 | private MultiplicationServiceImpl multiplicationServiceImpl; 27 | 28 | @Mock 29 | private RandomGeneratorService randomGeneratorService; 30 | 31 | @Mock 32 | private MultiplicationResultAttemptRepository attemptRepository; 33 | 34 | @Mock 35 | private UserRepository userRepository; 36 | 37 | @Mock 38 | private EventDispatcher eventDispatcher; 39 | 40 | @Before 41 | public void setUp() { 42 | // With this call to initMocks we tell Mockito to process the annotations 43 | MockitoAnnotations.initMocks(this); 44 | multiplicationServiceImpl = new MultiplicationServiceImpl(randomGeneratorService, attemptRepository, 45 | userRepository, eventDispatcher); 46 | } 47 | 48 | @Test 49 | public void createRandomMultiplicationTest() { 50 | // given (our mocked Random Generator service will return first 50, then 30) 51 | given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30); 52 | 53 | // when 54 | Multiplication multiplication = multiplicationServiceImpl.createRandomMultiplication(); 55 | 56 | // then 57 | assertThat(multiplication.getFactorA()).isEqualTo(50); 58 | assertThat(multiplication.getFactorB()).isEqualTo(30); 59 | } 60 | 61 | @Test 62 | public void checkCorrectAttemptTest() { 63 | // given 64 | Multiplication multiplication = new Multiplication(50, 60); 65 | User user = new User("john_doe"); 66 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 67 | user, multiplication, 3000, false); 68 | MultiplicationResultAttempt verifiedAttempt = new MultiplicationResultAttempt( 69 | user, multiplication, 3000, true); 70 | MultiplicationSolvedEvent event = new MultiplicationSolvedEvent(attempt.getId(), 71 | attempt.getUser().getId(), true); 72 | given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty()); 73 | 74 | // when 75 | boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt); 76 | 77 | // then 78 | assertThat(attemptResult).isTrue(); 79 | verify(attemptRepository).save(verifiedAttempt); 80 | verify(eventDispatcher).send(eq(event)); 81 | } 82 | 83 | @Test 84 | public void checkWrongAttemptTest() { 85 | // given 86 | Multiplication multiplication = new Multiplication(50, 60); 87 | User user = new User("john_doe"); 88 | MultiplicationResultAttempt attempt = new MultiplicationResultAttempt( 89 | user, multiplication, 3010, false); 90 | MultiplicationSolvedEvent event = new MultiplicationSolvedEvent(attempt.getId(), 91 | attempt.getUser().getId(), false); 92 | given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty()); 93 | 94 | // when 95 | boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt); 96 | 97 | // then 98 | assertThat(attemptResult).isFalse(); 99 | verify(attemptRepository).save(attempt); 100 | verify(eventDispatcher).send(eq(event)); 101 | } 102 | 103 | @Test 104 | public void retrieveStatsTest() { 105 | // given 106 | Multiplication multiplication = new Multiplication(50, 60); 107 | User user = new User("john_doe"); 108 | MultiplicationResultAttempt attempt1 = new MultiplicationResultAttempt( 109 | user, multiplication, 3010, false); 110 | MultiplicationResultAttempt attempt2 = new MultiplicationResultAttempt( 111 | user, multiplication, 3051, false); 112 | List latestAttempts = Lists.newArrayList(attempt1, attempt2); 113 | given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty()); 114 | given(attemptRepository.findTop5ByUserAliasOrderByIdDesc("john_doe")) 115 | .willReturn(latestAttempts); 116 | 117 | // when 118 | List latestAttemptsResult = 119 | multiplicationServiceImpl.getStatsForUser("john_doe"); 120 | 121 | // then 122 | assertThat(latestAttemptsResult).isEqualTo(latestAttempts); 123 | } 124 | } -------------------------------------------------------------------------------- /social-multiplication/src/test/java/microservices/book/multiplication/service/RandomGeneratorServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package microservices.book.multiplication.service; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.IntStream; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class RandomGeneratorServiceImplTest { 13 | 14 | private RandomGeneratorServiceImpl randomGeneratorServiceImpl; 15 | 16 | @Before 17 | public void setUp() { 18 | randomGeneratorServiceImpl = new RandomGeneratorServiceImpl(); 19 | } 20 | 21 | @Test 22 | public void generateRandomFactorIsBetweenExpectedLimits() throws Exception { 23 | // when a good sample of randomly generated factors is generated 24 | List randomFactors = IntStream.range(0, 1000) 25 | .map(i -> randomGeneratorServiceImpl.generateRandomFactor()) 26 | .boxed().collect(Collectors.toList()); 27 | 28 | // then all of them should be between 11 and 100 29 | // because we want a middle-complexity calculation 30 | assertThat(randomFactors).containsOnlyElementsOf(IntStream.range(11, 100) 31 | .boxed().collect(Collectors.toList())); 32 | } 33 | 34 | } --------------------------------------------------------------------------------