├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── com │ │ └── example │ │ ├── DemoApplication.java │ │ ├── controller │ │ ├── ChatController.java │ │ ├── MessageResource.java │ │ ├── StatisticResource.java │ │ └── vm │ │ │ ├── MessageVM.java │ │ │ ├── UserVM.java │ │ │ └── UsersStatisticVM.java │ │ ├── domain │ │ ├── Issue.java │ │ ├── Mention.java │ │ ├── Message.java │ │ ├── User.java │ │ └── utils │ │ │ └── ToCommaSeparatedValuesConverter.java │ │ ├── repository │ │ ├── MessageRepository.java │ │ └── UserRepository.java │ │ └── service │ │ ├── ChatService.java │ │ ├── MessageService.java │ │ ├── StatisticService.java │ │ ├── gitter │ │ ├── GitterClient.java │ │ ├── GitterConfiguration.java │ │ ├── GitterProperties.java │ │ ├── GitterUriBuilder.java │ │ └── dto │ │ │ ├── Issue.java │ │ │ ├── Mention.java │ │ │ ├── MessageResponse.java │ │ │ ├── Meta.java │ │ │ ├── Role.java │ │ │ ├── Url.java │ │ │ └── UserResponse.java │ │ └── impl │ │ ├── DefaultMessageService.java │ │ ├── DefaultStatisticService.java │ │ ├── GitterService.java │ │ └── utils │ │ ├── MessageMapper.java │ │ └── UserMapper.java └── resources │ ├── META-INF │ └── additional-spring-configuration-metadata.json │ ├── application-dev.yaml │ ├── application.yaml │ ├── liquibase │ ├── changelog │ │ └── 00000000000000_initial_schema.xml │ └── master.xml │ └── templates │ └── chat.html └── test ├── java └── com │ └── example │ ├── DemoApplicationTests.java │ ├── controller │ ├── ChatControllerTest.java │ ├── MessageResourceIntTest.java │ └── StatisticResourceIntTest.java │ ├── harness │ ├── Assertions.java │ └── ChatResponseFactory.java │ ├── repository │ ├── MessageRepositoryTest.java │ └── UserRepositoryTest.java │ └── service │ ├── GitterServiceTest.java │ ├── MessageServiceTest.java │ ├── StatisticServiceTest.java │ ├── gitter │ └── GitterClientTest.java │ └── utils │ ├── MessageMapperTest.java │ └── UserMapperTest.java └── resources └── com └── example ├── controller └── user-statistic.xml ├── repository ├── no-mentioned-user-info.xml ├── user-activity.xml └── user-popularity.xml └── service └── chat-messages-expectation.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /target/ 3 | /build/ 4 | /classes/ 5 | !gradle/wrapper/gradle-wrapper.jar 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get Reactive With Spring 5 and Project Reactor Demo App 2 | 3 | ## Structure 4 | 5 | In this repo, you will find a bunch of useful code examples and patterns. 6 | 7 | There are five branches in this repo: 8 | 9 | ### Brunches 10 | 11 | - **master** - contains initial Spring 4 project with the working Chat application, integrated with Gitter Channel. 12 | - **reactive** - include completed migration to Spring 5 with the adaptation of blocking JDBC driver + integration with Server-Sent Event. 13 | - **reactive+ws** - include completed migration to Spring 5 with adaptation of blocking JDBC driver + integration with reactive WebSockets. 14 | - **template** - includes the template for migration to Reactive Spring 5 with Blocking Database adaptation. This repo prepared to code and contained the list of TODOs and FIXMEs. It will be useful if you would like to start coding with Spring 5 and learn essentials of Reactive Programming with Project Reactor. If you are not familiar with Reactor 3 or RxJava, please take a look on one of [those](https://projectreactor.io/learn) useful tutorials, and practice your self with the [next](https://tech.io/playgrounds/929/reactive-programming-with-reactor-3/Intro) codding tutorial. 15 | - **migration-commit-machine** - commit by commit migration to Reactive Spring 5 + Non-blocking, reactive DB (MongoDB in that case). 16 | 17 | ### Project 18 | 19 | This project is based on the typical layered architecture. It includes four main folders: 20 | 21 | - **controller** - include list of endpoint for client-server communication purpose 22 | - **services** - include conceptual business logic: 23 | - Integration with Gitter Service 24 | - Statistic service 25 | - Messaging service for transferring messages from Gitter service through the server, store messages in the database, etc. 26 | - **repository** - Spring Data repositories for data-access purpose 27 | - **domain** - database structure representation in Java classes. 28 | 29 | ## Issues 30 | 31 | You are free to create an issue if you found one in the project. 32 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.5.7.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'eclipse' 15 | apply plugin: 'org.springframework.boot' 16 | 17 | version = '0.0.1-SNAPSHOT' 18 | sourceCompatibility = 1.8 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | compile('org.springframework.boot:spring-boot-starter-actuator') 26 | compile('org.springframework.boot:spring-boot-actuator-docs') 27 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 28 | compile('org.springframework.boot:spring-boot-starter-web') 29 | compile('org.springframework.boot:spring-boot-starter-thymeleaf') 30 | compile('org.liquibase:liquibase-core') 31 | compile('org.hibernate:hibernate-entitymanager:5.2.11.Final') 32 | compile('org.hibernate:hibernate-core:5.2.11.Final') 33 | runtime('com.h2database:h2') 34 | compileOnly('org.springframework.boot:spring-boot-configuration-processor') 35 | compileOnly('org.projectlombok:lombok') 36 | testCompile('org.springframework.boot:spring-boot-starter-test') 37 | testCompile('org.dbunit:dbunit:2.5.3') 38 | testCompile('org.mockito:mockito-core:2.8.+') 39 | testCompile('com.github.springtestdbunit:spring-test-dbunit:1.3.0') 40 | } 41 | 42 | bootRun { 43 | systemProperties System.properties 44 | } 45 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CollaborationInEncapsulation/get-reactive-with-spring5-demo/c26879fbf2c3ccc9b1c6a424872f21e152a5450f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 13 16:14:24 EEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/example/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DemoApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.controller.vm.UsersStatisticVM; 5 | import com.example.service.MessageService; 6 | import com.example.service.StatisticService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import java.util.List; 14 | 15 | @Controller 16 | @RequestMapping 17 | public class ChatController { 18 | 19 | private final MessageService messageService; 20 | private final StatisticService statisticService; 21 | 22 | @Autowired 23 | public ChatController(MessageService messageService, StatisticService statisticService) { 24 | this.messageService = messageService; 25 | this.statisticService = statisticService; 26 | } 27 | 28 | @GetMapping 29 | public String index(Model model) { 30 | List messages = messageService.latest(); 31 | UsersStatisticVM statistic = statisticService.getUsersStatistic(); 32 | 33 | model.addAttribute("messages", messages); 34 | model.addAttribute("statistic", statistic); 35 | 36 | if (messages != null && messages.size() > 0) { 37 | model.addAttribute("cursor", messages.get(messages.size() - 1).getId()); 38 | } 39 | 40 | return "chat"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/MessageResource.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.service.MessageService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | 17 | @RestController 18 | @RequestMapping("/api/v1/messages") 19 | public class MessageResource { 20 | private final MessageService messageService; 21 | 22 | @Autowired 23 | public MessageResource(MessageService messageService) { 24 | this.messageService = messageService; 25 | } 26 | 27 | @GetMapping 28 | public ResponseEntity> list(@RequestParam(value = "cursor", required = false) String cursor) { 29 | List messages = messageService.cursor(cursor); 30 | 31 | if (messages != null && messages.size() > 0) { 32 | return ResponseEntity.ok() 33 | .header("cursor", messages.get(messages.size() - 1).getId()) 34 | .body(messages); 35 | } else { 36 | ResponseEntity.HeadersBuilder headersBuilder = ResponseEntity.noContent(); 37 | 38 | if (cursor != null) { 39 | headersBuilder.header("cursor", cursor); 40 | } 41 | 42 | return headersBuilder.build(); 43 | } 44 | } 45 | 46 | @ResponseStatus 47 | @ExceptionHandler(RuntimeException.class) 48 | public ResponseEntity fallback(Exception e) { 49 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new Object() { 50 | public String message = e.getMessage(); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/StatisticResource.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.controller.vm.UsersStatisticVM; 4 | import com.example.service.StatisticService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/api/v1/statistics") 13 | public class StatisticResource { 14 | private final StatisticService statisticService; 15 | 16 | @Autowired 17 | public StatisticResource(StatisticService statisticService) { 18 | this.statisticService = statisticService; 19 | } 20 | 21 | @GetMapping("/users") 22 | public ResponseEntity getUsersStatistic() { 23 | return ResponseEntity.ok(statisticService.getUsersStatistic()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/vm/MessageVM.java: -------------------------------------------------------------------------------- 1 | package com.example.controller.vm; 2 | 3 | import lombok.NonNull; 4 | import lombok.Value; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.util.Date; 8 | 9 | @Value 10 | public class MessageVM { 11 | @NotNull 12 | @NonNull 13 | private String id; 14 | @NotNull 15 | @NonNull 16 | private String text; 17 | @NotNull 18 | @NonNull 19 | private String html; 20 | @NonNull 21 | @NotNull 22 | private String username; 23 | @NonNull 24 | @NotNull 25 | private String userAvatarUrl; 26 | @NotNull 27 | @NonNull 28 | private Date sent; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/vm/UserVM.java: -------------------------------------------------------------------------------- 1 | package com.example.controller.vm; 2 | 3 | import lombok.NonNull; 4 | import lombok.Value; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | @Value 9 | public class UserVM { 10 | @NotNull 11 | @NonNull 12 | private String id; 13 | @NotNull 14 | @NonNull 15 | private String name; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/vm/UsersStatisticVM.java: -------------------------------------------------------------------------------- 1 | package com.example.controller.vm; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class UsersStatisticVM { 7 | private UserVM mostActive; 8 | 9 | private UserVM mostMentioned; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/domain/Issue.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.NonNull; 8 | import lombok.experimental.Accessors; 9 | 10 | import javax.persistence.Embeddable; 11 | import javax.validation.constraints.NotNull; 12 | import java.io.Serializable; 13 | 14 | @Accessors(chain = true) 15 | @NoArgsConstructor 16 | @AllArgsConstructor(staticName = "of") 17 | @EqualsAndHashCode 18 | @Embeddable 19 | public class Issue implements Serializable { 20 | @NonNull 21 | @NotNull 22 | @Getter 23 | private Long id; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/example/domain/Mention.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.Accessors; 9 | import org.hibernate.annotations.NotFound; 10 | import org.hibernate.annotations.NotFoundAction; 11 | 12 | import javax.persistence.Column; 13 | import javax.persistence.ConstraintMode; 14 | import javax.persistence.Embeddable; 15 | import javax.persistence.EmbeddedId; 16 | import javax.persistence.Entity; 17 | import javax.persistence.ForeignKey; 18 | import javax.persistence.JoinColumn; 19 | import javax.persistence.ManyToOne; 20 | import javax.persistence.Table; 21 | import javax.validation.constraints.NotNull; 22 | import java.io.Serializable; 23 | 24 | @Data 25 | @Entity 26 | @Table(name = "mention") 27 | @Accessors(chain = true) 28 | @RequiredArgsConstructor(staticName = "of") 29 | @NoArgsConstructor 30 | public class Mention implements Serializable { 31 | @NotNull 32 | @NonNull 33 | @EmbeddedId 34 | private Key id; 35 | 36 | @Getter 37 | @ManyToOne 38 | @NotFound(action = NotFoundAction.IGNORE) 39 | @JoinColumn(name = "user_id", 40 | referencedColumnName = "id", 41 | insertable = false, 42 | updatable = false, 43 | foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) 44 | private User user = null; 45 | 46 | @Embeddable 47 | @Data 48 | @Accessors(chain = true) 49 | @RequiredArgsConstructor(staticName = "of") 50 | @NoArgsConstructor 51 | public static class Key implements Serializable { 52 | @NotNull 53 | @NonNull 54 | @Column(name = "message_id") 55 | private String messageId; 56 | 57 | @NotNull 58 | @NonNull 59 | @Column(name = "user_id") 60 | private String userId; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/example/domain/Message.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import com.example.domain.utils.ToCommaSeparatedValuesConverter; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.experimental.Accessors; 9 | 10 | import javax.persistence.CascadeType; 11 | import javax.persistence.CollectionTable; 12 | import javax.persistence.Convert; 13 | import javax.persistence.ElementCollection; 14 | import javax.persistence.Embedded; 15 | import javax.persistence.Entity; 16 | import javax.persistence.Id; 17 | import javax.persistence.JoinColumn; 18 | import javax.persistence.Lob; 19 | import javax.persistence.ManyToOne; 20 | import javax.persistence.NamedEntityGraph; 21 | import javax.persistence.OneToMany; 22 | import javax.persistence.Table; 23 | import javax.persistence.Temporal; 24 | import javax.persistence.TemporalType; 25 | import java.io.Serializable; 26 | import java.util.Date; 27 | import java.util.Set; 28 | 29 | @Entity 30 | @Table(name = "message") 31 | @Data 32 | @Builder 33 | @Accessors(chain = true) 34 | @NoArgsConstructor 35 | @AllArgsConstructor(staticName = "of") 36 | @NamedEntityGraph(name = "load.eager.all", includeAllAttributes = true) 37 | public class Message implements Serializable { 38 | @Id 39 | private String id; 40 | 41 | @Lob 42 | private String text; 43 | 44 | @Lob 45 | private String html; 46 | 47 | @Temporal(TemporalType.TIMESTAMP) 48 | private Date sent; 49 | 50 | @ManyToOne(cascade = CascadeType.ALL) 51 | private User user; 52 | 53 | private Boolean unread; 54 | 55 | private Long readBy; 56 | 57 | @Lob 58 | @Convert(converter = ToCommaSeparatedValuesConverter.class) 59 | private String[] urls; 60 | 61 | @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) 62 | @JoinColumn(name = "message_id", referencedColumnName = "id") 63 | private Set mentions; 64 | 65 | @Embedded 66 | @ElementCollection 67 | @CollectionTable(name = "issue", joinColumns = @JoinColumn(name = "message_id")) 68 | private Set issues; 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/example/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.NonNull; 7 | import lombok.experimental.Accessors; 8 | 9 | import javax.persistence.Entity; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | import javax.validation.constraints.NotNull; 13 | import java.io.Serializable; 14 | 15 | @Data 16 | @Entity 17 | @Table(name = "user") 18 | @Accessors(chain = true) 19 | @NoArgsConstructor 20 | @AllArgsConstructor(staticName = "of") 21 | public class User implements Serializable { 22 | @Id 23 | @NotNull 24 | @NonNull 25 | private String id; 26 | @NonNull 27 | @NotNull 28 | private String name; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/example/domain/utils/ToCommaSeparatedValuesConverter.java: -------------------------------------------------------------------------------- 1 | package com.example.domain.utils; 2 | 3 | 4 | import javax.persistence.AttributeConverter; 5 | import java.util.Arrays; 6 | import java.util.Optional; 7 | import java.util.stream.Collectors; 8 | 9 | public class ToCommaSeparatedValuesConverter implements AttributeConverter { 10 | private static final String COMMA_SEPARATOR = ", "; 11 | 12 | @Override 13 | public String convertToDatabaseColumn(String[] attribute) { 14 | return Optional.ofNullable(attribute) 15 | .map(a -> Arrays.stream(a).collect(Collectors.joining(COMMA_SEPARATOR))) 16 | .orElse(""); 17 | } 18 | 19 | @Override 20 | public String[] convertToEntityAttribute(String dbData) { 21 | return Optional.ofNullable(dbData) 22 | .map(v -> v.split(COMMA_SEPARATOR)) 23 | .orElse(new String[0]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/repository/MessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.repository; 2 | 3 | import com.example.domain.Message; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface MessageRepository extends JpaRepository { 13 | @Query("SELECT messages FROM Message messages") 14 | @EntityGraph(type = EntityGraph.EntityGraphType.LOAD, value = "load.eager.all") 15 | List findAllEager(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.repository; 2 | 3 | import com.example.domain.User; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.stereotype.Repository; 9 | 10 | @Repository 11 | public interface UserRepository extends JpaRepository { 12 | @Query("SELECT user " + 13 | "FROM Message messages " + 14 | "INNER JOIN messages.user user " + 15 | "GROUP BY user " + 16 | "ORDER BY COUNT(messages) DESC") 17 | Page findAllOrderedByActivityDesc(Pageable pageable); 18 | 19 | @Query("SELECT user " + 20 | "FROM Mention mentions " + 21 | "INNER JOIN mentions.user user " + 22 | "GROUP BY user " + 23 | "ORDER BY COUNT(mentions) DESC") 24 | Page findAllOrderedByMentionDesc(Pageable pageable); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/ChatService.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | public interface ChatService { 4 | Iterable getMessagesAfter(String messageId); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | 4 | import com.example.controller.vm.MessageVM; 5 | 6 | import java.util.List; 7 | 8 | public interface MessageService { 9 | List cursor(String cursor); 10 | 11 | List latest(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/StatisticService.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | import com.example.controller.vm.UsersStatisticVM; 4 | 5 | public interface StatisticService { 6 | UsersStatisticVM getUsersStatistic(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/GitterClient.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter; 2 | 3 | import com.example.service.gitter.dto.MessageResponse; 4 | import org.springframework.util.MultiValueMap; 5 | 6 | import java.util.List; 7 | 8 | 9 | public interface GitterClient { 10 | 11 | List getMessages(MultiValueMap query); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/GitterConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter; 2 | 3 | import com.example.service.gitter.dto.MessageResponse; 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 5 | import org.springframework.boot.web.client.RestTemplateBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.ParameterizedTypeReference; 9 | import org.springframework.http.HttpEntity; 10 | import org.springframework.http.HttpMethod; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.client.RestTemplate; 13 | import org.springframework.web.util.WebUtils; 14 | 15 | import java.util.List; 16 | 17 | @Configuration 18 | @EnableConfigurationProperties(GitterProperties.class) 19 | public class GitterConfiguration { 20 | 21 | @Bean 22 | public GitterClient gitterClient(GitterProperties gitterProperties, 23 | RestTemplate restTemplate) { 24 | return (query) -> { 25 | ResponseEntity> response = restTemplate.exchange( 26 | GitterUriBuilder.from(gitterProperties.getApi()) 27 | .queryParams(query) 28 | .build() 29 | .toUri(), 30 | HttpMethod.GET, 31 | new HttpEntity<>(WebUtils.parseMatrixVariables( 32 | "Authorization=Bearer " + gitterProperties.getApi().getAuth().getToken() 33 | )), 34 | new ParameterizedTypeReference>() { 35 | } 36 | ); 37 | 38 | if (response.getStatusCode().is2xxSuccessful()) { 39 | return response.getBody(); 40 | } else { 41 | //TODO replace with Custom Exception 42 | throw new RuntimeException(response.getBody().toString()); 43 | } 44 | }; 45 | } 46 | 47 | @Bean 48 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 49 | return builder.build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/GitterProperties.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import java.net.URI; 6 | 7 | @ConfigurationProperties(prefix = "gitter") 8 | public class GitterProperties { 9 | 10 | private GenericProperties api; 11 | 12 | public GenericProperties getApi() { 13 | return api; 14 | } 15 | 16 | public void setApi(GenericProperties api) { 17 | this.api = api; 18 | } 19 | 20 | public static class GenericProperties { 21 | private URI endpoint; 22 | private URI messagesResource; 23 | private String version = "v1"; 24 | private Auth auth = new Auth(); 25 | 26 | public URI getMessagesResource() { 27 | return messagesResource; 28 | } 29 | 30 | public void setMessagesResource(URI messagesResource) { 31 | this.messagesResource = messagesResource; 32 | } 33 | 34 | public URI getEndpoint() { 35 | return endpoint; 36 | } 37 | 38 | public void setEndpoint(URI endpoint) { 39 | this.endpoint = endpoint; 40 | } 41 | 42 | public String getVersion() { 43 | return version; 44 | } 45 | 46 | public void setVersion(String version) { 47 | this.version = version; 48 | } 49 | 50 | public Auth getAuth() { 51 | return auth; 52 | } 53 | 54 | public void setAuth(Auth auth) { 55 | this.auth = auth; 56 | } 57 | 58 | public static class Auth { 59 | private String token; 60 | 61 | public String getToken() { 62 | return token; 63 | } 64 | 65 | public void setToken(String token) { 66 | this.token = token; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/GitterUriBuilder.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter; 2 | 3 | 4 | import org.springframework.web.util.UriComponentsBuilder; 5 | 6 | public final class GitterUriBuilder { 7 | 8 | private GitterUriBuilder() { 9 | } 10 | 11 | public static UriComponentsBuilder from(GitterProperties.GenericProperties gitterProperties) { 12 | return UriComponentsBuilder.fromUri(gitterProperties.getEndpoint()) 13 | .pathSegment(gitterProperties.getVersion(), gitterProperties.getMessagesResource().toASCIIString()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/Issue.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Issue { 7 | private String number; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/Mention.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | import lombok.Value; 4 | 5 | import java.util.List; 6 | 7 | @Value 8 | public class Mention { 9 | private String screenName; 10 | private String userId; 11 | private List userIds; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/MessageResponse.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Value; 6 | 7 | import java.util.Date; 8 | import java.util.List; 9 | 10 | @Value 11 | @EqualsAndHashCode(exclude = "meta") 12 | public class MessageResponse { 13 | private String id; 14 | private String text; 15 | private String html; 16 | private Date sent; 17 | private String editedAt; 18 | private UserResponse fromUser; 19 | 20 | @JsonProperty("unread") 21 | private Boolean unRead; 22 | private Long readBy; 23 | private List urls; 24 | private List mentions; 25 | private List issues; 26 | private List meta; 27 | 28 | @JsonProperty("v") 29 | private Integer version; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/Meta.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | public class Meta { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/Role.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | public enum Role { 4 | ADMIN, 5 | STANDARD 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/Url.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Url { 7 | private String url; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/gitter/dto/UserResponse.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter.dto; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class UserResponse { 7 | private String id; 8 | private Integer v; 9 | private String username; 10 | private String displayName; 11 | private String avatarUrl; 12 | private String avatarUrlSmall; 13 | private String avatarUrlMedium; 14 | private Role role; 15 | private boolean staff; 16 | private String gv; 17 | private String url; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/DefaultMessageService.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.repository.MessageRepository; 5 | import com.example.service.ChatService; 6 | import com.example.service.MessageService; 7 | import com.example.service.gitter.dto.MessageResponse; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | import static com.example.service.impl.utils.MessageMapper.toDomainUnits; 14 | import static com.example.service.impl.utils.MessageMapper.toViewModelUnits; 15 | 16 | @Service 17 | public class DefaultMessageService implements MessageService { 18 | private final MessageRepository messageRepository; 19 | private final ChatService chatClient; 20 | 21 | @Autowired 22 | public DefaultMessageService(MessageRepository messageRepository, ChatService chatClient) { 23 | this.messageRepository = messageRepository; 24 | this.chatClient = chatClient; 25 | } 26 | 27 | @Override 28 | public List cursor(String cursor) { 29 | Iterable messages = chatClient.getMessagesAfter(cursor); 30 | 31 | messageRepository.save(toDomainUnits(messages)); 32 | 33 | return toViewModelUnits(messages); 34 | } 35 | 36 | @Override 37 | public List latest() { 38 | return cursor(null); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/DefaultStatisticService.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl; 2 | 3 | import com.example.controller.vm.UserVM; 4 | import com.example.controller.vm.UsersStatisticVM; 5 | import com.example.repository.UserRepository; 6 | import com.example.service.StatisticService; 7 | import com.example.service.impl.utils.UserMapper; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.PageRequest; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.transaction.Transactional; 13 | 14 | @Service 15 | public class DefaultStatisticService implements StatisticService { 16 | private static final UserVM EMPTY_USER = new UserVM("", ""); 17 | 18 | private final UserRepository userRepository; 19 | 20 | @Autowired 21 | public DefaultStatisticService(UserRepository userRepository) { 22 | this.userRepository = userRepository; 23 | } 24 | 25 | @Override 26 | @Transactional 27 | public UsersStatisticVM getUsersStatistic() { 28 | UserVM topActiveUser = userRepository.findAllOrderedByActivityDesc(new PageRequest(0, 1)) 29 | .map(UserMapper::toViewModelUnits) 30 | .getContent() 31 | .stream() 32 | .findFirst() 33 | .orElse(EMPTY_USER); 34 | 35 | UserVM topMentionedUser = userRepository.findAllOrderedByMentionDesc(new PageRequest(0, 1)) 36 | .map(UserMapper::toViewModelUnits) 37 | .getContent() 38 | .stream() 39 | .findFirst() 40 | .orElse(EMPTY_USER); 41 | 42 | return new UsersStatisticVM(topActiveUser, topMentionedUser); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/GitterService.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl; 2 | 3 | import com.example.service.ChatService; 4 | import com.example.service.gitter.GitterClient; 5 | import com.example.service.gitter.dto.MessageResponse; 6 | import lombok.SneakyThrows; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.util.LinkedMultiValueMap; 10 | import org.springframework.util.MultiValueMap; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | public class GitterService implements ChatService { 16 | 17 | private final GitterClient gitterClient; 18 | 19 | @Autowired 20 | public GitterService(GitterClient gitterClient) { 21 | this.gitterClient = gitterClient; 22 | } 23 | 24 | @Override 25 | @SneakyThrows 26 | public Iterable getMessagesAfter(String messageId) { 27 | MultiValueMap query = new LinkedMultiValueMap<>(); 28 | 29 | Optional.ofNullable(messageId).ifPresent(v -> query.add("afterId", v)); 30 | 31 | return gitterClient.getMessages(query); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/utils/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl.utils; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.domain.Issue; 5 | import com.example.domain.Mention; 6 | import com.example.domain.Message; 7 | import com.example.domain.User; 8 | import com.example.service.gitter.dto.MessageResponse; 9 | import com.example.service.gitter.dto.Url; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | public final class MessageMapper { 16 | private MessageMapper() { 17 | } 18 | 19 | public static List toViewModelUnits(Iterable messages) { 20 | if (messages == null) { 21 | return null; 22 | } 23 | 24 | List vms = new ArrayList<>(); 25 | 26 | for (MessageResponse message : messages) { 27 | vms.add(toViewModelUnit(message)); 28 | } 29 | 30 | return vms; 31 | 32 | } 33 | 34 | public static List toDomainUnits(Iterable messages) { 35 | if (messages == null) { 36 | return null; 37 | } 38 | 39 | List persistable = new ArrayList<>(); 40 | 41 | for (MessageResponse message : messages) { 42 | persistable.add(toDomainUnit(message)); 43 | } 44 | 45 | return persistable; 46 | } 47 | 48 | private static Message toDomainUnit(MessageResponse message) { 49 | if (message == null) { 50 | return null; 51 | } 52 | 53 | return Message.builder() 54 | .id(message.getId()) 55 | .html(message.getHtml()) 56 | .text(message.getText()) 57 | .sent(message.getSent()) 58 | .readBy(message.getReadBy()) 59 | .unread(message.getUnRead()) 60 | .user(User.of(message.getFromUser().getId(), message.getFromUser().getUsername())) 61 | .urls(message.getUrls().stream().map(Url::getUrl).toArray(String[]::new)) 62 | .mentions(message.getMentions().stream().filter(m -> m.getUserId() != null) 63 | .map(m -> Mention.of(Mention.Key.of(message.getId(), m.getUserId()))) 64 | .collect(Collectors.toSet())) 65 | .issues(message.getIssues().stream() 66 | .map(i -> Issue.of(Long.valueOf(i.getNumber()))).collect(Collectors.toSet())) 67 | .build(); 68 | } 69 | 70 | private static MessageVM toViewModelUnit(MessageResponse message) { 71 | if (message == null) { 72 | return null; 73 | } 74 | 75 | return new MessageVM(message.getId(), 76 | message.getText(), 77 | message.getHtml(), 78 | message.getFromUser().getUsername(), 79 | message.getFromUser().getAvatarUrl(), 80 | message.getSent()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/utils/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl.utils; 2 | 3 | import com.example.controller.vm.UserVM; 4 | import com.example.domain.User; 5 | 6 | public final class UserMapper { 7 | private UserMapper() { 8 | } 9 | 10 | public static UserVM toViewModelUnits(User domainUser) { 11 | if (domainUser == null) { 12 | return null; 13 | } 14 | 15 | return new UserVM(domainUser.getId(), domainUser.getName()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "name": "gitter", 5 | "type": "com.example.service.gitter.GitterProperties", 6 | "sourceType": "com.example.service.gitter.GitterProperties", 7 | "description": "Gitter Api Configurations Properties." 8 | } 9 | ], 10 | "properties": [ 11 | { 12 | "name": "gitter.api.endpoint", 13 | "type": "java.net.URI", 14 | "sourceType": "com.example.service.gitter.GitterProperties", 15 | "description": "Endpoint URI for interact with rooms, users and messages." 16 | }, 17 | { 18 | "name": "gitter.api.messages-resource", 19 | "type": "java.net.URI", 20 | "sourceType": "com.example.service.gitter.GitterProperties", 21 | "description": "Messages resource path property. Should include room id too." 22 | }, 23 | { 24 | "name": "gitter.api.version", 25 | "type": "java.lang.String", 26 | "sourceType": "com.example.service.gitter.GitterProperties", 27 | "description": "Gitter API version.", 28 | "defaultValue": "v1" 29 | }, 30 | { 31 | "name": "gitter.api.auth.token", 32 | "type": "java.lang.String", 33 | "sourceType": "com.example.service.gitter.GitterProperties", 34 | "description": "Gitter API Authentication token." 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/main/resources/application-dev.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | thymeleaf: 3 | cache: false 4 | jpa: 5 | properties: 6 | format_sql: true 7 | show_sql: true -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | liquibase: 2 | change-log: classpath:liquibase/master.xml 3 | 4 | spring: 5 | datasource: 6 | url: jdbc:h2:file:./target/h2db/db/demodb;DB_CLOSE_DELAY=-1 7 | h2: 8 | console: 9 | enabled: true 10 | jackson: 11 | parser: 12 | profiles: 13 | active: live 14 | jpa: 15 | hibernate: 16 | ddl-auto: validate 17 | gitter: 18 | api: 19 | endpoint: https://api.gitter.im/ 20 | messages-resource: rooms/591ca8acd73408ce4f6049c1/chatMessages 21 | auth: 22 | token: ${GITTER_TOKEN} 23 | -------------------------------------------------------------------------------- /src/main/resources/liquibase/changelog/00000000000000_initial_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 78 | 79 | 82 | 83 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/resources/liquibase/master.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat 6 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 21 | 22 | 23 | 54 | 55 | 56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 | 65 | Most Active User : 66 | 67 | 68 |
69 |
70 | 71 | 72 | Most Mentioned User : 73 | 74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 |
    82 |
  • 83 |
    84 |
    85 | 86 | 88 | 89 |
    90 | title 91 |
    92 | 96 |
    97 |
    98 |
    99 |
    100 |
  • 101 |
102 |
103 |
104 |
105 |
106 |
107 | 155 | 156 | -------------------------------------------------------------------------------- /src/test/java/com/example/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class DemoApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/example/controller/ChatControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.controller.vm.UserVM; 5 | import com.example.controller.vm.UsersStatisticVM; 6 | import com.example.service.ChatService; 7 | import com.example.service.gitter.dto.MessageResponse; 8 | import com.example.harness.Assertions; 9 | import com.example.harness.ChatResponseFactory; 10 | import com.github.springtestdbunit.DbUnitTestExecutionListener; 11 | import com.github.springtestdbunit.annotation.DatabaseSetup; 12 | import org.hamcrest.BaseMatcher; 13 | import org.hamcrest.Description; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mockito; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 19 | import org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener; 20 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 21 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 22 | import org.springframework.boot.test.context.SpringBootTest; 23 | import org.springframework.boot.test.mock.mockito.MockBean; 24 | import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener; 25 | import org.springframework.http.MediaType; 26 | import org.springframework.test.context.TestExecutionListeners; 27 | import org.springframework.test.context.junit4.SpringRunner; 28 | import org.springframework.test.web.servlet.MockMvc; 29 | 30 | import java.util.List; 31 | 32 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 36 | 37 | @SpringBootTest 38 | @RunWith(SpringRunner.class) 39 | @AutoConfigureMockMvc 40 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 41 | @TestExecutionListeners({ 42 | MockitoTestExecutionListener.class, 43 | SpringBootDependencyInjectionTestExecutionListener.class, 44 | DbUnitTestExecutionListener.class 45 | }) 46 | public class ChatControllerTest { 47 | 48 | @Autowired 49 | private MockMvc mockMvc; 50 | @MockBean 51 | private ChatService chatClient; 52 | 53 | @Test 54 | @DatabaseSetup("user-statistic.xml") 55 | public void shouldRespondOnRootUrlWithCorrectModel() throws Exception { 56 | Mockito.when(chatClient.getMessagesAfter(null)).thenReturn(ChatResponseFactory.messages(10)); 57 | 58 | mockMvc.perform(get("") 59 | .accept(MediaType.APPLICATION_JSON) 60 | .accept(MediaType.APPLICATION_JSON_UTF8)) 61 | .andExpect(status().isOk()) 62 | .andExpect(model().attribute("messages", new BaseMatcher>() { 63 | @Override 64 | public void describeTo(Description description) { 65 | 66 | } 67 | 68 | @Override 69 | public boolean matches(Object item) { 70 | Assertions.assertMessages((List) item); 71 | return true; 72 | } 73 | })) 74 | .andExpect(model().attribute("statistic", 75 | new UsersStatisticVM(new UserVM("53307734c3599d1de448e192", "suprememoocow"), 76 | new UserVM("53316dc47bfc1a000000000f", "oledok")))) 77 | .andExpect(view().name("chat")); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/example/controller/MessageResourceIntTest.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.service.ChatService; 4 | import com.example.service.gitter.dto.MessageResponse; 5 | import com.example.harness.ChatResponseFactory; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mockito; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 11 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.boot.test.mock.mockito.MockBean; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | 19 | import static org.hamcrest.Matchers.hasItems; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | 24 | @SpringBootTest 25 | @RunWith(SpringRunner.class) 26 | @AutoConfigureMockMvc 27 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 28 | public class MessageResourceIntTest { 29 | 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @MockBean 34 | private ChatService chatClient; 35 | 36 | 37 | @Test 38 | public void shouldReturnExpectedJson() throws Exception { 39 | Mockito.when(chatClient.getMessagesAfter(null)).thenReturn(ChatResponseFactory.messages(10)); 40 | 41 | mockMvc.perform(get("/api/v1/messages") 42 | .accept(MediaType.APPLICATION_JSON) 43 | .accept(MediaType.APPLICATION_JSON_UTF8)) 44 | .andExpect(status().isOk()) 45 | .andExpect(jsonPath("$[*].id").value(hasItems("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"))); 46 | } 47 | 48 | @Test 49 | public void shouldReturnExpectedJsonAfterGivenCursor() throws Exception { 50 | Mockito.when(chatClient.getMessagesAfter(Mockito.anyString())).thenReturn(ChatResponseFactory.messages(10)); 51 | 52 | mockMvc.perform(get("/api/v1/messages") 53 | .param("cursor", "qwerty") 54 | .accept(MediaType.APPLICATION_JSON) 55 | .accept(MediaType.APPLICATION_JSON_UTF8)) 56 | .andExpect(status().isOk()) 57 | .andExpect(jsonPath("$[*].id").value(hasItems("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"))); 58 | 59 | Mockito.verify(chatClient).getMessagesAfter("qwerty"); 60 | } 61 | 62 | @Test 63 | public void shouldRespondWithNoContent() throws Exception { 64 | Mockito.when(chatClient.getMessagesAfter(null)).thenReturn(null); 65 | 66 | mockMvc.perform(get("/api/v1/messages") 67 | .accept(MediaType.APPLICATION_JSON) 68 | .accept(MediaType.APPLICATION_JSON_UTF8)) 69 | .andExpect(status().isNoContent()); 70 | } 71 | 72 | @Test 73 | public void shouldHandleExternalExceptions() throws Exception { 74 | Mockito.when(chatClient.getMessagesAfter(null)).thenThrow(new RuntimeException("Wrong cursor")); 75 | 76 | 77 | mockMvc.perform(get("/api/v1/messages") 78 | .accept(MediaType.APPLICATION_JSON) 79 | .accept(MediaType.APPLICATION_JSON_UTF8)) 80 | .andExpect(status().isInternalServerError()) 81 | .andExpect(jsonPath("$.message").value("Wrong cursor")); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/example/controller/StatisticResourceIntTest.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.github.springtestdbunit.DbUnitTestExecutionListener; 4 | import com.github.springtestdbunit.annotation.DatabaseSetup; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 9 | import org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener; 10 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.context.TestExecutionListeners; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 21 | 22 | @SpringBootTest 23 | @RunWith(SpringRunner.class) 24 | @AutoConfigureMockMvc 25 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 26 | @TestExecutionListeners({SpringBootDependencyInjectionTestExecutionListener.class, DbUnitTestExecutionListener.class}) 27 | public class StatisticResourceIntTest { 28 | 29 | @Autowired 30 | private MockMvc mockMvc; 31 | 32 | @Test 33 | @DatabaseSetup("user-statistic.xml") 34 | public void shouldReturnExpectedJson() throws Exception { 35 | mockMvc.perform(get("/api/v1/statistics/users") 36 | .accept(MediaType.APPLICATION_JSON) 37 | .accept(MediaType.APPLICATION_JSON_UTF8)) 38 | .andExpect(status().isOk()) 39 | .andExpect(jsonPath("$.mostActive.id").value("53307734c3599d1de448e192")) 40 | .andExpect(jsonPath("$.mostMentioned.id").value("53316dc47bfc1a000000000f")); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/example/harness/Assertions.java: -------------------------------------------------------------------------------- 1 | package com.example.harness; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.service.gitter.dto.MessageResponse; 5 | import org.junit.Assert; 6 | 7 | import java.util.Collection; 8 | import java.util.Iterator; 9 | 10 | public final class Assertions { 11 | private Assertions() { 12 | } 13 | 14 | public static void assertMessages(Collection messages) { 15 | Iterator iterator = messages.iterator(); 16 | 17 | for (int i = 0; iterator.hasNext(); i++) { 18 | MessageVM messageVM = iterator.next(); 19 | String expected = String.valueOf(i); 20 | 21 | Assert.assertEquals(expected, messageVM.getId()); 22 | Assert.assertEquals(expected, messageVM.getText()); 23 | Assert.assertEquals(expected, messageVM.getHtml()); 24 | Assert.assertEquals(expected, messageVM.getUserAvatarUrl()); 25 | Assert.assertEquals(expected, messageVM.getUsername()); 26 | } 27 | } 28 | 29 | public static void assertMessages(Iterable messages) { 30 | Iterator iterator = messages.iterator(); 31 | 32 | for (int i = 0; iterator.hasNext(); i++) { 33 | MessageResponse message = iterator.next(); 34 | 35 | Assert.assertEquals(ChatResponseFactory.message(String.valueOf(i)), message); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/com/example/harness/ChatResponseFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.harness; 2 | 3 | 4 | import com.example.service.gitter.dto.*; 5 | 6 | import java.time.Instant; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.IntStream; 13 | 14 | public final class ChatResponseFactory { 15 | private ChatResponseFactory() { 16 | } 17 | 18 | public static List messages(int amount) { 19 | return IntStream.range(0, amount) 20 | .mapToObj(String::valueOf) 21 | .map(ChatResponseFactory::message) 22 | .collect(Collectors.toList()); 23 | } 24 | 25 | public static MessageResponse message(String id) { 26 | return new MessageResponse( 27 | id, 28 | id, 29 | id, 30 | Date.from(Instant.ofEpochSecond(1395748292L)), 31 | null, 32 | user(id), 33 | true, 34 | 0L, 35 | Arrays.asList(new Url(id), new Url(id), new Url(id)), 36 | Collections.singletonList(mention(id)), 37 | Collections.singletonList(issue(id)), 38 | Collections.emptyList(), 39 | 1 40 | ); 41 | } 42 | 43 | public static Issue issue(String id) { 44 | return new Issue(id); 45 | } 46 | 47 | public static Mention mention(String id) { 48 | return new Mention(id, id, Collections.emptyList()); 49 | } 50 | 51 | public static UserResponse user(String id) { 52 | return new UserResponse(id, null, id, id, id, id, id, Role.STANDARD, false, id, id); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/example/repository/MessageRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.example.repository; 2 | 3 | import com.example.domain.Mention; 4 | import com.example.domain.Message; 5 | import com.github.springtestdbunit.DbUnitTestExecutionListener; 6 | import com.github.springtestdbunit.annotation.DatabaseSetup; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 12 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 14 | import org.springframework.test.context.TestExecutionListeners; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; 17 | 18 | import java.util.List; 19 | 20 | @RunWith(SpringRunner.class) 21 | @DataJpaTest 22 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 23 | @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DbUnitTestExecutionListener.class}) 24 | public class MessageRepositoryTest { 25 | 26 | @Autowired 27 | private MessageRepository messageRepository; 28 | 29 | @Test 30 | @DatabaseSetup("no-mentioned-user-info.xml") 31 | public void userShouldBeNullInMentions() { 32 | List messages = messageRepository.findAllEager(); 33 | 34 | Assert.assertEquals(messages.size(), 1); 35 | messages.stream() 36 | .flatMap(m -> m.getMentions().stream()) 37 | .map(Mention::getUser) 38 | .forEach(Assert::assertNull); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/example/repository/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.example.repository; 2 | 3 | import com.example.domain.User; 4 | import com.github.springtestdbunit.DbUnitTestExecutionListener; 5 | import com.github.springtestdbunit.annotation.DatabaseSetup; 6 | import org.junit.After; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 12 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageRequest; 16 | import org.springframework.jdbc.core.JdbcTemplate; 17 | import org.springframework.test.context.TestExecutionListeners; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; 20 | import org.springframework.test.jdbc.JdbcTestUtils; 21 | 22 | @RunWith(SpringRunner.class) 23 | @DataJpaTest 24 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 25 | @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DbUnitTestExecutionListener.class}) 26 | public class UserRepositoryTest { 27 | 28 | @Autowired 29 | private UserRepository userRepository; 30 | 31 | @Autowired 32 | private JdbcTemplate jdbcTemplate; 33 | 34 | @After 35 | public void cleanUp() { 36 | JdbcTestUtils.deleteFromTables(jdbcTemplate, "issue", "mention", "message", "user"); 37 | } 38 | 39 | @Test 40 | @DatabaseSetup(value = "user-activity.xml") 41 | public void shouldFindExpectedMostActiveUser() { 42 | Page mostActiveUsers = userRepository.findAllOrderedByActivityDesc(new PageRequest(0, 1)); 43 | 44 | Assert.assertEquals(mostActiveUsers.getNumberOfElements(), 1); 45 | Assert.assertEquals(User.of("53316dc47bfc1a000000000f", "oledok"), 46 | mostActiveUsers.getContent().stream().findFirst().orElse(null)); 47 | } 48 | 49 | @Test 50 | @DatabaseSetup("user-popularity.xml") 51 | public void shouldFindExpectedMostPopularUser() { 52 | Page mostActiveUsers = userRepository.findAllOrderedByMentionDesc(new PageRequest(0, 1)); 53 | 54 | Assert.assertEquals(mostActiveUsers.getNumberOfElements(), 1); 55 | Assert.assertEquals(User.of("53316dc47bfc1a000000000f", "oledok"), 56 | mostActiveUsers.getContent().stream().findFirst().orElse(null)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/GitterServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | 4 | import com.example.service.gitter.GitterClient; 5 | import com.example.service.gitter.dto.MessageResponse; 6 | import com.example.service.impl.GitterService; 7 | import com.example.harness.Assertions; 8 | import com.example.harness.ChatResponseFactory; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.mockito.Mockito; 12 | import org.springframework.web.util.WebUtils; 13 | 14 | import static org.mockito.Matchers.any; 15 | import static org.mockito.Mockito.eq; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class GitterServiceTest { 19 | private GitterService gitterService; 20 | private GitterClient gitterClient; 21 | 22 | @Before 23 | public void setUp() { 24 | gitterClient = Mockito.mock(GitterClient.class); 25 | gitterService = new GitterService(gitterClient); 26 | } 27 | 28 | @Test 29 | public void shouldReturnMessagesFromGitter() { 30 | when(gitterClient.getMessages(any())).thenReturn(ChatResponseFactory.messages(10)); 31 | Iterable response = gitterService.getMessagesAfter(null); 32 | 33 | Assertions.assertMessages(response); 34 | } 35 | 36 | @Test 37 | public void shouldReturnMessagesFromGitterAfterGivenCursor() { 38 | when(gitterClient.getMessages(any())).thenReturn(ChatResponseFactory.messages(1)); 39 | Iterable response = gitterService.getMessagesAfter("qwerty"); 40 | 41 | Mockito.verify(gitterClient).getMessages(eq(WebUtils.parseMatrixVariables("afterId=qwerty"))); 42 | Assertions.assertMessages(response); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/MessageServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | import com.example.controller.vm.MessageVM; 4 | import com.example.service.gitter.dto.MessageResponse; 5 | import com.example.service.impl.DefaultMessageService; 6 | import com.example.harness.Assertions; 7 | import com.example.harness.ChatResponseFactory; 8 | import com.github.springtestdbunit.DbUnitTestExecutionListener; 9 | import com.github.springtestdbunit.annotation.ExpectedDatabase; 10 | import com.github.springtestdbunit.assertion.DatabaseAssertionMode; 11 | import org.junit.Assert; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mockito; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; 17 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 18 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 19 | import org.springframework.boot.test.mock.mockito.MockBean; 20 | import org.springframework.context.annotation.Import; 21 | import org.springframework.test.context.TestExecutionListeners; 22 | import org.springframework.test.context.junit4.SpringRunner; 23 | import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; 24 | 25 | import java.util.List; 26 | 27 | @RunWith(SpringRunner.class) 28 | @DataJpaTest 29 | @Import(DefaultMessageService.class) 30 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 31 | @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DbUnitTestExecutionListener.class}) 32 | public class MessageServiceTest { 33 | 34 | @Autowired 35 | private MessageService messageService; 36 | 37 | @MockBean 38 | @Autowired 39 | private ChatService chatClient; 40 | 41 | @Test 42 | @ExpectedDatabase(value = "chat-messages-expectation.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) 43 | public void shouldReturnAndStoreLatestMessagesFromChat() { 44 | Mockito.when(chatClient.getMessagesAfter(null)).thenReturn(ChatResponseFactory.messages(10)); 45 | List messages = messageService.latest(); 46 | 47 | Assert.assertEquals(messages.size(), 10); 48 | Assertions.assertMessages(messages); 49 | } 50 | 51 | @Test 52 | @ExpectedDatabase(value = "chat-messages-expectation.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) 53 | public void shouldReturnAndStoreMessagesFromChatAfterGivenCursor() { 54 | Mockito.when(chatClient.getMessagesAfter(Mockito.anyString())).thenReturn(ChatResponseFactory.messages(10)); 55 | List messages = messageService.cursor("qwerty"); 56 | 57 | Mockito.verify(chatClient).getMessagesAfter("qwerty"); 58 | Assert.assertEquals(messages.size(), 10); 59 | Assertions.assertMessages(messages); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/StatisticServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | import com.example.controller.vm.UserVM; 4 | import com.example.controller.vm.UsersStatisticVM; 5 | import com.example.domain.User; 6 | import com.example.repository.UserRepository; 7 | import com.example.service.impl.DefaultStatisticService; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.data.domain.PageImpl; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | 19 | import java.util.Collections; 20 | 21 | @RunWith(SpringRunner.class) 22 | @Import(DefaultStatisticService.class) 23 | public class StatisticServiceTest { 24 | private static final UserVM EMPTY_USER = new UserVM("", ""); 25 | 26 | @Autowired 27 | private StatisticService statisticService; 28 | 29 | @MockBean 30 | private UserRepository userRepository; 31 | 32 | @Test 33 | public void shouldReturnEmptyStatisticOnEmptyDataBase() { 34 | Mockito.when(userRepository.findAllOrderedByActivityDesc(Mockito.any())) 35 | .then(a -> new PageImpl(Collections.emptyList(), (Pageable) a.getArguments()[0], 0)); 36 | Mockito.when(userRepository.findAllOrderedByMentionDesc(Mockito.any())) 37 | .then(a -> new PageImpl(Collections.emptyList(), (Pageable) a.getArguments()[0], 0)); 38 | UsersStatisticVM usersStatistic = statisticService.getUsersStatistic(); 39 | 40 | Assert.assertNotNull(usersStatistic); 41 | Assert.assertEquals(usersStatistic.getMostActive(), EMPTY_USER); 42 | Assert.assertEquals(usersStatistic.getMostMentioned(), EMPTY_USER); 43 | } 44 | 45 | @Test 46 | public void shouldReturnStatistic() { 47 | Mockito.when(userRepository.findAllOrderedByActivityDesc(Mockito.any())) 48 | .then(a -> new PageImpl<>(Collections.singletonList(User.of("1", "1")), 49 | (Pageable) a.getArguments()[0], 1)); 50 | Mockito.when(userRepository.findAllOrderedByMentionDesc(Mockito.any())) 51 | .then(a -> new PageImpl<>(Collections.singletonList(User.of("1", "1")), 52 | (Pageable) a.getArguments()[0], 1)); 53 | UsersStatisticVM usersStatistic = statisticService.getUsersStatistic(); 54 | 55 | Assert.assertNotNull(usersStatistic); 56 | Assert.assertEquals(new UserVM("1", "1"), usersStatistic.getMostActive()); 57 | Assert.assertEquals(new UserVM("1", "1"), usersStatistic.getMostMentioned()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/gitter/GitterClientTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service.gitter; 2 | 3 | import com.example.harness.Assertions; 4 | import com.example.harness.ChatResponseFactory; 5 | import com.example.service.gitter.dto.MessageResponse; 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.client.MockRestServiceServer; 17 | import org.springframework.web.util.WebUtils; 18 | 19 | import java.util.List; 20 | 21 | import static org.hamcrest.Matchers.startsWith; 22 | import static org.springframework.test.web.client.ExpectedCount.once; 23 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; 24 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.queryParam; 25 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; 26 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; 27 | 28 | @RunWith(SpringRunner.class) 29 | @RestClientTest(GitterConfiguration.class) 30 | public class GitterClientTest { 31 | 32 | @Autowired 33 | private GitterClient gitterClient; 34 | @Autowired 35 | private MockRestServiceServer server; 36 | @Autowired 37 | private ObjectMapper objectMapper; 38 | @Autowired 39 | private GitterProperties properties; 40 | 41 | @Test 42 | public void shouldExpectRequestWithNoQueryAndResponseWithLatestMessages() throws JsonProcessingException { 43 | server.expect(once(), requestTo(GitterUriBuilder.from(properties.getApi()).build().toUriString())) 44 | .andExpect(method(HttpMethod.GET)) 45 | .andRespond(withSuccess(objectMapper.writeValueAsBytes(ChatResponseFactory.messages(5)), 46 | MediaType.APPLICATION_JSON)); 47 | 48 | List messages = gitterClient.getMessages(null); 49 | 50 | server.verify(); 51 | Assert.assertEquals(5, messages.size()); 52 | Assertions.assertMessages(messages); 53 | } 54 | 55 | 56 | @Test 57 | public void shouldExpectRequestWithAfterIdQueryAndResponseWithOneMessage() throws JsonProcessingException { 58 | server.expect(once(), requestTo(startsWith(GitterUriBuilder.from(properties.getApi()).build().toUriString()))) 59 | .andExpect(method(HttpMethod.GET)) 60 | .andExpect(queryParam("afterId", "qwerty")) 61 | .andRespond(withSuccess(objectMapper.writeValueAsBytes(ChatResponseFactory.messages(1)), 62 | MediaType.APPLICATION_JSON)); 63 | 64 | List messages = gitterClient.getMessages(WebUtils.parseMatrixVariables("afterId=qwerty")); 65 | 66 | server.verify(); 67 | Assert.assertEquals(1, messages.size()); 68 | Assertions.assertMessages(messages); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/utils/MessageMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service.utils; 2 | 3 | 4 | import com.example.controller.vm.MessageVM; 5 | import com.example.domain.Issue; 6 | import com.example.domain.Mention; 7 | import com.example.domain.Message; 8 | import com.example.domain.User; 9 | import com.example.service.impl.utils.MessageMapper; 10 | import com.example.harness.ChatResponseFactory; 11 | import org.junit.Assert; 12 | import org.junit.Test; 13 | 14 | import java.time.Instant; 15 | import java.util.Collections; 16 | import java.util.Date; 17 | 18 | import static java.util.Collections.singletonList; 19 | 20 | public class MessageMapperTest { 21 | 22 | @Test 23 | public void shouldCorrectlyMapChatModelToViewModel() { 24 | Assert.assertEquals( 25 | singletonList(new MessageVM("0", "0", "0", "0", "0", Date.from(Instant.ofEpochSecond(1395748292L)))), 26 | MessageMapper.toViewModelUnits(ChatResponseFactory.messages(1)) 27 | ); 28 | } 29 | 30 | @Test 31 | public void shouldReturnNullViewModelOnNullableChatModelList() { 32 | Assert.assertEquals( 33 | null, 34 | MessageMapper.toViewModelUnits(null) 35 | ); 36 | } 37 | 38 | @Test 39 | public void shouldReturnCollectionWithNullsViewModelOnCollectionWithNullableChatModels() { 40 | Assert.assertEquals( 41 | singletonList(null), 42 | MessageMapper.toViewModelUnits(singletonList(null)) 43 | ); 44 | } 45 | 46 | @Test 47 | public void shouldCorrectlyMapChatModelToDomainModel() { 48 | Assert.assertEquals( 49 | singletonList(Message.of("0", "0", "0", 50 | Date.from(Instant.ofEpochSecond(1395748292L)), 51 | User.of("0", "0"), true, 0L, new String[]{"0", "0", "0"}, 52 | Collections.singleton(Mention.of(Mention.Key.of("0", "0"))), 53 | Collections.singleton(Issue.of(0L)))), 54 | MessageMapper.toDomainUnits(ChatResponseFactory.messages(1)) 55 | ); 56 | } 57 | 58 | @Test 59 | public void shouldReturnNullDomainModelOnNullableChatModelList() { 60 | Assert.assertEquals( 61 | null, 62 | MessageMapper.toDomainUnits(null) 63 | ); 64 | } 65 | 66 | @Test 67 | public void shouldReturnCollectionWithNullsDomainModelOnCollectionWithNullableChatModels() { 68 | Assert.assertEquals( 69 | singletonList(null), 70 | MessageMapper.toDomainUnits(singletonList(null)) 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/example/service/utils/UserMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.service.utils; 2 | 3 | 4 | import com.example.controller.vm.UserVM; 5 | import com.example.domain.User; 6 | import com.example.service.impl.utils.UserMapper; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | 10 | public class UserMapperTest { 11 | 12 | @Test 13 | public void shouldCorrectlyMapDomainModelToViewModel() { 14 | Assert.assertEquals( 15 | new UserVM("0", "0"), 16 | UserMapper.toViewModelUnits(User.of("0", "0")) 17 | ); 18 | } 19 | 20 | @Test 21 | public void shouldReturnNullViewModelOnNullableDomainModel() { 22 | Assert.assertEquals( 23 | null, 24 | UserMapper.toViewModelUnits(null) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/com/example/controller/user-statistic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/resources/com/example/repository/no-mentioned-user-info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /src/test/resources/com/example/repository/user-activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | -------------------------------------------------------------------------------- /src/test/resources/com/example/repository/user-popularity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/resources/com/example/service/chat-messages-expectation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | --------------------------------------------------------------------------------