├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── activator ├── activator-launch-1.2.3.jar ├── activator.properties ├── app ├── ApplicationLifecycleModule.scala ├── Filters.scala ├── Module.scala ├── com │ └── josip │ │ └── reactiveluxury │ │ ├── configuration │ │ ├── AuthenticationConfigurationSetup.scala │ │ ├── CustomExecutionContext.scala │ │ ├── DatabaseProvider.scala │ │ └── actor │ │ │ └── ActorFactory.scala │ │ ├── core │ │ ├── Asserts.scala │ │ ├── Converter.scala │ │ ├── Field.scala │ │ ├── GeneratedId.scala │ │ ├── Validator.scala │ │ ├── authentication │ │ │ └── Credentials.scala │ │ ├── communication │ │ │ └── CORSFilter.scala │ │ ├── json │ │ │ └── customfomatters │ │ │ │ └── CustomFormatter.scala │ │ ├── jwt │ │ │ ├── JwtSecret.scala │ │ │ ├── JwtUtil.scala │ │ │ ├── ResponseToken.scala │ │ │ └── TokenPayload.scala │ │ ├── messages │ │ │ ├── Message.scala │ │ │ ├── MessageKey.scala │ │ │ ├── MessageType.java │ │ │ └── Messages.scala │ │ ├── pagination │ │ │ ├── Pagination.scala │ │ │ └── PaginationResult.scala │ │ ├── response │ │ │ ├── GlobalMessagesRestResponse.scala │ │ │ ├── LocalMessagesRestResponse.scala │ │ │ ├── MessagesRestResponse.scala │ │ │ ├── ResponseTools.scala │ │ │ └── RestResponse.scala │ │ ├── service │ │ │ ├── ChildRelationService.scala │ │ │ └── EntityService.scala │ │ ├── slick │ │ │ ├── custommapper │ │ │ │ └── CustomSlickMapper.scala │ │ │ ├── generic │ │ │ │ ├── ChildRelation.scala │ │ │ │ ├── ChildRelationsCruds.scala │ │ │ │ ├── Cruds.scala │ │ │ │ ├── Entity.scala │ │ │ │ └── EntityHelper.scala │ │ │ ├── postgres │ │ │ │ └── MyPostgresDriver.scala │ │ │ └── utils │ │ │ │ └── SlickUtils.scala │ │ └── utils │ │ │ ├── BigDecimalUtils.scala │ │ │ ├── ConfigurationUtils.scala │ │ │ ├── DateUtils.scala │ │ │ ├── HashUtils.scala │ │ │ ├── ReactiveValidateUtils.scala │ │ │ ├── StringUtils.scala │ │ │ └── ValidateUtils.scala │ │ └── module │ │ ├── actor │ │ └── actionlog │ │ │ └── ActionLogActor.scala │ │ ├── dao │ │ ├── actionlog │ │ │ └── sql │ │ │ │ └── ActionLogMapper.scala │ │ └── user │ │ │ ├── UserRepository.scala │ │ │ └── sql │ │ │ ├── UserEntityMapper.scala │ │ │ └── UserRepositoryImpl.scala │ │ ├── domain │ │ ├── actionlog │ │ │ ├── ActionDomainType.java │ │ │ ├── ActionLogEntity.scala │ │ │ └── ActionType.java │ │ ├── authentication │ │ │ └── AuthenticationConfiguration.scala │ │ └── user │ │ │ ├── Contact.scala │ │ │ ├── Gender.java │ │ │ ├── SystemUser.scala │ │ │ ├── User.scala │ │ │ ├── UserCreateModel.scala │ │ │ ├── UserRole.java │ │ │ └── UserStatus.java │ │ ├── service │ │ └── domain │ │ │ ├── authentication │ │ │ ├── AuthenticationService.scala │ │ │ └── AuthenticationServiceImpl.scala │ │ │ └── user │ │ │ ├── UserDomainService.scala │ │ │ └── UserDomainServiceImpl.scala │ │ └── validation │ │ └── user │ │ └── UserCreateValidator.scala └── controllers │ ├── AuthenticationController.scala │ ├── api │ └── v1 │ │ └── UserController.scala │ └── core │ └── SecuredController.scala ├── build.sbt ├── conf ├── api.v1.routes ├── application.conf ├── db │ └── migration │ │ └── default │ │ ├── V1_1__Create_users.sql │ │ └── V1_2__Action_log.sql ├── logback.xml └── routes ├── db_init └── Inital_db_creation_script.sql ├── project ├── build.properties └── plugins.sbt ├── public ├── images │ └── favicon.png ├── javascripts │ └── hello.js └── stylesheets │ └── main.css └── tutorial └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | .sbtserver/* 10 | project/.sbtserver 11 | project/.sbtserver.lock 12 | project/play-fork-run.sbt 13 | project/sbt-ui.sbt 14 | /*.iml 15 | /out 16 | /.idea_modules 17 | /.classpath 18 | /.project 19 | /RUNNING_PID 20 | /.settings 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Josip Medic 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: target/universal/stage/bin/luxury-akka -Dhttp.port=$PORT -DapplyEvolutions.default=true -Ddb.default.driver=org.postgresql.Driver -Ddb.default.url=${DATABASE_URL} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | restful scala reactive backend 2 | ====================== 3 | 4 | DDD DOMAIN VALIDATION: http://codemozzer.me/domain,validation,action,composable,messages/2015/09/26/domain_validation.html 5 | 6 | This is rest application seed using Scala 2.11.8, PlayFramework 2.5.13, Slick 3.2, Postgres 9.6, Akka Actors, FlyWay DB migrations, JWT Token Authentication and Deep Domain validation 7 | 8 | - introduced working generic and reusable Slick Repository 9 | - introduced generic domain service 10 | - Seed is having multiple developed tools for deep domain validation. Deep Domain Validation is representing custom validation of Domain (or any others) objects that have simple or complex validation rules, from simple ones like notNullNorEmpty, lengthIsBiggerOrEqualTo, validEmail to complex ones like unique in DB some other complex dependencies between objects. It is providing simple solution of how to write structured ItemValidators[T] . 11 | - Domain Validation is populating `Messages` which can have INFO, WARNING or ERROR messages, which can later be presented to API user, also populated `Messages` can be used to decide what to do if i.e. WARNING is present, then we can decide to go in some direction like retry our attempt, or if ERROR is present then we will revert multiple actions. 12 | - in application is implementing deep validation where all ERRORS, WARNING and INFO messages are collected and returned in unified response 13 | 14 | - all rest responses are unified and response has same structure every time so it is easier to handle errors, warning and information messages and also it is easier to handle specific data on pages. 15 | Response is structured to have GLOBAL and LOCAL messages. LOCAL messages are messages that are coupled to some field i.e. = "username is too log. Allowed length is 80 chars". Global messages are messages that are reflecting state of whole data on page, i.e. "User will not be active until is approved". Local and Global messages are having three levels: ERROR, WARNING and INFORMATION. 16 | example response: 17 | 18 | - GLOBAL messages: 19 | 20 | ```json 21 | { 22 | "messages" : { 23 | "global" : { 24 | "info": ["User successfully created."], 25 | "warnings": ["User will not be available for login until is activated"], 26 | "errors": [] 27 | }, 28 | "local" : [] 29 | }, 30 | "data":{ 31 | "id": 2, 32 | "firstName": "Mister", 33 | "lastName": "Sir", 34 | "username": "mistersir", 35 | "email": "mistersir@example.com" 36 | } 37 | } 38 | ``` 39 | 40 | - LOCAL messages: 41 | 42 | ```json 43 | { 44 | "messages" : { 45 | "global" : { 46 | "info": [], 47 | "warnings": [], 48 | "errors": [] 49 | }, 50 | "local" : [ 51 | { 52 | "formId" : "username", 53 | "errors" : ["User with this username already exists."], 54 | "warnings" : [], 55 | "info" : [] 56 | } 57 | ] 58 | }, 59 | "data":{ 60 | "id": 2, 61 | "firstName": "Mister", 62 | "lastName": "Sir", 63 | "username": "mistersir", 64 | "email": "mistersir@example.com" 65 | } 66 | } 67 | ``` 68 | 69 | - JSON Web Tokens (JWT) is used for user identification and authentication 70 | 71 | - application is divided into modules i.e. user module, user module etc. Each module have dao, domain, validation, service packages. 72 | 73 | - Database migrations: 74 | - in `db_init` directory is initial postgres script that will create database `luxuryakka` with user `luxuryakka` and password `luxuryakka` . That can be easily done manually. 75 | - when application is started db migrations are available on : `http://localhost:9000/@flyway/default` where pending migrations can be applied as described here: [Play FlayWay](https://github.com/flyway/flyway-play) 76 | 77 | -------------------------------------------------------------------------------- /activator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### ------------------------------- ### 4 | ### Helper methods for BASH scripts ### 5 | ### ------------------------------- ### 6 | 7 | realpath () { 8 | ( 9 | TARGET_FILE="$1" 10 | 11 | cd $(dirname "$TARGET_FILE") 12 | TARGET_FILE=$(basename "$TARGET_FILE") 13 | 14 | COUNT=0 15 | while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] 16 | do 17 | TARGET_FILE=$(readlink "$TARGET_FILE") 18 | cd $(dirname "$TARGET_FILE") 19 | TARGET_FILE=$(basename "$TARGET_FILE") 20 | COUNT=$(($COUNT + 1)) 21 | done 22 | 23 | if [ "$TARGET_FILE" == "." -o "$TARGET_FILE" == ".." ]; then 24 | cd "$TARGET_FILE" 25 | TARGET_FILEPATH= 26 | else 27 | TARGET_FILEPATH=/$TARGET_FILE 28 | fi 29 | 30 | # make sure we grab the actual windows path, instead of cygwin's path. 31 | if ! is_cygwin; then 32 | echo "$(pwd -P)/$TARGET_FILE" 33 | else 34 | echo $(cygwinpath "$(pwd -P)/$TARGET_FILE") 35 | fi 36 | ) 37 | } 38 | 39 | # TODO - Do we need to detect msys? 40 | 41 | # Uses uname to detect if we're in the odd cygwin environment. 42 | is_cygwin() { 43 | local os=$(uname -s) 44 | case "$os" in 45 | CYGWIN*) return 0 ;; 46 | *) return 1 ;; 47 | esac 48 | } 49 | 50 | # This can fix cygwin style /cygdrive paths so we get the 51 | # windows style paths. 52 | cygwinpath() { 53 | local file="$1" 54 | if is_cygwin; then 55 | echo $(cygpath -w $file) 56 | else 57 | echo $file 58 | fi 59 | } 60 | 61 | # Make something URI friendly 62 | make_url() { 63 | url="$1" 64 | local nospaces=${url// /%20} 65 | if is_cygwin; then 66 | echo "/${nospaces//\\//}" 67 | else 68 | echo "$nospaces" 69 | fi 70 | } 71 | 72 | # Detect if we should use JAVA_HOME or just try PATH. 73 | get_java_cmd() { 74 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 75 | echo "$JAVA_HOME/bin/java" 76 | else 77 | echo "java" 78 | fi 79 | } 80 | 81 | echoerr () { 82 | echo 1>&2 "$@" 83 | } 84 | vlog () { 85 | [[ $verbose || $debug ]] && echoerr "$@" 86 | } 87 | dlog () { 88 | [[ $debug ]] && echoerr "$@" 89 | } 90 | execRunner () { 91 | # print the arguments one to a line, quoting any containing spaces 92 | [[ $verbose || $debug ]] && echo "# Executing command line:" && { 93 | for arg; do 94 | if printf "%s\n" "$arg" | grep -q ' '; then 95 | printf "\"%s\"\n" "$arg" 96 | else 97 | printf "%s\n" "$arg" 98 | fi 99 | done 100 | echo "" 101 | } 102 | 103 | exec "$@" 104 | } 105 | addJava () { 106 | dlog "[addJava] arg = '$1'" 107 | java_args=( "${java_args[@]}" "$1" ) 108 | } 109 | addApp () { 110 | dlog "[addApp] arg = '$1'" 111 | sbt_commands=( "${app_commands[@]}" "$1" ) 112 | } 113 | addResidual () { 114 | dlog "[residual] arg = '$1'" 115 | residual_args=( "${residual_args[@]}" "$1" ) 116 | } 117 | addDebugger () { 118 | addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" 119 | } 120 | addConfigOpts () { 121 | dlog "[addConfigOpts] arg = '$*'" 122 | for item in $* 123 | do 124 | addJava "$item" 125 | done 126 | } 127 | # a ham-fisted attempt to move some memory settings in concert 128 | # so they need not be messed around with individually. 129 | get_mem_opts () { 130 | local mem=${1:-1024} 131 | local meta=$(( $mem / 4 )) 132 | (( $meta > 256 )) || meta=256 133 | (( $meta < 1024 )) || meta=1024 134 | 135 | # default is to set memory options but this can be overridden by code section below 136 | memopts="-Xms${mem}m -Xmx${mem}m" 137 | if [[ "${java_version}" > "1.8" ]]; then 138 | extmemopts="-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=${meta}m" 139 | else 140 | extmemopts="-XX:PermSize=64m -XX:MaxPermSize=${meta}m" 141 | fi 142 | 143 | if [[ "${java_opts}" == *-Xmx* ]] || [[ "${java_opts}" == *-Xms* ]] || [[ "${java_opts}" == *-XX:MaxPermSize* ]] || [[ "${java_opts}" == *-XX:ReservedCodeCacheSize* ]] || [[ "${java_opts}" == *-XX:MaxMetaspaceSize* ]]; then 144 | # if we detect any of these settings in ${java_opts} we need to NOT output our settings. 145 | # The reason is the Xms/Xmx, if they don't line up, cause errors. 146 | memopts="" 147 | extmemopts="" 148 | fi 149 | 150 | echo "${memopts} ${extmemopts}" 151 | } 152 | require_arg () { 153 | local type="$1" 154 | local opt="$2" 155 | local arg="$3" 156 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 157 | die "$opt requires <$type> argument" 158 | fi 159 | } 160 | require_arg () { 161 | local type="$1" 162 | local opt="$2" 163 | local arg="$3" 164 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 165 | die "$opt requires <$type> argument" 166 | fi 167 | } 168 | is_function_defined() { 169 | declare -f "$1" > /dev/null 170 | } 171 | 172 | # If we're *not* running in a terminal, and we don't have any arguments, then we need to add the 'ui' parameter 173 | detect_terminal_for_ui() { 174 | [[ ! -t 0 ]] && [[ "${#residual_args}" == "0" ]] && { 175 | addResidual "ui" 176 | } 177 | # SPECIAL TEST FOR MAC 178 | [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]] && [[ "${#residual_args}" == "0" ]] && { 179 | echo "Detected MAC OSX launched script...." 180 | echo "Swapping to UI" 181 | addResidual "ui" 182 | } 183 | } 184 | 185 | # Processes incoming arguments and places them in appropriate global variables. called by the run method. 186 | process_args () { 187 | while [[ $# -gt 0 ]]; do 188 | case "$1" in 189 | -h|-help) usage; exit 1 ;; 190 | -v|-verbose) verbose=1 && shift ;; 191 | -d|-debug) debug=1 && shift ;; 192 | -mem) require_arg integer "$1" "$2" && app_mem="$2" && shift 2 ;; 193 | -jvm-debug) [[ "$2" =~ ^[0-9]+$ ]] && addDebugger "$2" && shift || addDebugger 9999 && shift ;; 194 | -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; 195 | -D*) addJava "$1" && shift ;; 196 | -J*) addJava "${1:2}" && shift ;; 197 | *) addResidual "$1" && shift ;; 198 | esac 199 | done 200 | 201 | is_function_defined process_my_args && { 202 | myargs=("${residual_args[@]}") 203 | residual_args=() 204 | process_my_args "${myargs[@]}" 205 | } 206 | } 207 | 208 | # Actually runs the script. 209 | run() { 210 | # TODO - check for sane environment 211 | 212 | # process the combined args, then reset "$@" to the residuals 213 | process_args "$@" 214 | detect_terminal_for_ui 215 | set -- "${residual_args[@]}" 216 | argumentCount=$# 217 | 218 | #check for jline terminal fixes on cygwin 219 | if is_cygwin; then 220 | stty -icanon min 1 -echo > /dev/null 2>&1 221 | addJava "-Djline.terminal=jline.UnixTerminal" 222 | addJava "-Dsbt.cygwin=true" 223 | fi 224 | 225 | # run sbt 226 | execRunner "$java_cmd" \ 227 | "-Dactivator.home=$(make_url "$activator_home")" \ 228 | $(get_mem_opts $app_mem) \ 229 | ${java_opts[@]} \ 230 | ${java_args[@]} \ 231 | -jar "$app_launcher" \ 232 | "${app_commands[@]}" \ 233 | "${residual_args[@]}" 234 | 235 | local exit_code=$? 236 | if is_cygwin; then 237 | stty icanon echo > /dev/null 2>&1 238 | fi 239 | exit $exit_code 240 | } 241 | 242 | # Loads a configuration file full of default command line options for this script. 243 | loadConfigFile() { 244 | cat "$1" | sed '/^\#/d' 245 | } 246 | 247 | ### ------------------------------- ### 248 | ### Start of customized settings ### 249 | ### ------------------------------- ### 250 | usage() { 251 | cat < [options] 253 | 254 | Command: 255 | ui Start the Activator UI 256 | new [name] [template-id] Create a new project with [name] using template [template-id] 257 | list-templates Print all available template names 258 | -h | -help Print this message 259 | 260 | Options: 261 | -v | -verbose Make this runner chattier 262 | -d | -debug Set sbt log level to debug 263 | -mem Set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) 264 | -jvm-debug Turn on JVM debugging, open at the given port. 265 | 266 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 267 | -java-home Alternate JAVA_HOME 268 | 269 | # jvm options and output control 270 | -Dkey=val Pass -Dkey=val directly to the java runtime 271 | -J-X Pass option -X directly to the java runtime 272 | (-J is stripped) 273 | 274 | # environment variables (read from context) 275 | JAVA_OPTS Environment variable, if unset uses "" 276 | SBT_OPTS Environment variable, if unset uses "" 277 | ACTIVATOR_OPTS Environment variable, if unset uses "" 278 | 279 | In the case of duplicated or conflicting options, the order above 280 | shows precedence: environment variables lowest, command line options highest. 281 | EOM 282 | } 283 | 284 | ### ------------------------------- ### 285 | ### Main script ### 286 | ### ------------------------------- ### 287 | 288 | declare -a residual_args 289 | declare -a java_args 290 | declare -a app_commands 291 | declare -r real_script_path="$(realpath "$0")" 292 | declare -r activator_home="$(realpath "$(dirname "$real_script_path")")" 293 | declare -r app_version="1.2.3" 294 | 295 | declare -r app_launcher="${activator_home}/activator-launch-${app_version}.jar" 296 | declare -r script_name=activator 297 | declare -r java_cmd=$(get_java_cmd) 298 | declare -r java_opts=( "${ACTIVATOR_OPTS[@]}" "${SBT_OPTS[@]}" "${JAVA_OPTS[@]}" "${java_opts[@]}" ) 299 | userhome="$HOME" 300 | if is_cygwin; then 301 | # cygwin sets home to something f-d up, set to real windows homedir 302 | userhome="$USERPROFILE" 303 | fi 304 | declare -r activator_user_home_dir="${userhome}/.activator" 305 | declare -r java_opts_config_home="${activator_user_home_dir}/activatorconfig.txt" 306 | declare -r java_opts_config_version="${activator_user_home_dir}/${app_version}/activatorconfig.txt" 307 | 308 | # Now check to see if it's a good enough version 309 | declare -r java_version=$("$java_cmd" -version 2>&1 | awk -F '"' '/version/ {print $2}') 310 | if [[ "$java_version" == "" ]]; then 311 | echo 312 | echo No java installations was detected. 313 | echo Please go to http://www.java.com/getjava/ and download 314 | echo 315 | exit 1 316 | elif [[ ! "$java_version" > "1.6" ]]; then 317 | echo 318 | echo The java installation you have is not up to date 319 | echo Activator requires at least version 1.6+, you have 320 | echo version $java_version 321 | echo 322 | echo Please go to http://www.java.com/getjava/ and download 323 | echo a valid Java Runtime and install before running Activator. 324 | echo 325 | exit 1 326 | fi 327 | 328 | # if configuration files exist, prepend their contents to the java args so it can be processed by this runner 329 | # a "versioned" config trumps one on the top level 330 | if [[ -f "$java_opts_config_version" ]]; then 331 | addConfigOpts $(loadConfigFile "$java_opts_config_version") 332 | elif [[ -f "$java_opts_config_home" ]]; then 333 | addConfigOpts $(loadConfigFile "$java_opts_config_home") 334 | fi 335 | 336 | run "$@" 337 | -------------------------------------------------------------------------------- /activator-launch-1.2.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-josip/reactive-play-scala-akka-slick-guice-domain_validation-seed/253aae3ddb4587084f658767f6161857907244f6/activator-launch-1.2.3.jar -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=reactive-play-scala-akka-slick-guice-domain_validation-seed 2 | title=Reactive Play, Scala, Slick, Generic Slick repository, Akka, Flyway, JWT Token auth, Guice DI, DDD validation seed 3 | description=Generic Slick repository, Generic Service, DDD DOMAIN VALIDATION: http://codemozzer.me/domain,validation,action,composable,messages/2015/09/26/domain_validation.html Description: A restful Scala Play framework 2.5.13 application using Akka, Reactive Slick 3.2, Akka Actors for ActionLogs, JWT Token user identification and authentication, DDD domain validation, Guice Dependency Injection 4 | tags=akka,scala,slick,reactive,domain,validation,rest,seed,api,DDD,token,flyway 5 | authorName=codemozzer 6 | authorLink=http://codemozzer.me 7 | authorTwitter=cromozzer 8 | authorBio=Engineer 9 | authorLogo=https://s.gravatar.com/avatar/d2c32f31f601545292b7efa27d3d0613?s=80 10 | -------------------------------------------------------------------------------- /app/ApplicationLifecycleModule.scala: -------------------------------------------------------------------------------- 1 | import scala.concurrent.Future 2 | import javax.inject._ 3 | 4 | import akka.actor.ActorSystem 5 | import play.api.inject.ApplicationLifecycle 6 | 7 | @Singleton 8 | class ApplicationLifecycleModule @Inject() ( 9 | lifecycle: ApplicationLifecycle, 10 | actorSystem: ActorSystem 11 | ) { 12 | 13 | lifecycle.addStopHook { () => 14 | Future.successful({ 15 | actorSystem.terminate() 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filters.scala: -------------------------------------------------------------------------------- 1 | import javax.inject.{Inject, Singleton} 2 | 3 | import play.api.http.HttpFilters 4 | import play.filters.cors.CORSFilter 5 | 6 | @Singleton 7 | class Filters @Inject() ( 8 | corsFilter: CORSFilter 9 | 10 | ) extends HttpFilters { 11 | override val filters = Seq(corsFilter) 12 | } 13 | -------------------------------------------------------------------------------- /app/Module.scala: -------------------------------------------------------------------------------- 1 | import java.util.TimeZone 2 | import javax.inject.Inject 3 | 4 | import com.google.inject.AbstractModule 5 | import org.joda.time.DateTimeZone 6 | import play.api.{Configuration, Environment} 7 | 8 | class Module @Inject() ( 9 | environment: Environment, 10 | configuration: Configuration 11 | ) extends AbstractModule { 12 | override def configure(): Unit = { 13 | // bind as singelton configuration parts 14 | bind(classOf[ApplicationLifecycleModule]).asEagerSingleton() 15 | 16 | // set all dates to be default UTC timezone 17 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")) 18 | DateTimeZone.setDefault(DateTimeZone.UTC) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/configuration/AuthenticationConfigurationSetup.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.configuration 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.josip.reactiveluxury.core.utils.ConfigurationUtils 6 | import com.josip.reactiveluxury.module.domain.authentication.AuthenticationConfiguration 7 | 8 | @Singleton() 9 | class AuthenticationConfigurationSetup @Inject() (configurationUtils: ConfigurationUtils) extends AuthenticationConfiguration( 10 | secret = configurationUtils.getConfigurationByKey[String]("jwt.token.secret"), 11 | tokenHoursToLive = configurationUtils.getConfigurationByKey[Int]("jwt.token.hoursToLive") 12 | ) 13 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/configuration/CustomExecutionContext.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.configuration 2 | 3 | import play.api.Logger 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | object CustomExecutionContext { 8 | 9 | // implicit lazy val customForkJoinPoolContext = createExecutionContext() 10 | 11 | implicit class ErrorMessageFuture[A](val future: Future[A]) extends AnyVal { 12 | def handleError(error: String = "Exception occurred.")(implicit ec : ExecutionContext): Future[A] = future.recoverWith { 13 | case t: Throwable => 14 | Logger.logger.error(error, t) 15 | Future.failed(new RuntimeException(error, t)) 16 | } 17 | } 18 | 19 | // protected def createExecutionContext(): ExecutionContextExecutorService = { 20 | // class NamedFjpThread(fjp: ForkJoinPool) extends ForkJoinWorkerThread(fjp) 21 | // 22 | // /** 23 | // * A named thread factory for the scala fjp as distinct from the Java one. 24 | // */ 25 | // case class NamedFjpThreadFactory(name: String) extends ForkJoinWorkerThreadFactory { 26 | // val threadNo = new AtomicInteger() 27 | // val backingThreadFactory = Executors.defaultThreadFactory() 28 | // 29 | // def newThread(fjp: ForkJoinPool) = { 30 | // val thread = new NamedFjpThread(fjp) 31 | // thread.setName(name + "-" + threadNo.incrementAndGet()) 32 | // thread 33 | // } 34 | // } 35 | // 36 | // def loggerReporter(throwable: Throwable): Unit = { 37 | // Logger.logger.error("Exception occurred.", throwable) 38 | // } 39 | // 40 | // val uncaughtExceptionHandler: Thread.UncaughtExceptionHandler = new Thread.UncaughtExceptionHandler { 41 | // def uncaughtException(thread: Thread, cause: Throwable): Unit = loggerReporter(cause) 42 | // } 43 | // 44 | // val numberOfThreads = Runtime.getRuntime.availableProcessors 45 | // val service = new ForkJoinPool( 46 | // numberOfThreads, 47 | // NamedFjpThreadFactory("custom-execution-context"), 48 | // uncaughtExceptionHandler, 49 | // true 50 | // ) 51 | // 52 | // ExecutionContext.fromExecutorService(service) 53 | // } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/configuration/DatabaseProvider.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.configuration 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.db.DBApi 6 | import slick.jdbc.PostgresProfile.api._ 7 | 8 | @Singleton() 9 | class DatabaseProvider @Inject() (dbApi: DBApi) { 10 | lazy val db = Database.forDataSource(dbApi.database("default").dataSource, None) 11 | } 12 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/configuration/actor/ActorFactory.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.configuration.actor 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.{ActorSystem, Props} 6 | import akka.routing.FromConfig 7 | import com.josip.reactiveluxury.configuration.DatabaseProvider 8 | import com.josip.reactiveluxury.module.actor.actionlog.ActionLogActor 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | @Singleton() 13 | class ActorFactory @Inject() (databaseProvider: DatabaseProvider, actorSystem: ActorSystem, implicit val ec : ExecutionContext) { 14 | actorSystem.actorOf(Props(new ActionLogActor(databaseProvider, ec)).withRouter(FromConfig()), name = ActionLogActor.NAME) 15 | } 16 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/Asserts.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core 2 | 3 | object Asserts 4 | { 5 | def argumentIsNotNull[T](arg: T) 6 | { 7 | if (arg == null) 8 | throw new IllegalArgumentException(ExceptionMessages.NULL_ARGUMENT_PASSED) 9 | } 10 | 11 | def argumentIsNotNull[T](arg: T, argName: String) 12 | { 13 | if(argName == null){ 14 | throw new IllegalArgumentException(ExceptionMessages.ARGUMENT_NAME_MUST_NOT_BE_NULL) 15 | } 16 | if(argName.isEmpty){ 17 | throw new IllegalArgumentException(ExceptionMessages.ARGUMENT_NAME_MUST_NOT_BE_EMPTY) 18 | } 19 | 20 | if (arg == null) 21 | throw new IllegalArgumentException(ExceptionMessages.NULL_ARGUMENT_PASSED_ARGUMENT_NAME_IS + argName) 22 | } 23 | 24 | def argumentIsTrue(arg: Boolean) 25 | { 26 | if (!arg) 27 | throw new IllegalArgumentException(ExceptionMessages.ARGUMENT_MUST_BE_TRUE) 28 | } 29 | 30 | def argumentIsTrue(arg: Boolean, message: String) 31 | { 32 | argumentIsNotNullNorEmpty(message) 33 | 34 | if (!arg) 35 | throw new IllegalArgumentException(message) 36 | } 37 | 38 | def argumentIsNotNullNorEmpty(arg: String) 39 | { 40 | if (arg == null ) 41 | throw new IllegalArgumentException(ExceptionMessages.NULL_ARGUMENT_PASSED) 42 | if (arg.isEmpty) 43 | throw new IllegalArgumentException(ExceptionMessages.EMPTY_ARGUMENT_PASSED) 44 | } 45 | 46 | def argumentIsNotNullNorEmpty(arg: String, description: => String) 47 | { 48 | if(description == null){ 49 | throw new IllegalArgumentException(ExceptionMessages.DESCRIPTION_MUST_NOT_BE_NULL) 50 | } 51 | if(description.isEmpty){ 52 | throw new IllegalArgumentException(ExceptionMessages.DESCRIPTION_MUST_NOT_BE_EMPTY) 53 | } 54 | 55 | if (arg == null ) 56 | throw new IllegalArgumentException(ExceptionMessages.NULL_ARGUMENT_PASSED + addDescription(description)) 57 | if (arg.isEmpty) 58 | throw new IllegalArgumentException(ExceptionMessages.EMPTY_ARGUMENT_PASSED + addDescription(description)) 59 | } 60 | 61 | private object ExceptionMessages 62 | { 63 | val NULL_ARGUMENT_PASSED = "Null argument passed!" 64 | val NULL_ARGUMENT_PASSED_ARGUMENT_NAME_IS = "Null argument passed! Argument name:" 65 | val EMPTY_ARGUMENT_PASSED = "Empty argument passed!" 66 | val ARGUMENT_NAME_MUST_NOT_BE_NULL = "Argument name must not be null" 67 | val ARGUMENT_NAME_MUST_NOT_BE_EMPTY = "Argument name must not be empty" 68 | val ARGUMENT_MUST_BE_TRUE = "Argument must be true!" 69 | val DESCRIPTION_MUST_NOT_BE_NULL = "Description must not be null" 70 | val DESCRIPTION_MUST_NOT_BE_EMPTY = "Description must not be empty" 71 | } 72 | 73 | private def addDescription(description: => String): String = 74 | { 75 | String.format(" (%s)", description) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/Converter.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core 2 | 3 | trait Converter[TIn, TOut] 4 | { 5 | def convert(in :TIn): TOut 6 | } 7 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/Field.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core 2 | 3 | import com.josip.reactiveluxury.core.messages.{Messages, MessageKey} 4 | import com.josip.reactiveluxury.core.utils.{StringUtils, ValidateUtils} 5 | import Asserts._ 6 | import com.josip.reactiveluxury.core.messages.MessageKey 7 | import com.josip.reactiveluxury.core.utils.ValidateUtils 8 | import StringUtils._ 9 | import org.joda.time.format.DateTimeFormatter 10 | import org.joda.time.{DateTime, DateTimeZone, LocalDate} 11 | import play.api.libs.json.JsValue 12 | 13 | import scala.language.existentials 14 | 15 | sealed abstract class Field[TField](key: String, val evaluateBindHelper: (MessageKey, String, Messages) => Unit) 16 | { 17 | argumentIsNotNullNorEmpty(key) 18 | argumentIsNotNull(evaluateBindHelper) 19 | 20 | final val messageKey = MessageKey(key) 21 | 22 | def bind(valueAsString: Option[String]) : TField 23 | def bindJsValue(valueAsJsValue: Option[JsValue]): TField 24 | 25 | def evaluateBind(valueAsString: Option[String], messages: Messages) 26 | def evaluateBindJsValue(valueAsJsValue: Option[JsValue], messages: Messages) 27 | } 28 | 29 | object Field 30 | { 31 | private[this] final val DEFAULT_TO_STRING_METHOD = (v: Any) => { v.toString } 32 | 33 | def jsValueToString(jsValue: Option[JsValue]): Option[String] = 34 | { 35 | jsValue.map(jv => trimExtraQuotes(jv.toString())) 36 | } 37 | 38 | sealed case class MandatoryField[TField] 39 | ( 40 | key : String, 41 | override val evaluateBindHelper : (MessageKey, String, Messages) => Unit, 42 | bindFromString : (String ) => TField, 43 | toStringMethod : (TField ) => String = DEFAULT_TO_STRING_METHOD 44 | ) extends Field[TField](key, evaluateBindHelper) 45 | { 46 | argumentIsNotNullNorEmpty(key) 47 | argumentIsNotNull(evaluateBindHelper) 48 | argumentIsNotNull(bindFromString) 49 | argumentIsNotNull(toStringMethod) 50 | 51 | override def bind(valueAsString: Option[String]): TField = 52 | { 53 | argumentIsNotNull(valueAsString) 54 | argumentIsTrue(valueAsString.isDefined) 55 | 56 | val localMessages = Messages.of 57 | this.evaluateBind(valueAsString, localMessages) 58 | argumentIsTrue(!localMessages.hasErrors) 59 | 60 | bindFromString(valueAsString.get) 61 | } 62 | 63 | override def bindJsValue(valueAsJsValue: Option[JsValue]): TField = 64 | { 65 | argumentIsNotNull(valueAsJsValue) 66 | argumentIsTrue(valueAsJsValue.isDefined) 67 | 68 | this.bind(Field.jsValueToString(valueAsJsValue)) 69 | } 70 | 71 | override def evaluateBind(valueAsString: Option[String], messages: Messages) 72 | { 73 | argumentIsNotNull(valueAsString) 74 | argumentIsNotNull(messages) 75 | 76 | val localMessages = Messages.of(messages) 77 | 78 | ValidateUtils.validateIsOptionDefined(messageKey, valueAsString, localMessages) 79 | if(!localMessages.hasErrors) 80 | { 81 | this.evaluateBindHelper(messageKey, valueAsString.get, messages) 82 | } 83 | } 84 | 85 | override def evaluateBindJsValue(valueAsJsValue: Option[JsValue], messages: Messages) 86 | { 87 | argumentIsNotNull(valueAsJsValue) 88 | argumentIsNotNull(messages) 89 | 90 | this.evaluateBind( 91 | valueAsString = Field.jsValueToString(valueAsJsValue), 92 | messages = messages 93 | ) 94 | } 95 | } 96 | object MandatoryField 97 | { 98 | def of[TField] 99 | ( 100 | key : String, 101 | evaluateBindFromString : (MessageKey, String, Messages) => Unit, 102 | bindFromString : (String) => TField, 103 | toStringMethod : TField => String 104 | ): MandatoryField[TField] = 105 | { 106 | argumentIsNotNullNorEmpty(key) 107 | argumentIsNotNull(evaluateBindFromString) 108 | argumentIsNotNull(bindFromString) 109 | argumentIsNotNull(toStringMethod) 110 | 111 | MandatoryField( 112 | key = key, 113 | evaluateBindHelper = evaluateBindFromString, 114 | bindFromString = bindFromString, 115 | toStringMethod = toStringMethod 116 | ) 117 | } 118 | 119 | def of[TField] 120 | ( 121 | key : String, 122 | evaluateBindFromString : (MessageKey, String, Messages) => Unit, 123 | bindFromString : (String) => TField 124 | ): MandatoryField[TField] = 125 | { 126 | argumentIsNotNullNorEmpty(key) 127 | argumentIsNotNull(evaluateBindFromString) 128 | argumentIsNotNull(bindFromString) 129 | 130 | MandatoryField( 131 | key = key, 132 | evaluateBindHelper = evaluateBindFromString, 133 | bindFromString = bindFromString 134 | ) 135 | } 136 | } 137 | 138 | sealed case class OptionalField[TField] 139 | ( 140 | field: MandatoryField[TField] 141 | ) extends Field[Option[TField]](field.key, field.evaluateBindHelper) 142 | { 143 | argumentIsNotNull(field) 144 | 145 | override def bind(valueAsString: Option[String]): Option[TField] = 146 | { 147 | argumentIsNotNull(valueAsString) 148 | 149 | if (!valueAsString.isDefined) return None 150 | 151 | val localMessages = Messages.of 152 | this.field.evaluateBind(valueAsString, localMessages) 153 | argumentIsTrue(!localMessages.hasErrors) 154 | 155 | Some(field.bind(valueAsString)) 156 | } 157 | 158 | override def bindJsValue(valueAsJsValue: Option[JsValue]): Option[TField] = 159 | { 160 | argumentIsNotNull(valueAsJsValue) 161 | argumentIsTrue(valueAsJsValue.isDefined) 162 | 163 | this.bind(Field.jsValueToString(valueAsJsValue)) 164 | } 165 | 166 | override def evaluateBind(valueAsString: Option[String], messages: Messages) 167 | { 168 | argumentIsNotNull(valueAsString) 169 | argumentIsNotNull(messages) 170 | 171 | if(valueAsString.isDefined) 172 | { 173 | this.field.evaluateBind(valueAsString, messages) 174 | } 175 | } 176 | 177 | override def evaluateBindJsValue(valueAsJsValue: Option[JsValue], messages: Messages) 178 | { 179 | argumentIsNotNull(valueAsJsValue) 180 | argumentIsNotNull(messages) 181 | 182 | this.evaluateBind( 183 | valueAsString = Field.jsValueToString(valueAsJsValue), 184 | messages = messages 185 | ) 186 | } 187 | } 188 | 189 | def longNumber(fieldKey: String): MandatoryField[Long] = 190 | { 191 | argumentIsNotNullNorEmpty(fieldKey) 192 | 193 | MandatoryField.of[Long](fieldKey, evaluateParsingLong _, parseLong _) 194 | } 195 | 196 | def number(fieldKey: String): MandatoryField[Int] = 197 | { 198 | argumentIsNotNullNorEmpty(fieldKey) 199 | 200 | MandatoryField.of[Int](fieldKey, evaluateParsingInt _, parseInt _) 201 | } 202 | 203 | def boolean(fieldKey: String): MandatoryField[Boolean] = 204 | { 205 | argumentIsNotNullNorEmpty(fieldKey) 206 | 207 | MandatoryField.of[scala.Boolean](fieldKey, evaluateParsingBoolean _, parseBoolean _) 208 | } 209 | 210 | def bigDecimal(fieldKey: String): MandatoryField[BigDecimal] = 211 | { 212 | argumentIsNotNullNorEmpty(fieldKey) 213 | 214 | MandatoryField.of[BigDecimal] (fieldKey, evaluateParsingBigDecimal _, parseBigDecimal _) 215 | } 216 | 217 | def text(key: String): MandatoryField[String] = 218 | { 219 | argumentIsNotNullNorEmpty(key) 220 | 221 | val evaluateParsingString = (k: MessageKey, v: String, m: Messages) => { 222 | argumentIsNotNull(k) 223 | argumentIsNotNull(v) 224 | argumentIsNotNull(m) 225 | } 226 | val parseString = (v: String) => { 227 | argumentIsNotNull(v) 228 | v 229 | } 230 | MandatoryField.of[String](key, evaluateParsingString, parseString) 231 | } 232 | 233 | def nonEmptyText(key: String): MandatoryField[String] = 234 | { 235 | argumentIsNotNullNorEmpty(key) 236 | 237 | val evaluateParsingString = (k: MessageKey, v: String, m: Messages) => { 238 | argumentIsNotNull(k) 239 | argumentIsNotNull(v) 240 | argumentIsNotNull(m) 241 | 242 | ValidateUtils.isNotEmpty(v, m, k.value, "Value must not be empty") 243 | } 244 | val parseString = (v: String) => { 245 | argumentIsNotNull(v) 246 | v 247 | } 248 | MandatoryField.of[String](key, evaluateParsingString, parseString) 249 | } 250 | 251 | def dateTime(key: String)(dateFormatter: DateTimeFormatter): MandatoryField[DateTime] = 252 | { 253 | argumentIsNotNullNorEmpty(key) 254 | argumentIsNotNull(dateFormatter) 255 | 256 | val evaluateParsingMethod = (k: MessageKey, v: String, m: Messages) => { evaluateParsingDateTime(k, v, dateFormatter, m) } 257 | val parseDateTimeMethod = (v: String) => { parseDateTime(v, dateFormatter) } 258 | val toStringMethod = (v: DateTime) => { dateFormatter.print(v) } 259 | MandatoryField.of[DateTime](key, evaluateParsingMethod, parseDateTimeMethod, toStringMethod) 260 | } 261 | 262 | def dateTimeFromMillis(key: String): MandatoryField[DateTime] = 263 | { 264 | argumentIsNotNullNorEmpty(key) 265 | 266 | val evaluateParsingMethod = (k: MessageKey, v: String, m: Messages) => { 267 | val localMessages = Messages.of 268 | StringUtils.evaluateParsingLong(k, v, localMessages) 269 | if (localMessages.hasErrors) 270 | m.putError(k, "invalid time in milliseconds") 271 | } 272 | val parseDateTimeMethod = (v: String) => { new DateTime(StringUtils.parseLong(v), DateTimeZone.UTC) } 273 | val toStringMethod = (v: DateTime) => { v.getMillis.toString } 274 | MandatoryField.of[DateTime](key, evaluateParsingMethod, parseDateTimeMethod, toStringMethod) 275 | } 276 | 277 | def localDate(key: String)(dateFormatter: DateTimeFormatter): MandatoryField[LocalDate] = 278 | { 279 | argumentIsNotNullNorEmpty(key) 280 | argumentIsNotNull(dateFormatter) 281 | 282 | val evaluateParsingMethod = (k: MessageKey, v: String, m: Messages) => { evaluateParsingLocalDate(k, v, dateFormatter, m) } 283 | val parseMethod = (v: String) => { parseLocalDate(v, dateFormatter) } 284 | val toStringMethod = (v: LocalDate) => { dateFormatter.print(v) } 285 | 286 | MandatoryField.of[LocalDate](key, evaluateParsingMethod, parseMethod, toStringMethod) 287 | } 288 | 289 | def optional[TField](field: MandatoryField[TField]): OptionalField[TField] = 290 | { 291 | argumentIsNotNull(field) 292 | 293 | OptionalField(field) 294 | } 295 | } 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/GeneratedId.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core 2 | 3 | case class GeneratedId(id: Long) 4 | { 5 | Asserts.argumentIsTrue(id > 0) 6 | } 7 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/Validator.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core 2 | 3 | import com.josip.reactiveluxury.core.messages.Messages 4 | import com.josip.reactiveluxury.core.response.RestResponse 5 | import com.josip.reactiveluxury.core.response.ResponseTools 6 | import play.api.libs.json.Writes 7 | import scala.concurrent.Future 8 | 9 | trait Validator[T] { 10 | def validate(item:T, userId: Option[Long]) : Future[ValidationResult[T]] 11 | } 12 | 13 | case class ValidationResult[T: Writes]( 14 | validatedItem : T, 15 | messages : Messages 16 | ) { 17 | Asserts.argumentIsNotNull(validatedItem) 18 | Asserts.argumentIsNotNull(messages) 19 | 20 | def isValid: Boolean = { 21 | !this.messages.hasErrors(); 22 | } 23 | 24 | def errorsRestResponse: RestResponse[T] = { 25 | ResponseTools.of( 26 | data = this.validatedItem, 27 | messages = Some(this.messages) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/authentication/Credentials.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.authentication 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.{Json, Format} 5 | 6 | case class Credentials( 7 | email : String, 8 | password : String 9 | ) { 10 | Asserts.argumentIsNotNull(email) 11 | Asserts.argumentIsNotNull(password) 12 | } 13 | 14 | object Credentials { 15 | implicit val jsonFormat : Format[Credentials] = Json.format[Credentials] 16 | } 17 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/communication/CORSFilter.scala: -------------------------------------------------------------------------------- 1 | //package com.josip.reactiveluxury.core.communication 2 | // 3 | //import controllers.Default 4 | //import play.api.{mvc, Logger} 5 | //import play.api.mvc.RequestHeader 6 | //import play.api.mvc.Result 7 | // 8 | //object CORSFilter extends mvc.Filter 9 | //{ 10 | // import scala.concurrent._ 11 | // import ExecutionContext.Implicits.global 12 | // 13 | // lazy val allowedDomain = play.api.Play.current.configuration.getString("cors.allowed.domain") 14 | // 15 | // def isPreFlight(r: RequestHeader) = ( 16 | // r.method.toLowerCase.equals("options") 17 | // && 18 | // r.headers.get("Access-Control-Request-Method").nonEmpty 19 | // ) 20 | // 21 | // def apply(f: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = { 22 | // Logger.trace("[cors] filtering request to add cors") 23 | // if (isPreFlight(request)) { 24 | // Logger.trace("[cors] request is preflight") 25 | // Logger.trace(s"[cors] default allowed domain is $allowedDomain") 26 | // Future.successful(Default.Ok.withHeaders( 27 | // "Access-Control-Allow-Origin" -> allowedDomain.orElse(request.headers.get("Origin")).getOrElse(""), 28 | // "Access-Control-Allow-Methods" -> request.headers.get("Access-Control-Request-Method").getOrElse("*"), 29 | // "Access-Control-Allow-Headers" -> request.headers.get("Access-Control-Request-Headers").getOrElse(""), 30 | // "Access-Control-Allow-Credentials" -> "true" 31 | // )) 32 | // } else { 33 | // Logger.trace("[cors] request is normal") 34 | // Logger.trace(s"[cors] default allowed domain is $allowedDomain") 35 | // f(request).map { 36 | // _.withHeaders( 37 | // "Access-Control-Allow-Origin" -> allowedDomain.orElse(request.headers.get("Origin")).getOrElse(""), 38 | // "Access-Control-Allow-Methods" -> request.headers.get("Access-Control-Request-Method").getOrElse("*"), 39 | // "Access-Control-Allow-Headers" -> request.headers.get("Access-Control-Request-Headers").getOrElse(""), 40 | // "Access-Control-Allow-Credentials" -> "true" 41 | // ) 42 | // } 43 | // } 44 | // } 45 | //} 46 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/json/customfomatters/CustomFormatter.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.json.customfomatters 2 | 3 | import com.josip.reactiveluxury.core.messages.MessageKey 4 | import com.josip.reactiveluxury.core.utils.{DateUtils, StringUtils} 5 | import org.joda.time.{DateTime, LocalDate, Period} 6 | import play.api.data.validation.ValidationError 7 | import play.api.libs.json._ 8 | 9 | object CustomFormatter 10 | { 11 | object Enum { 12 | def enumWritesByName[TEnum <: Enum[TEnum]]= new Writes[TEnum] { 13 | def writes(o: TEnum): JsValue = JsString(o.name) 14 | } 15 | 16 | def enumReadsByName[TEnum <: Enum[TEnum]](implicit classManifest: Manifest[TEnum]) = new Reads[TEnum] { 17 | def reads(json: JsValue) = json match { 18 | case JsString(valueCandidate) => 19 | if (StringUtils.canParseEnum[TEnum](MessageKey("enum"), valueCandidate)) { 20 | JsSuccess(StringUtils.parseEnum(valueCandidate)) 21 | } else { 22 | JsError(Seq(JsPath() -> Seq(ValidationError(s"Not valid '${classManifest.runtimeClass.getSimpleName}' value")))) 23 | } 24 | case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsstring")))) 25 | } 26 | } 27 | 28 | implicit def enumFormatByName[TEnum <: Enum[TEnum]](implicit classManifest: Manifest[TEnum])= new Format[TEnum] { 29 | override def writes(o: TEnum): JsValue = JsString(o.name) 30 | 31 | override def reads(json: JsValue) = json match { 32 | case JsString(valueCandidate) => 33 | if (StringUtils.canParseEnum[TEnum](MessageKey("enum"), valueCandidate)) { 34 | JsSuccess(StringUtils.parseEnum(valueCandidate)) 35 | } else { 36 | JsError(Seq(JsPath() -> Seq(ValidationError(s"Not valid '${classManifest.runtimeClass.getSimpleName}' value")))) 37 | } 38 | case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsstring")))) 39 | } 40 | } 41 | } 42 | 43 | object Joda { 44 | implicit def period = new Format[Period] { 45 | override def writes(p: Period): JsValue = JsString(DateUtils.PERIOD_FORMATTER.print(p)) 46 | 47 | override def reads(json: JsValue) = json match { 48 | case JsString(valueCandidate) => 49 | if (DateUtils.canParsePeriod(valueCandidate)) { 50 | JsSuccess(DateUtils.PERIOD_FORMATTER.parsePeriod(valueCandidate)) 51 | } else { 52 | JsError(Seq(JsPath() -> Seq(ValidationError(s"Not valid joda period value")))) 53 | } 54 | case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsstring")))) 55 | } 56 | } 57 | 58 | implicit def localDate = new Format[LocalDate] { 59 | override def writes(ld: LocalDate): JsValue = JsNumber(ld.toDateTimeAtStartOfDay.getMillis) 60 | 61 | override def reads(json: JsValue) = json match { 62 | case JsNumber(valueCandidate) => 63 | JsSuccess(new LocalDate(valueCandidate.toLong)) 64 | case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsnumber")))) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/jwt/JwtSecret.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.jwt 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.nimbusds.jose.crypto.{MACVerifier, MACSigner} 5 | 6 | case class JwtSecret(secret : String) { 7 | Asserts.argumentIsNotNullNorEmpty(secret) 8 | 9 | val signer = new MACSigner(secret) 10 | val verifier = new MACVerifier(secret) 11 | } 12 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/jwt/JwtUtil.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.jwt 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.{Reads, Writes, JsValue, Json} 5 | import com.nimbusds.jose.{Payload, JWSAlgorithm, JWSHeader, JWSObject} 6 | import scala.concurrent.Future 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | 9 | object JwtUtil { 10 | def signJwtPayload(payload : String)(implicit secret : JwtSecret) : String = { 11 | Asserts.argumentIsNotNullNorEmpty(payload) 12 | Asserts.argumentIsNotNull(secret) 13 | 14 | val jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload(payload)) 15 | jwsObject.sign(secret.signer) 16 | jwsObject.serialize 17 | } 18 | 19 | def signJwtPayload(payload : JsValue)(implicit secret : JwtSecret) : String = { 20 | Asserts.argumentIsNotNull(payload) 21 | Asserts.argumentIsNotNull(secret) 22 | 23 | this.signJwtPayload(payload.toString()) 24 | } 25 | 26 | def signJwtPayload[T](payload : T)(implicit secret : JwtSecret, jsonWrites : Writes[T]) : String = { 27 | Asserts.argumentIsNotNull(payload) 28 | Asserts.argumentIsNotNull(secret) 29 | Asserts.argumentIsNotNull(jsonWrites) 30 | 31 | this.signJwtPayload(Json.toJson(payload)) 32 | } 33 | 34 | def tryGetPayloadStringIfValidToken(token : String)(implicit secret : JwtSecret) : Future[Option[String]] = { 35 | Asserts.argumentIsNotNull(token) 36 | Asserts.argumentIsNotNull(secret) 37 | 38 | try { 39 | val jwsObject = JWSObject.parse(token) 40 | 41 | jwsObject.verify(secret.verifier) match 42 | { 43 | case true => Future.successful(Some(jwsObject.getPayload.toString)) 44 | case false => Future.successful(None) 45 | } 46 | } 47 | catch { 48 | case _ : Throwable => Future.successful(None) 49 | } 50 | } 51 | 52 | def getPayloadIfValidToken[T](token : String)(implicit secret : JwtSecret, jsonWrites : Reads[T]) : Future[Option[T]] = { 53 | Asserts.argumentIsNotNull(token) 54 | Asserts.argumentIsNotNull(secret) 55 | Asserts.argumentIsNotNull(jsonWrites) 56 | 57 | this.tryGetPayloadStringIfValidToken(token).flatMap { 58 | case Some(sv) => Future.successful(Some(Json.parse(sv).as[T])) 59 | case None => Future.successful(None) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/jwt/ResponseToken.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.jwt 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.{Format, Json} 5 | 6 | case class ResponseToken(token: String) { 7 | Asserts.argumentIsNotNullNorEmpty(token) 8 | } 9 | 10 | object ResponseToken { 11 | implicit val jsonFormat : Format[ResponseToken] = Json.format[ResponseToken] 12 | } 13 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/jwt/TokenPayload.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.jwt 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import org.joda.time.DateTime 5 | import play.api.libs.json.{Json, Format} 6 | 7 | case class TokenPayload( 8 | userId : Long, 9 | email : String, 10 | expiration : DateTime 11 | ) { 12 | Asserts.argumentIsNotNull(userId) 13 | Asserts.argumentIsNotNull(email) 14 | Asserts.argumentIsNotNull(expiration) 15 | } 16 | 17 | object TokenPayload 18 | { 19 | implicit val jsonFormat : Format[TokenPayload] = Json.format[TokenPayload] 20 | } -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/messages/Message.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.messages 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.functional.syntax._ 5 | import play.api.libs.json._ 6 | import Asserts._ 7 | 8 | case class Message private 9 | ( 10 | messageType : MessageType, 11 | key : Option[MessageKey], 12 | text : String, 13 | childMessages : Messages 14 | ) 15 | { 16 | argumentIsNotNull(messageType) 17 | argumentIsNotNull(key) 18 | argumentIsNotNullNorEmpty(text) 19 | argumentIsNotNull(childMessages) 20 | } 21 | 22 | object Message { 23 | def information(text: String) = { 24 | argumentIsNotNullNorEmpty(text) 25 | 26 | Message.of(MessageType.INFORMATION, text) 27 | } 28 | 29 | def information(text: String, childMessages: Messages) = { 30 | argumentIsNotNullNorEmpty(text) 31 | argumentIsNotNull(childMessages) 32 | 33 | Message.of(childMessages, MessageType.INFORMATION, text) 34 | } 35 | 36 | def information(key: MessageKey, text: String) = { 37 | argumentIsNotNullNorEmpty(text) 38 | argumentIsNotNull(key) 39 | 40 | Message.of(MessageType.INFORMATION, key, text) 41 | } 42 | 43 | def information(key: MessageKey, text: String, childMessages: Messages) = { 44 | argumentIsNotNull(key) 45 | argumentIsNotNullNorEmpty(text) 46 | argumentIsNotNull(childMessages) 47 | 48 | Message.of(MessageType.INFORMATION, key, text, childMessages) 49 | } 50 | 51 | def warning(text: String) = { 52 | argumentIsNotNullNorEmpty(text) 53 | 54 | Message.of(MessageType.WARNING, text) 55 | } 56 | 57 | def warning(text: String, childMessages: Messages) = { 58 | argumentIsNotNullNorEmpty(text) 59 | argumentIsNotNull(childMessages) 60 | 61 | Message.of(childMessages, MessageType.WARNING, text) 62 | } 63 | 64 | def warning(key: MessageKey, text: String) = { 65 | argumentIsNotNull(key) 66 | argumentIsNotNullNorEmpty(text) 67 | 68 | Message.of(MessageType.WARNING, key, text) 69 | } 70 | 71 | def warning(key: MessageKey, text: String, childMessages: Messages) = { 72 | argumentIsNotNull(key) 73 | argumentIsNotNullNorEmpty(text) 74 | argumentIsNotNull(childMessages) 75 | 76 | Message.of(MessageType.WARNING, key, text, childMessages) 77 | } 78 | 79 | def error(text: String) = { 80 | argumentIsNotNullNorEmpty(text) 81 | 82 | Message.of(MessageType.ERROR, text) 83 | } 84 | 85 | def error(text: String, childMessages: Messages) = { 86 | argumentIsNotNullNorEmpty(text) 87 | argumentIsNotNull(childMessages) 88 | 89 | Message.of(childMessages, MessageType.ERROR, text) 90 | } 91 | 92 | def error(key : MessageKey, text: String) = { 93 | argumentIsNotNullNorEmpty(text) 94 | argumentIsNotNull(key) 95 | 96 | Message.of(MessageType.ERROR, key, text) 97 | } 98 | 99 | def error(key: MessageKey, text: String, childMessages: Messages) = { 100 | argumentIsNotNull(key) 101 | argumentIsNotNullNorEmpty(text) 102 | argumentIsNotNull(childMessages) 103 | 104 | Message.of(MessageType.ERROR, key, text, childMessages) 105 | } 106 | 107 | private def of(messageType: MessageType, text: String) = { 108 | Message(messageType, None, text, Messages.of) 109 | } 110 | private def of(childMessages: Messages, messageType: MessageType, text: String) = { 111 | val childMessagesCopied = Messages.of 112 | childMessagesCopied.putMessages(childMessages) 113 | 114 | Message(messageType, None, text, childMessagesCopied) 115 | } 116 | private def of(messageType: MessageType, key: MessageKey, text: String) = { 117 | Message(messageType, Some(key), text, Messages.of) 118 | } 119 | private def of(messageType: MessageType, key: MessageKey, text: String, childMessages: Messages) = { 120 | Message(messageType, Some(key), text, childMessages) 121 | } 122 | 123 | implicit val MESSAGE_TYPE: Writes[MessageType] = new Writes[MessageType] { 124 | def writes(o: MessageType): JsValue = { 125 | Json.obj( 126 | "Type" -> o.name 127 | ) 128 | } 129 | } 130 | implicit val jsonWrites: Writes[Message] = ( 131 | (__ \ "type") .write[MessageType] and 132 | (__ \ "messageKey") .write[Option[MessageKey]] and 133 | (__ \ "text") .write[String] and 134 | (__ \ "children") .lazyWrite(Messages.jsonWrites) 135 | )(unlift(Message.unapply)) 136 | } 137 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/messages/MessageKey.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.messages 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import Asserts._ 5 | import play.api.libs.json.Json 6 | 7 | case class MessageKey(value: String) { 8 | argumentIsNotNullNorEmpty(value) 9 | } 10 | 11 | object MessageKey { 12 | implicit val jsonWrites = Json.writes[MessageKey] 13 | } 14 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/messages/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.messages; 2 | 3 | public enum MessageType 4 | { 5 | INFORMATION ("Information"), 6 | WARNING ("Warning"), 7 | ERROR ("Error"); 8 | 9 | private MessageType(String displayName) { 10 | this.displayName = displayName; 11 | } 12 | private final String displayName; 13 | 14 | public String displayName() { 15 | return this.displayName; 16 | } 17 | 18 | public boolean isInformation() { 19 | return this == MessageType.INFORMATION; 20 | } 21 | 22 | public boolean isWarning() { 23 | return this == MessageType.WARNING; 24 | } 25 | 26 | public boolean isError() { 27 | return this == MessageType.ERROR; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/messages/Messages.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.messages 2 | 3 | import com.google.common.base.Objects 4 | import com.josip.reactiveluxury.core.Asserts 5 | import play.api.libs.json.{JsValue, Json, Writes} 6 | import Asserts._ 7 | import scala.collection.mutable.{ListBuffer => MutebleList} 8 | 9 | case class Messages private 10 | ( 11 | private val parentMessages: Option[Messages] 12 | ) 13 | { 14 | argumentIsNotNull(parentMessages) 15 | 16 | private val _messages : MutebleList[Message] = MutebleList.empty[Message] 17 | 18 | def putMessage(message: Message) { 19 | argumentIsNotNull(message) 20 | 21 | _messages.synchronized { 22 | _messages += message 23 | 24 | if(parentMessages.isDefined) { 25 | parentMessages.get.putMessage(message) 26 | } 27 | } 28 | } 29 | 30 | def putMessages(messages: Messages) { 31 | argumentIsNotNull(messages) 32 | 33 | _messages.synchronized { 34 | _messages ++= messages.messages 35 | 36 | if(parentMessages.isDefined) { 37 | parentMessages.get.putMessages(messages) 38 | } 39 | } 40 | } 41 | 42 | def putInformation(text: String) = { 43 | argumentIsNotNullNorEmpty(text) 44 | 45 | this.putMessage(Message.information(text)) 46 | } 47 | def putInformation(key: MessageKey, text: String) = { 48 | argumentIsNotNull(key) 49 | argumentIsNotNullNorEmpty(text) 50 | 51 | this.putMessage(Message.information(key, text)) 52 | } 53 | def putInformation(text: String, childMessages: Messages) = { 54 | argumentIsNotNullNorEmpty(text) 55 | argumentIsNotNull(childMessages) 56 | 57 | this.putMessage(Message.information(text, childMessages)) 58 | } 59 | 60 | def putInformation(key: MessageKey, text: String, childMessages: Messages) = { 61 | argumentIsNotNullNorEmpty(text) 62 | argumentIsNotNull(key) 63 | argumentIsNotNull(childMessages) 64 | 65 | this.putMessage(Message.information(key, text, childMessages)) 66 | } 67 | 68 | def putWarning(text: String) = { 69 | argumentIsNotNullNorEmpty(text) 70 | 71 | this.putMessage(Message.warning(text)) 72 | } 73 | def putWarning(key: MessageKey, text: String) = { 74 | argumentIsNotNull(key) 75 | argumentIsNotNullNorEmpty(text) 76 | 77 | this.putMessage(Message.warning(key, text)) 78 | } 79 | def putWarning(text: String, childMessages: Messages) = { 80 | argumentIsNotNullNorEmpty(text) 81 | argumentIsNotNull(childMessages) 82 | 83 | this.putMessage(Message.warning(text, childMessages)) 84 | } 85 | 86 | def putWarning(key: MessageKey, text: String, childMessages: Messages) = { 87 | argumentIsNotNullNorEmpty(text) 88 | argumentIsNotNull(key) 89 | argumentIsNotNull(childMessages) 90 | 91 | this.putMessage(Message.warning(key, text, childMessages)) 92 | } 93 | 94 | def putError(text: String) = { 95 | argumentIsNotNullNorEmpty(text) 96 | 97 | this.putMessage(Message.error(text)) 98 | } 99 | def putError(key: MessageKey, text: String) = { 100 | argumentIsNotNullNorEmpty(text) 101 | argumentIsNotNull(key) 102 | 103 | this.putMessage(Message.error(key, text)) 104 | } 105 | def putError(text: String, childMessages: Messages) = { 106 | argumentIsNotNullNorEmpty(text) 107 | argumentIsNotNull(childMessages) 108 | 109 | this.putMessage(Message.error(text, childMessages)) 110 | } 111 | 112 | def putError(key: MessageKey, text: String, childMessages: Messages) = { 113 | argumentIsNotNullNorEmpty(text) 114 | argumentIsNotNull(key) 115 | argumentIsNotNull(childMessages) 116 | 117 | this.putMessage(Message.error(key, text, childMessages)) 118 | } 119 | 120 | def hasAnyMessageWith(messageType: MessageType): Boolean = { 121 | argumentIsNotNull(messageType) 122 | 123 | messages.foreach(message => { 124 | if( isMessageWantedType(message, messageType)){ return true } 125 | }) 126 | 127 | false 128 | } 129 | 130 | def hasInformation() = { 131 | this.hasAnyMessageWith(MessageType.INFORMATION) 132 | } 133 | 134 | def hasWarnings() = { 135 | this.hasAnyMessageWith(MessageType.WARNING) 136 | } 137 | 138 | def hasErrors() = { 139 | this.hasAnyMessageWith(MessageType.ERROR) 140 | } 141 | 142 | def hasAnyErrorsWithMessageKey(messageKey: MessageKey): Boolean = { 143 | val errorsWithGivenKey = this.errors().filter(_.key.isDefined).filter(_.key.get.equals(messageKey)) 144 | errorsWithGivenKey.nonEmpty 145 | } 146 | 147 | def messages = { 148 | _messages.toSeq 149 | } 150 | 151 | def messages(messageType: MessageType) :Seq[Message] = { 152 | argumentIsNotNull(messageType) 153 | 154 | filterMessages(this.messages, Seq(messageType)) 155 | } 156 | 157 | def information() = { 158 | this.messages(MessageType.INFORMATION) 159 | } 160 | 161 | def warnings() = { 162 | this.messages(MessageType.WARNING) 163 | } 164 | 165 | def errors() = { 166 | this.messages(MessageType.ERROR) 167 | } 168 | 169 | def bindMessages = { 170 | this.messages.filter(_.key.isDefined) 171 | } 172 | 173 | def bindInformation() = { 174 | filterMessages(this.bindMessages, Seq(MessageType.INFORMATION)) 175 | } 176 | 177 | def bindWarnings() = { 178 | filterMessages(this.bindMessages, Seq(MessageType.WARNING)) 179 | } 180 | 181 | def bindErrors() = { 182 | filterMessages(this.bindMessages, Seq(MessageType.ERROR)) 183 | } 184 | 185 | private def filterMessages(messages: Seq[Message], messageTypes: Seq[MessageType]) = { 186 | messages.filter(message => { 187 | isMessagesInWantedTypes(message, messageTypes) 188 | }) 189 | } 190 | 191 | private def isMessagesInWantedTypes(message: Message, messageTypes: Seq[MessageType]) = { 192 | val filteredTypes = messageTypes.filter(messageType => isMessageWantedType(message, messageType)) 193 | filteredTypes.size match { 194 | case 0 => false 195 | case 1 => true 196 | case _ => throw new RuntimeException 197 | } 198 | } 199 | private def isMessageWantedType(message: Message, messageType: MessageType) = { 200 | messageType match { 201 | case MessageType.INFORMATION => message.messageType.isInformation 202 | case MessageType.WARNING => message.messageType.isWarning 203 | case MessageType.ERROR => message.messageType.isError 204 | case _ => throw new RuntimeException("Invalid type " + messageType.name) 205 | 206 | } 207 | } 208 | 209 | def responseErrors: JsValue = { 210 | val errorMessages = this.errors() 211 | val errorMessagesWithKey = errorMessages.filter(_.key.isDefined) 212 | val messagesGroupedByKey = errorMessagesWithKey.groupBy(_.key) 213 | val errorTextByKey = messagesGroupedByKey.map(record => (record._1.get.value, record._2.map(_.text))) 214 | Json.toJson(errorTextByKey) 215 | } 216 | 217 | def toJson: String = { 218 | Json.prettyPrint(Json.toJson(this)(Messages.jsonWrites)) 219 | } 220 | 221 | override def equals(otherAsAny: Any): Boolean = { 222 | if(otherAsAny == null) return false 223 | if(!otherAsAny.isInstanceOf[Messages]) return false 224 | 225 | val other = otherAsAny.asInstanceOf[Messages] 226 | 227 | return this.messages.equals(other.messages) && 228 | this.parentMessages.equals(other.parentMessages) 229 | } 230 | 231 | override def hashCode: Int = { 232 | return Objects.hashCode( 233 | this.parentMessages, 234 | this.messages 235 | ) 236 | } 237 | } 238 | 239 | object Messages 240 | { 241 | def of = { 242 | Messages(None); 243 | } 244 | 245 | def of(parentMessages: Messages) = { 246 | argumentIsNotNull(parentMessages) 247 | 248 | Messages(Some(parentMessages)) 249 | } 250 | 251 | implicit val jsonWrites = new Writes[Messages] { 252 | def writes(messages: Messages): JsValue = { 253 | Json.obj( 254 | "Items" -> messages.messages 255 | ) 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/pagination/Pagination.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.pagination 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.Json 5 | 6 | case class Pagination( 7 | pageNumber : Int, 8 | itemsPerPage: Int 9 | ) { 10 | Asserts.argumentIsNotNull(pageNumber) 11 | Asserts.argumentIsTrue(pageNumber > 0) 12 | Asserts.argumentIsNotNull(itemsPerPage) 13 | Asserts.argumentIsTrue(itemsPerPage >= 1) 14 | 15 | lazy val offset = itemsPerPage * (pageNumber - 1) 16 | } 17 | 18 | object Pagination { 19 | final val ALL = Pagination(1, Integer.MAX_VALUE) 20 | final val DEFAULT = Pagination(1, 25) 21 | 22 | implicit val jsonWFormat = Json.format[Pagination] 23 | } 24 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/pagination/PaginationResult.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.pagination 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json._ 5 | 6 | case class PaginationResult[TItem]( 7 | currentPagination : Pagination, 8 | totalItemCount : Long, 9 | items : Seq[TItem] 10 | ) { 11 | Asserts.argumentIsNotNull(currentPagination) 12 | Asserts.argumentIsTrue(totalItemCount >= 0) 13 | Asserts.argumentIsNotNull(items) 14 | 15 | def totalPagesCount = { 16 | (totalItemCount / currentPagination.itemsPerPage) + (if (totalItemCount % currentPagination.itemsPerPage > 0) 1 else 0) 17 | } 18 | } 19 | 20 | object PaginationResult 21 | { 22 | implicit def jsonWrites[T](implicit fmt: Writes[T]): Writes[PaginationResult[T]] = new Writes[PaginationResult[T]] { 23 | def writes(ts: PaginationResult[T]) = JsObject(Seq( 24 | "currentPagination" -> Json.toJson(ts.currentPagination), 25 | "totalItemCount" -> JsNumber(ts.totalItemCount), 26 | "totalPagesCount" -> JsNumber(ts.totalPagesCount), 27 | "items" -> JsArray(ts.items.map(Json.toJson(_))) 28 | )) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/response/GlobalMessagesRestResponse.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.response 2 | 3 | import play.api.libs.json.Json 4 | 5 | case class GlobalMessagesRestResponse 6 | ( 7 | info : List[String] = List.empty, 8 | warnings : List[String] = List.empty, 9 | errors : List[String] = List.empty 10 | ) 11 | 12 | object GlobalMessagesRestResponse 13 | { 14 | implicit val jsonFormat = Json.format[GlobalMessagesRestResponse] 15 | 16 | val EMPTY = GlobalMessagesRestResponse() 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/response/LocalMessagesRestResponse.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.response 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.Json 5 | 6 | case class LocalMessagesRestResponse 7 | ( 8 | formIdentifier : String, 9 | info : List[String] = List.empty, 10 | warnings : List[String] = List.empty, 11 | errors : List[String] = List.empty 12 | ) 13 | { 14 | Asserts.argumentIsNotNull(formIdentifier) 15 | Asserts.argumentIsNotNull(info) 16 | Asserts.argumentIsNotNull(warnings) 17 | Asserts.argumentIsNotNull(errors) 18 | } 19 | 20 | object LocalMessagesRestResponse 21 | { 22 | implicit val jsonFormat = Json.format[LocalMessagesRestResponse] 23 | } 24 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/response/MessagesRestResponse.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.response 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.Json 5 | 6 | case class MessagesRestResponse( 7 | global : Option[GlobalMessagesRestResponse] = Some(GlobalMessagesRestResponse.EMPTY), 8 | local : List[LocalMessagesRestResponse] = List.empty 9 | ) { 10 | Asserts.argumentIsNotNull(global) 11 | Asserts.argumentIsNotNull(local) 12 | } 13 | 14 | object MessagesRestResponse { 15 | implicit val jsonFormat = Json.format[MessagesRestResponse] 16 | 17 | val EMPTY = MessagesRestResponse() 18 | } 19 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/response/ResponseTools.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.response 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.josip.reactiveluxury.core.messages.{MessageKey, Messages} 5 | import com.josip.reactiveluxury.core.messages.MessageKey 6 | import play.api.libs.json.{Json, JsError, JsValue, Writes} 7 | 8 | object ResponseTools 9 | { 10 | val GLOBAL_MESSAGE_KEY = MessageKey("GLOBAL_MESSAGE") 11 | 12 | def restResponseOf[T: Writes](data: T, messages: Messages): RestResponse[T] = 13 | { 14 | val messagesRestResponse = ResponseTools.messagesToMessagesRestResponse(messages) 15 | 16 | RestResponse[T]( 17 | data = Some(data), 18 | messages = Some(messagesRestResponse) 19 | ) 20 | } 21 | 22 | def messagesToMessagesRestResponse(messages: Messages): MessagesRestResponse = 23 | { 24 | val global = Helper.messagesToGlobalMessagesRestResponse(messages) 25 | val local = Helper.messagesToLocalMessagesRestResponse(messages) 26 | 27 | MessagesRestResponse( 28 | global = Some(global), 29 | local = local 30 | ) 31 | } 32 | 33 | def data[T: Writes](data: T) = 34 | { 35 | Asserts.argumentIsNotNull(data) 36 | 37 | RestResponse[T](data = Some(data)) 38 | } 39 | 40 | def noData(messages: Messages) = { 41 | Asserts.argumentIsNotNull(messages) 42 | 43 | ResponseTools.restResponseOf(Json.toJson("{}"), messages) 44 | } 45 | 46 | def messages(messagesRestResponse: MessagesRestResponse) = 47 | { 48 | Asserts.argumentIsNotNull(messagesRestResponse) 49 | 50 | RestResponse( 51 | data = Option.empty[JsValue], 52 | messages = Some(messagesRestResponse) 53 | ) 54 | } 55 | 56 | def of[T: Writes](data: T, messages: Option[Messages]) = { 57 | Asserts.argumentIsNotNull(data) 58 | Asserts.argumentIsNotNull(messages) 59 | 60 | RestResponse[T]( 61 | data = Some(data), 62 | messages = messages.map(ResponseTools.messagesToMessagesRestResponse) 63 | ) 64 | } 65 | 66 | def jsErrorToRestResponse[T: Writes](error: JsError): RestResponse[T] = 67 | { 68 | Asserts.argumentIsNotNull(error) 69 | 70 | val errorsGroupByFormId = error.errors.groupBy(_._1.toJsonString).toList 71 | 72 | val restLocalErrors = errorsGroupByFormId.map(v => 73 | LocalMessagesRestResponse( 74 | formIdentifier = v._1.replaceFirst("obj.", ""), 75 | errors = v._2.map(_._2).flatten.map(_.message).toList 76 | ) 77 | ) 78 | 79 | val messagesRestResponse = MessagesRestResponse(local = restLocalErrors) 80 | 81 | RestResponse[T](data = None, messages = Some(messagesRestResponse)) 82 | } 83 | 84 | def errorsToRestResponse(errors: List[String]) = 85 | { 86 | Asserts.argumentIsNotNull(errors) 87 | 88 | val messagesResponse = MessagesRestResponse( 89 | global = Some(GlobalMessagesRestResponse(errors = errors)) 90 | ) 91 | ResponseTools.messages(messagesResponse) 92 | } 93 | 94 | def errorToRestResponse(error: String) = 95 | { 96 | Asserts.argumentIsNotNull(error) 97 | 98 | ResponseTools.errorsToRestResponse(List(error)) 99 | } 100 | 101 | private object Helper 102 | { 103 | def messagesToGlobalMessagesRestResponse(messages: Messages): GlobalMessagesRestResponse = 104 | { 105 | val allBindMessages = messages.bindMessages 106 | val allGlobalBindMessage = allBindMessages.filter(_.key == Some(GLOBAL_MESSAGE_KEY)).toList 107 | 108 | val globalInfo = allGlobalBindMessage.filter(_.messageType.isInformation).map(_.text) 109 | val globalWarnings = allGlobalBindMessage.filter(_.messageType.isWarning).map(_.text) 110 | val globalErrors = allGlobalBindMessage.filter(_.messageType.isError).map(_.text) 111 | 112 | GlobalMessagesRestResponse( 113 | info = globalInfo, 114 | warnings = globalWarnings, 115 | errors = globalErrors 116 | ) 117 | } 118 | 119 | def messagesToLocalMessagesRestResponse(messages: Messages): List[LocalMessagesRestResponse] = 120 | { 121 | val allBindMessages = messages.bindMessages 122 | val allNonGlobalMessages = allBindMessages.filter(_.key.get != GLOBAL_MESSAGE_KEY).toList 123 | val messagesGroupedByKey = allNonGlobalMessages.groupBy(_.key.get) 124 | 125 | messagesGroupedByKey.map(record => 126 | LocalMessagesRestResponse( 127 | formIdentifier = record._1.value, 128 | info = record._2.filter(_.messageType.isInformation).map(_.text), 129 | warnings = record._2.filter(_.messageType.isWarning).map(_.text), 130 | errors = record._2.filter(_.messageType.isError).map(_.text) 131 | ) 132 | ).toList 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/response/RestResponse.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.response 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.functional.syntax._ 5 | import play.api.libs.json._ 6 | 7 | case class RestResponse[TItem: Writes]( 8 | data : Option[TItem], 9 | messages : Option[MessagesRestResponse] = Some(MessagesRestResponse.EMPTY) 10 | ) { 11 | selfRef => 12 | Asserts.argumentIsNotNull(data) 13 | Asserts.argumentIsNotNull(messages) 14 | 15 | lazy val json = Json.toJson(selfRef) 16 | } 17 | 18 | object RestResponse { 19 | implicit def writes[TItem: Writes]: Writes[RestResponse[TItem]] = ( 20 | (__ \ 'data).writeNullable[TItem] and 21 | (__ \ 'messages).writeNullable[MessagesRestResponse] 22 | )(unlift(RestResponse.unapply[TItem])) 23 | } 24 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/service/ChildRelationService.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.service 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.josip.reactiveluxury.core.slick.generic.{ChildRelationsCrudRepository, ChildRelation} 5 | import scala.concurrent.Future 6 | 7 | trait ChildRelationService[ChildRelationItem <: ChildRelation[ChildRelationItem]] { 8 | def create(item: ChildRelationItem): Future[Int] 9 | def createAll(items: List[ChildRelationItem]): Future[Option[Int]] 10 | } 11 | 12 | abstract class ChildRelationServiceImpl[ChildRelationItem <: ChildRelation[ChildRelationItem], ChildRelationRepo <: ChildRelationsCrudRepository[ChildRelationItem]] 13 | (val childRelationRepository: ChildRelationRepo) extends ChildRelationService[ChildRelationItem] { 14 | Asserts.argumentIsNotNull(childRelationRepository) 15 | 16 | override def create(item: ChildRelationItem): Future[Int] = { 17 | Asserts.argumentIsNotNull(item) 18 | 19 | this.childRelationRepository.insert(item) 20 | } 21 | 22 | override def createAll(items: List[ChildRelationItem]): Future[Option[Int]] = { 23 | Asserts.argumentIsNotNull(items) 24 | 25 | this.childRelationRepository.insertAll(items) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/service/EntityService.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.service 2 | 3 | import akka.actor.ActorSystem 4 | import akka.util.Timeout 5 | import com.josip.reactiveluxury.core.pagination.Pagination 6 | import com.josip.reactiveluxury.core.slick.generic.{CrudRepository, Entity} 7 | import com.josip.reactiveluxury.core.utils.DateUtils 8 | import com.josip.reactiveluxury.core.Asserts 9 | import com.josip.reactiveluxury.module.actor.actionlog.{ActionLogActor, ActionLogCreateMsg} 10 | import com.josip.reactiveluxury.module.domain.actionlog.{ActionDomainType, ActionLogEntity, ActionType} 11 | import com.josip.reactiveluxury.module.domain.user.SystemUser 12 | import play.api.libs.json.Format 13 | 14 | import scala.concurrent.duration._ 15 | import scala.concurrent.{ExecutionContext, Future} 16 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 17 | 18 | trait EntityService[EntityItem <: Entity[EntityItem]] { 19 | def create(item: EntityItem)(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[EntityItem] 20 | def createAll(items: List[EntityItem])(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[List[EntityItem]] 21 | def update(item: EntityItem)(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[EntityItem] 22 | 23 | def tryGetById(id: Long): Future[Option[EntityItem]] 24 | def tryGetByExternalId(externalId: String): Future[Option[EntityItem]] 25 | 26 | def getAll: Future[List[EntityItem]] 27 | def getAllPaginated(pagination: Pagination): Future[List[EntityItem]] 28 | 29 | def deleteById(id: Long) 30 | 31 | def getById(id: Long)(implicit ec : ExecutionContext): Future[EntityItem] = { 32 | this.tryGetById(id) 33 | .map(_.getOrElse(throw new RuntimeException(s"entity with this id does not exist. id: '$id', class: '${this.getClass}'")) 34 | ) 35 | } 36 | 37 | def getByExternalId(externalId: String)(implicit ec : ExecutionContext): Future[EntityItem] = { 38 | Asserts.argumentIsNotNull(externalId) 39 | 40 | this.tryGetByExternalId(externalId) 41 | .map(_.getOrElse(throw new RuntimeException(s"entity with this externalId does not exist. externalId: '$externalId', class: '${this.getClass}'")) 42 | ) 43 | } 44 | } 45 | 46 | abstract class EntityServiceImpl[EntityItem <: Entity[EntityItem], EntityRepository <: CrudRepository[EntityItem]](val entityRepository: EntityRepository, val system: ActorSystem) 47 | (implicit entityClassManifest: Manifest[EntityItem], implicit val ec : ExecutionContext) extends EntityService[EntityItem] { 48 | Asserts.argumentIsNotNull(entityRepository) 49 | Asserts.argumentIsNotNull(entityClassManifest) 50 | 51 | override def create(item: EntityItem)(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[EntityItem] = { 52 | Asserts.argumentIsNotNull(item) 53 | 54 | for { 55 | externalId <- this.entityRepository.generateUniqueExternalId handleError() 56 | createdEntity <- this.entityRepository.insert(item.copyWith(("externalId", Some(externalId)))) handleError() 57 | _ <- { 58 | 59 | val actionUserId = userId match { 60 | case Some(id) => id 61 | case None => SystemUser.id 62 | } 63 | 64 | val createdAction = ActionLogEntity.of[EntityItem, EntityItem]( 65 | userId = actionUserId, 66 | domainType = ActionDomainType.getByEntityClass(entityClassManifest.runtimeClass), 67 | domainId = createdEntity.id.get, 68 | actionType = ActionType.CREATED, 69 | before = None, 70 | after = Some(createdEntity) 71 | ) 72 | 73 | system.actorSelection(s"user/${ActionLogActor.NAME}") ! ActionLogCreateMsg(createdAction) 74 | Future.successful({}) 75 | } 76 | } yield createdEntity 77 | } 78 | 79 | def createAll(items: List[EntityItem])(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[List[EntityItem]] = { 80 | Asserts.argumentIsNotNull(items) 81 | Asserts.argumentIsNotNull(userId) 82 | 83 | val insertedValues = items.map(item => { 84 | for { 85 | insertValue <- this.create(item)(userId) handleError() 86 | } yield insertValue 87 | }) 88 | 89 | Future.sequence(insertedValues) 90 | } 91 | 92 | override def update(item: EntityItem)(userId: Option[Long] = None)(implicit format: Format[EntityItem]): Future[EntityItem] = { 93 | Asserts.argumentIsNotNull(item) 94 | 95 | for { 96 | updatedEntity <- this.entityRepository.update(item.copyWith(("modifiedOn", Some(DateUtils.nowDateTimeUTC)))) handleError() 97 | _ <- { 98 | val actionUserId = userId match { 99 | case Some(id) => id 100 | case None => SystemUser.id 101 | } 102 | val updateAction = ActionLogEntity.of[EntityItem, EntityItem]( 103 | userId = actionUserId, 104 | domainType = ActionDomainType.getByEntityClass(entityClassManifest.runtimeClass), 105 | domainId = item.id.get, 106 | actionType = ActionType.UPDATED, 107 | before = Some(item), 108 | after = Some(updatedEntity) 109 | ) 110 | system.actorSelection(s"user/${ActionLogActor.NAME}") ! ActionLogCreateMsg(updateAction) 111 | Future.successful({}) 112 | } 113 | } yield updatedEntity 114 | } 115 | 116 | override def deleteById(id: Long) { 117 | this.entityRepository.deleteById(id) 118 | } 119 | 120 | override def tryGetById(id: Long): Future[Option[EntityItem]] = { 121 | this.entityRepository.findById(id) 122 | } 123 | 124 | override def tryGetByExternalId(externalId: String): Future[Option[EntityItem]] = { 125 | Asserts.argumentIsNotNull(externalId) 126 | 127 | this.entityRepository.findByExternalId(externalId) 128 | } 129 | 130 | override def getAll: Future[List[EntityItem]] = { 131 | this.entityRepository.findAll 132 | } 133 | 134 | override def getAllPaginated(pagination: Pagination): Future[List[EntityItem]] = { 135 | this.entityRepository.findPaginatedOrdered(pagination) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/custommapper/CustomSlickMapper.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.custommapper 2 | 3 | import com.github.tminglei.slickpg.Interval 4 | import com.josip.reactiveluxury.core.utils.StringUtils 5 | import org.joda.time.{LocalDate, Period, DurationFieldType} 6 | import play.api.libs.json.{Json, JsValue} 7 | import slick.jdbc.PostgresProfile 8 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 9 | 10 | object CustomSlickMapper 11 | { 12 | object Postgres 13 | { 14 | implicit def enumColumnType[T<: Enum[T]](implicit classManifest: Manifest[T]): ColumnType[T] = 15 | { 16 | PostgresProfile.MappedColumnType.base[T, String]( 17 | { adt => adt.name }, 18 | { dv => StringUtils.parseEnum[T](dv)(classManifest) } 19 | ) 20 | } 21 | 22 | implicit val jodaDateTimeColumnType = PostgresProfile.MappedColumnType.base[org.joda.time.DateTime, java.sql.Timestamp]( 23 | { dt => new java.sql.Timestamp(dt.getMillis) }, 24 | { ts => new org.joda.time.DateTime(ts) } 25 | ) 26 | 27 | implicit val jodaLocalDateColumnType = PostgresProfile.MappedColumnType.base[org.joda.time.LocalDate, java.sql.Date]( 28 | { dt => new java.sql.Date(dt.toDateTimeAtStartOfDay.getMillis) }, 29 | { d => new LocalDate(d.getTime) } 30 | ) 31 | 32 | implicit val jsValueColumnType = PostgresProfile.MappedColumnType.base[JsValue, String]( 33 | { jsv => jsv.toString() }, 34 | { sv => Json.parse(sv) } 35 | ) 36 | 37 | implicit val jodaPeriodTimeColumnType = PostgresProfile.MappedColumnType.base[org.joda.time.Period, Interval]( 38 | { period => 39 | val years = period.get(DurationFieldType.years()) 40 | val months = period.get(DurationFieldType.months()) 41 | val days = period.get(DurationFieldType.days()) 42 | val hours = period.get(DurationFieldType.hours()) 43 | val mins = period.get(DurationFieldType.minutes()) 44 | val secs = period.get(DurationFieldType.seconds()) 45 | 46 | Interval( 47 | years = years, 48 | months = months, 49 | days = days, 50 | hours = hours, 51 | minutes = mins, 52 | seconds = secs 53 | ) 54 | }, 55 | { interval => 56 | val years = interval.years 57 | val months = interval.months 58 | val days = interval.days 59 | val hours = interval.hours 60 | val mins = interval.minutes 61 | val secs = interval.seconds.toInt 62 | val millis = interval.milliseconds 63 | 64 | new Period(years, months, 0, days, hours, mins, secs, millis); 65 | } 66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/generic/ChildRelation.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.generic 2 | 3 | trait ChildRelation[T] 4 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/generic/ChildRelationsCruds.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.generic 2 | 3 | import com.josip.reactiveluxury.configuration.DatabaseProvider 4 | import com.josip.reactiveluxury.core.Asserts 5 | import slick.lifted 6 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 10 | 11 | trait ChildRelationsCrudRepository [ChildRelationItem <: ChildRelation[ChildRelationItem]] { 12 | def insert(item: ChildRelationItem): Future[Int] 13 | def insertAll(items: Seq[ChildRelationItem]): Future[Option[Int]] 14 | } 15 | 16 | abstract class ChildRelationsCrudRepositoryImpl[T <: Table[ChildRelationItem], ChildRelationItem <: ChildRelation[ChildRelationItem]]( 17 | val databaseProvider: DatabaseProvider, 18 | val query: lifted.TableQuery[T], 19 | implicit val ec : ExecutionContext 20 | ) extends ChildRelationsCrudRepository[ChildRelationItem] { 21 | Asserts.argumentIsNotNull(databaseProvider) 22 | Asserts.argumentIsNotNull(query) 23 | 24 | override def insert(item: ChildRelationItem): Future[Int] = { 25 | Asserts.argumentIsNotNull(item) 26 | 27 | for { 28 | result <- databaseProvider.db.run((query += item).transactionally) handleError() 29 | } yield result 30 | } 31 | 32 | override def insertAll(items: Seq[ChildRelationItem]): Future[Option[Int]] = { 33 | Asserts.argumentIsNotNull(items) 34 | 35 | this.databaseProvider.db.run((query ++= items).transactionally) 36 | } 37 | } -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/generic/Cruds.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.generic 2 | 3 | import java.util.UUID 4 | 5 | import com.josip.reactiveluxury.configuration.DatabaseProvider 6 | import com.josip.reactiveluxury.core.pagination.Pagination 7 | import com.josip.reactiveluxury.core.{Asserts, GeneratedId} 8 | import org.joda.time.DateTime 9 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 10 | import slick.lifted 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 14 | 15 | trait CrudRepository[EntityItem <: Entity[EntityItem]] { 16 | def count: Future[Int] 17 | 18 | def findAll: Future[List[EntityItem]] 19 | def findPaginatedOrdered(pagination: Pagination): Future[List[EntityItem]] 20 | def findById(id: Long): Future[Option[EntityItem]] 21 | def findByExternalId(externalId: String): Future[Option[EntityItem]] 22 | 23 | def insert(item: EntityItem): Future[EntityItem] 24 | def insertAll(items: Seq[EntityItem]): Future[Option[Int]] 25 | def update(item: EntityItem): Future[EntityItem] 26 | def deleteById(id: Long): Future[Int] 27 | 28 | def generateUniqueExternalId: Future[String] 29 | } 30 | 31 | abstract class CrudRepositoryImpl[T <: Table[EntityItem] with IdentifiableTable, EntityItem <: Entity[EntityItem]]( 32 | val databaseProvider: DatabaseProvider, 33 | val query: lifted.TableQuery[T], 34 | implicit val ec : ExecutionContext 35 | ) extends CrudRepository[EntityItem]{ 36 | Asserts.argumentIsNotNull(databaseProvider) 37 | Asserts.argumentIsNotNull(query) 38 | 39 | val queryWithId = this.query returning query.map(_.id) 40 | 41 | override def count: Future[Int] = this.databaseProvider.db.run(query.length.result) 42 | 43 | override def findAll: Future[List[EntityItem]] = { 44 | this.databaseProvider.db.run(this.query.result).map(_.toList) 45 | } 46 | 47 | def findPaginated(pagination: Pagination): Future[List[EntityItem]] = { 48 | this.databaseProvider.db.run(this.query.drop(pagination.offset).take(pagination.itemsPerPage).result).map(_.toList) 49 | } 50 | 51 | def findPaginatedOrdered(pagination: Pagination): Future[List[EntityItem]] = { 52 | this.databaseProvider.db 53 | .run(this.query.drop(pagination.offset).take(pagination.itemsPerPage).sortBy(_.id.desc).result) 54 | .map(_.toList) 55 | } 56 | 57 | override def findById(id: Long): Future[Option[EntityItem]] = { 58 | Asserts.argumentIsNotNull(id) 59 | 60 | val action = this.query.filter(_.id === id).result.headOption 61 | databaseProvider.db.run(action) 62 | } 63 | 64 | override def findByExternalId(externalId: String): Future[Option[EntityItem]] = { 65 | Asserts.argumentIsNotNull(externalId) 66 | 67 | val action = this.query.filter(_.externalId === externalId).result.headOption 68 | databaseProvider.db.run(action) 69 | } 70 | 71 | override def insert(item: EntityItem): Future[EntityItem] = { 72 | Asserts.argumentIsNotNull(item) 73 | 74 | for { 75 | generatedId <- databaseProvider.db.run((queryWithId += item).transactionally).map(id => GeneratedId(id)) handleError() 76 | entity <- this.findById(generatedId.id) handleError() 77 | } yield entity.get 78 | } 79 | 80 | override def insertAll(items: Seq[EntityItem]): Future[Option[Int]] = { 81 | Asserts.argumentIsNotNull(items) 82 | 83 | this.databaseProvider.db.run((query ++= items).transactionally) 84 | } 85 | 86 | override def update(item: EntityItem): Future[EntityItem] = { 87 | Asserts.argumentIsNotNull(item) 88 | 89 | for { 90 | _ <- this.databaseProvider.db.run(this.query.filter(_.id === item.id).update(item).transactionally) handleError() 91 | entity <- this.findById(item.id.get) handleError() 92 | } yield entity.get 93 | } 94 | 95 | override def deleteById(id: Long): Future[Int] = { 96 | Asserts.argumentIsNotNull(id) 97 | 98 | this.databaseProvider.db.run(this.query.filter(_.id === id).delete.transactionally) handleError() 99 | } 100 | 101 | override def generateUniqueExternalId: Future[String] = { 102 | val externalIdCandidate = UUID.randomUUID.toString 103 | 104 | val eventualEventualString: Future[Future[String]] = for { 105 | userCandidate <- this.findByExternalId(externalIdCandidate) 106 | } yield { 107 | if (userCandidate.isDefined) this.generateUniqueExternalId 108 | else Future.successful(externalIdCandidate) 109 | } 110 | 111 | eventualEventualString.flatMap(f => f) 112 | } 113 | } 114 | 115 | trait IdentifiableTable { 116 | def id: slick.lifted.Rep[Long] 117 | def externalId: slick.lifted.Rep[String] 118 | def createdOn: slick.lifted.Rep[DateTime] 119 | def modifiedOn: slick.lifted.Rep[DateTime] 120 | } 121 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/generic/Entity.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.generic 2 | 3 | import org.joda.time.DateTime 4 | 5 | trait Entity[T] { 6 | def id : Option[Long] 7 | def externalId : Option[String] 8 | def createdOn : Option[DateTime] 9 | def modifiedOn : Option[DateTime] 10 | 11 | def copyWith(changes: (String, AnyRef)*): T = { 12 | val clas = getClass 13 | val constructor = clas.getDeclaredConstructors.head 14 | val argumentCount = constructor.getParameterTypes.size 15 | val fields = clas.getDeclaredFields 16 | val arguments = (0 until argumentCount) map { i => 17 | val fieldName = fields(i).getName 18 | changes.find(_._1 == fieldName) match { 19 | case Some(change) => change._2 20 | case None => { 21 | val getter = clas.getMethod(fieldName) 22 | getter.invoke(this) 23 | } 24 | } 25 | } 26 | constructor.newInstance(arguments: _*).asInstanceOf[T] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/generic/EntityHelper.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.generic 2 | 3 | import scala.language.experimental.macros 4 | 5 | //:TODO: macros needs to be in separate build to be used 6 | object EntityHelper { 7 | import scala.reflect.macros.blackbox 8 | 9 | def withNewProperty[T, I](entity: T, propertyValue: I): T = macro withNewPropertyImpl[T, I] 10 | 11 | def withNewPropertyImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: blackbox.Context)(entity: c.Expr[T], propertyValue: c.Expr[I]): c.Expr[T] = { 12 | import c.universe._ 13 | 14 | val tree = reify(entity.splice).tree 15 | val copy = entity.actualType.member(TermName("copy")) 16 | 17 | c.Expr[T](Apply( 18 | Select(tree, copy), 19 | AssignOrNamedArg(Ident(TermName("createdOn")), reify(propertyValue.splice).tree) :: Nil 20 | )) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/postgres/MyPostgresDriver.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.postgres 2 | 3 | import com.github.tminglei.slickpg._ 4 | 5 | trait MyPostgresDriver extends ExPostgresProfile 6 | with PgDateSupport { 7 | 8 | override val api = MyAPI 9 | 10 | object MyAPI extends API with DateTimeImplicits { 11 | } 12 | } 13 | 14 | object MyPostgresDriver extends MyPostgresDriver -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/slick/utils/SlickUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.slick.utils 2 | 3 | import slick.dbio.Effect 4 | import slick.jdbc.{GetResult, SQLActionBuilder} 5 | import slick.jdbc.SetParameter.SetUnit 6 | import slick.sql.SqlStreamingAction 7 | 8 | object SlickUtils { 9 | def staticQueryToSQLActionBuilder(query: String): SQLActionBuilder = { 10 | SQLActionBuilder(Seq(query), SetUnit) 11 | } 12 | 13 | def staticQueryToStreamingAction[T](query: String)(implicit rconv: GetResult[T]): SqlStreamingAction[Vector[T], T, Effect] = { 14 | SlickUtils.staticQueryToSQLActionBuilder(query).as[T] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/BigDecimalUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import Asserts._ 5 | import java.math.{BigDecimal => JavaBigDecimal} 6 | 7 | object BigDecimalUtils 8 | { 9 | def optionScalaToOptionJava(value: Option[BigDecimal]): Option[JavaBigDecimal] = 10 | { 11 | argumentIsNotNull(value) 12 | 13 | if(value.isDefined) Option(BigDecimalUtils.scalaToJava(value.get)) else Option.empty 14 | } 15 | 16 | def scalaToJava(value: BigDecimal) = 17 | { 18 | argumentIsNotNull(value) 19 | 20 | new JavaBigDecimal(value.toString) 21 | } 22 | 23 | def optionJavaToOptionScala(value: Option[JavaBigDecimal]): Option[BigDecimal] = 24 | { 25 | argumentIsNotNull(value) 26 | 27 | if(value.isDefined) Option(BigDecimalUtils.javaToScala(value.get)) else Option.empty 28 | } 29 | 30 | def javaToScala(value: JavaBigDecimal) = 31 | { 32 | argumentIsNotNull(value) 33 | 34 | BigDecimal(value) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/ConfigurationUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.josip.reactiveluxury.core.Asserts 6 | import play.api.Configuration 7 | 8 | @Singleton() 9 | class ConfigurationUtils @Inject()(configuration: Configuration) { 10 | private final val KEY_IS_NOT_APP_CONF_ERROR_FORMATTER = "%s not specified in application.conf" 11 | private final val TYPE_NOT_SUPPORTED_ERROR_FORMATTER = "%s type is not supported" 12 | 13 | def getConfigurationByKey[T](key: String)(implicit classManifest: Manifest[T]): T = { 14 | Asserts.argumentIsNotNullNorEmpty(key) 15 | 16 | classManifest match { 17 | case m if m == manifest[String] => this.configuration.getString(key).getOrElse(assureKeyState(key)).asInstanceOf[T] 18 | case m if m == manifest[Int] => this.configuration.getInt(key).getOrElse(assureKeyState(key)).asInstanceOf[T] 19 | case m if m == manifest[Long] => this.configuration.getLong(key).getOrElse(assureKeyState(key)).asInstanceOf[T] 20 | case _ => throw new RuntimeException(TYPE_NOT_SUPPORTED_ERROR_FORMATTER.format(classManifest.runtimeClass.getSimpleName)) 21 | } 22 | } 23 | 24 | private def assureKeyState(key: String) { 25 | throw new IllegalStateException(KEY_IS_NOT_APP_CONF_ERROR_FORMATTER.format(key)) 26 | } 27 | } -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/DateUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | 5 | import scala.language.implicitConversions 6 | import org.joda.time._ 7 | import java.util.Date 8 | import org.joda.time.format.{PeriodFormatterBuilder, PeriodFormatter, DateTimeFormatter, DateTimeFormat} 9 | import Asserts._ 10 | import java.sql.{Date => SqlDate} 11 | 12 | object DateUtils 13 | { 14 | final val dateTimeFormatter : DateTimeFormatter = DateTimeFormat.forPattern("dd MMMM YYYY"); 15 | final val dateTimeFormatterWithMinutes : DateTimeFormatter = DateTimeFormat.forPattern("dd MMMM YYYY, HH:mm"); 16 | final val dateTimeFormatterWithSeconds : DateTimeFormatter = DateTimeFormat.forPattern("dd MMMM YYYY HH:mm:ss"); 17 | final val DD_MMM_YYYY__dateFormatter : DateTimeFormatter = DateTimeFormat.forPattern("dd MMM YYYY"); 18 | final val POSTGRES_DATE_FORMAT : DateTimeFormatter = DateTimeFormat.forPattern("YYYY-MM-dd") 19 | final val HH_mm_DATE_FORMATTER : DateTimeFormatter = DateTimeFormat.forPattern("HH:mm") 20 | final val YYYY_MM_dd__dateFormatter : DateTimeFormatter = DateTimeFormat.forPattern("YYYY-MM-dd"); 21 | 22 | final val PERIOD_FORMATTER: PeriodFormatter = new PeriodFormatterBuilder() 23 | .appendHours() 24 | .appendSeparator(":") 25 | .appendMinutes() 26 | .appendSeparator(":") 27 | .appendSeconds() 28 | .toFormatter 29 | 30 | def isValidValueForFormatter(value: String, dateTimeFormatter: DateTimeFormatter) = { 31 | Asserts.argumentIsNotNull(value) 32 | Asserts.argumentIsNotNull(dateTimeFormatter) 33 | 34 | try { 35 | dateTimeFormatter.parseLocalTime(value) 36 | 37 | true 38 | } catch { 39 | case _ : Throwable => false 40 | } 41 | } 42 | 43 | def canParsePeriod(valueCandidate: String): Boolean = { 44 | try { 45 | DateUtils.PERIOD_FORMATTER.parsePeriod(valueCandidate) 46 | true 47 | } catch { 48 | case _: Throwable => false 49 | } 50 | } 51 | 52 | def parsePeriod(valueCandidate: String): Period = { 53 | Asserts.argumentIsNotNull(valueCandidate) 54 | Asserts.argumentIsTrue(DateUtils.canParsePeriod(valueCandidate)) 55 | 56 | DateUtils.PERIOD_FORMATTER.parsePeriod(valueCandidate) 57 | } 58 | 59 | def nowDateTimeUTC: DateTime = 60 | { 61 | DateTime.now(DateTimeZone.UTC) 62 | } 63 | 64 | def nowLocalDateUTC: LocalDate = 65 | { 66 | LocalDate.now(DateTimeZone.UTC) 67 | } 68 | 69 | def jodaDateTimeToJavaDate(value: Option[DateTime]) : Option[Date] = 70 | { 71 | argumentIsNotNull(value) 72 | 73 | value.map(v=>DateUtils.jodaDateTimeToJavaDate(v)) 74 | } 75 | def jodaDateTimeToJavaDate(value: DateTime): Date = 76 | { 77 | argumentIsNotNull(value) 78 | 79 | value.toDate 80 | } 81 | 82 | def jodaDateTimeToSqlDate(value: Option[DateTime]) : Option[SqlDate] = 83 | { 84 | argumentIsNotNull(value) 85 | 86 | value.map(v=>DateUtils.jodaDateTimeToSqlDate(v)) 87 | } 88 | def jodaDateTimeToSqlDate(value: DateTime): SqlDate = 89 | { 90 | argumentIsNotNull(value) 91 | 92 | new SqlDate(value.getMillis) 93 | } 94 | 95 | def javaDateToJodaDateTime(value: Date): DateTime = 96 | { 97 | argumentIsNotNull(value) 98 | 99 | new DateTime(value).withZone(DateTimeZone.UTC) 100 | } 101 | def javaDateToJodaDateTime(value: Option[Date]): Option[DateTime] = 102 | { 103 | argumentIsNotNull(value) 104 | 105 | value.map(v=>DateUtils.javaDateToJodaDateTime(v)) 106 | } 107 | 108 | def jodaLocalDateToJavaDate(value: Option[LocalDate]) : Option[Date] = 109 | { 110 | argumentIsNotNull(value) 111 | 112 | value.map(v=>DateUtils.jodaLocalDateToJavaDate(v)) 113 | } 114 | def jodaLocalDateToJavaDate(value: LocalDate): Date = 115 | { 116 | argumentIsNotNull(value) 117 | 118 | value.toDate 119 | } 120 | def javaDateToJodaLocalDate(value: Date): LocalDate = 121 | { 122 | argumentIsNotNull(value) 123 | 124 | new LocalDate(value, DateTimeZone.UTC) 125 | } 126 | def javaDateToJodaLocalDate(value: Option[Date]): Option[LocalDate] = 127 | { 128 | argumentIsNotNull(value) 129 | 130 | value.map(v=>DateUtils.javaDateToJodaLocalDate(v)) 131 | } 132 | 133 | def jodaLocalTimeToJavaDate(value: Option[LocalTime]) : Option[Date] = 134 | { 135 | argumentIsNotNull(value) 136 | 137 | value.map(DateUtils.jodaLocalTimeToJavaDate(_)) 138 | } 139 | def jodaLocalTimeToJavaDate(value: LocalTime): Date = 140 | { 141 | argumentIsNotNull(value) 142 | 143 | new Date(value.toDateTimeToday.getMillisOfDay) 144 | } 145 | def javaDateToJodaLocalTime(value: Date): LocalTime = 146 | { 147 | argumentIsNotNull(value) 148 | 149 | new LocalTime(value) 150 | } 151 | def javaDateToJodaLocalTime(value: Option[Date]): Option[LocalTime] = 152 | { 153 | argumentIsNotNull(value) 154 | 155 | value.map(DateUtils.javaDateToJodaLocalTime(_)) 156 | } 157 | 158 | def calculateLocalTimeRange(from :LocalTime, to :LocalTime, step :Period) :Seq[LocalTime] = 159 | { 160 | Iterator.iterate(from)(_.plus(step)).takeWhile(!_.isAfter(to)).toList 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/HashUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.Codecs 5 | 6 | 7 | object HashUtils 8 | { 9 | val salt = "jdhk4:AGla5765G6.,GR453u(%)(A435=gskgjde4)=(#%)(gsdgksj<468)" 10 | val numberOfPasses = 20 11 | 12 | def sha1(value: String) : String = 13 | { 14 | Asserts.argumentIsNotNull(value) 15 | 16 | var result = value 17 | for(i <- 1 to numberOfPasses) { 18 | result = calculateSha1Digest(result) 19 | } 20 | 21 | result 22 | } 23 | 24 | private def calculateSha1Digest(value: String) : String = Codecs.sha1(salt + value) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/ReactiveValidateUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import com.josip.reactiveluxury.core.Asserts._ 4 | import com.josip.reactiveluxury.core.messages.{MessageKey, Messages} 5 | 6 | import scala.concurrent.Future 7 | 8 | object ReactiveValidateUtils { 9 | def continueIfMessagesHaveErrors(messages: Messages)(action: Future[Messages]): Future[Messages] = { 10 | argumentIsNotNull(messages) 11 | 12 | if(!messages.hasErrors()) { 13 | action 14 | } else { 15 | Future.successful(messages) 16 | } 17 | } 18 | 19 | 20 | def validateLengthIsLessThanOrEqual(value: String, minLength: Int, messages: Messages, errorKey:String, errorMessage: String): Future[Messages] = { 21 | argumentIsNotNull(value) 22 | argumentIsNotNull(minLength) 23 | argumentIsTrue(minLength >= 0) 24 | argumentIsNotNull(messages) 25 | argumentIsNotNullNorEmpty(errorMessage) 26 | 27 | ValidateUtils.validateLengthIsLessThanOrEqual(value, minLength, messages, errorKey, errorMessage) 28 | 29 | Future.successful(messages) 30 | } 31 | 32 | def isFalse(value: Boolean, messages: Messages, errorKey: String, errorMessage: String): Future[Messages] = { 33 | argumentIsNotNull(value) 34 | argumentIsNotNull(messages) 35 | argumentIsNotNullNorEmpty(errorKey) 36 | argumentIsNotNullNorEmpty(errorMessage) 37 | 38 | ValidateUtils.isFalse(value, messages, errorKey, errorMessage) 39 | 40 | Future.successful(messages) 41 | } 42 | 43 | def isTrue(value: Boolean, messages: Messages, errorKey: String, errorMessage: String): Future[Messages] = { 44 | argumentIsNotNull(value) 45 | argumentIsNotNull(messages) 46 | argumentIsNotNullNorEmpty(errorKey) 47 | argumentIsNotNullNorEmpty(errorMessage) 48 | 49 | ValidateUtils.isTrue(value, messages, errorKey, errorMessage) 50 | 51 | Future.successful(messages) 52 | } 53 | 54 | def isNotNull[T](value: T, messages: Messages, errorKey: String, errorMessage: String): Future[Messages] = { 55 | argumentIsNotNull(messages) 56 | argumentIsNotNullNorEmpty(errorMessage) 57 | 58 | ValidateUtils.isNotNull(value, messages, errorKey, errorMessage) 59 | 60 | Future.successful(messages) 61 | } 62 | 63 | def isNotNullNorEmpty(value: String, messages: Messages, errorKey: String, errorMessage: String): Future[Messages] = { 64 | argumentIsNotNull(messages) 65 | argumentIsNotNullNorEmpty(errorMessage) 66 | 67 | ValidateUtils.isNotNullNorEmpty(value, messages, errorKey, errorMessage) 68 | 69 | Future.successful(messages) 70 | } 71 | 72 | def validateEmail(email: String, errorKey: MessageKey, messages: Messages): Future[Messages] = { 73 | argumentIsNotNull(email) 74 | argumentIsNotNull(errorKey) 75 | argumentIsNotNull(messages) 76 | 77 | ValidateUtils.validateEmail( 78 | email, 79 | errorKey, 80 | messages 81 | ) 82 | 83 | Future.successful(messages) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/core/utils/StringUtils.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.core.utils 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.josip.reactiveluxury.core.messages.{MessageKey, Messages} 5 | import Asserts._ 6 | import org.joda.time.format.DateTimeFormatter 7 | import org.joda.time.{LocalDate, DateTime} 8 | import com.josip.reactiveluxury.core.messages.MessageKey 9 | 10 | object StringUtils 11 | { 12 | final val WHITESPACE = " " 13 | final val EMPTY_STRING = "" 14 | 15 | private final val ERROR_DEFAULT_KEY = MessageKey("ErrorKey") 16 | 17 | private final val SINGLE_QUOTE_FORMATTER = "'%s'" 18 | 19 | private object ErrorMessages 20 | { 21 | val VALUE_CANT_BE_PARSED_DEFAULT_ERROR_FORMATTER = "Text '%s' cannot be parsed to class '%s'." 22 | 23 | def createDefaultParsingError[TItem](valueAsString: String)(implicit fieldManifest: Manifest[TItem]): String = 24 | { 25 | val valueClass = fieldManifest.runtimeClass 26 | String.format(VALUE_CANT_BE_PARSED_DEFAULT_ERROR_FORMATTER, valueAsString, valueClass.getSimpleName) 27 | } 28 | } 29 | 30 | final val COMMA_SEPARATOR = "," 31 | 32 | def trimExtraQuotes(value: String): String = 33 | { 34 | if(value.startsWith("\"") && value.endsWith("\"")) { 35 | value.substring(1, value.length - 1) 36 | } else { 37 | value 38 | } 39 | } 40 | 41 | def singleQuote(value: String): String = 42 | { 43 | argumentIsNotNull(value) 44 | 45 | String.format(SINGLE_QUOTE_FORMATTER, value) 46 | } 47 | 48 | def removeWhiteSpaces(value: String) = 49 | { 50 | argumentIsNotNull(value) 51 | 52 | value.replace(WHITESPACE, EMPTY_STRING) 53 | } 54 | 55 | def trim(value: String) = 56 | { 57 | argumentIsNotNull(value) 58 | 59 | value.trim 60 | } 61 | 62 | def isEmpty(valueAsString: String) = 63 | { 64 | argumentIsNotNull(valueAsString) 65 | 66 | valueAsString.isEmpty 67 | } 68 | 69 | def isNotEmpty(valueAsString: String) = 70 | { 71 | argumentIsNotNull(valueAsString) 72 | 73 | !StringUtils.isEmpty(valueAsString) 74 | } 75 | 76 | def split(valueAsString: String, separator: String): List[String] = 77 | { 78 | argumentIsNotNull(valueAsString) 79 | argumentIsNotNullNorEmpty(separator) 80 | 81 | val localMessages = Messages.of 82 | this.evaluateSplit(valueAsString, separator, localMessages) 83 | argumentIsTrue(!localMessages.hasErrors) 84 | 85 | valueAsString.split(separator).toList 86 | } 87 | def evaluateSplit(valueAsString: String, separator: String, messages: Messages) 88 | { 89 | argumentIsNotNull(valueAsString) 90 | argumentIsNotNullNorEmpty(separator) 91 | argumentIsNotNull(messages) 92 | 93 | try { valueAsString.split(separator) } 94 | catch { case _: Throwable => messages.putError("Value can be split by this separator.")} 95 | } 96 | def canSplit(valueAsString: String, separator: String) = 97 | { 98 | argumentIsNotNullNorEmpty(separator) 99 | 100 | val messages = Messages.of 101 | this.evaluateSplit(valueAsString, separator, messages) 102 | 103 | !messages.hasErrors 104 | } 105 | 106 | def tryParseBigDecimal(key: MessageKey, valueAsString: String, messages: Messages): Option[BigDecimal] = 107 | { 108 | argumentIsNotNull(key) 109 | argumentIsNotNull(valueAsString) 110 | argumentIsNotNull(messages) 111 | 112 | val localMessages = Messages.of(messages) 113 | this.evaluateParsingBigDecimal(key, valueAsString, localMessages) 114 | 115 | if(localMessages.hasErrors) { None } else { Some(this.parseBigDecimalWithErrorKey(key, valueAsString)) } 116 | } 117 | def parseBigDecimalWithErrorKey(key: MessageKey, valueAsString: String) = 118 | { 119 | argumentIsNotNull(key) 120 | argumentIsNotNull(valueAsString) 121 | argumentIsTrue(StringUtils.canParseBigDecimal(key, valueAsString)) 122 | 123 | BigDecimal(valueAsString) 124 | } 125 | def parseBigDecimal(valueAsString: String): BigDecimal = 126 | { 127 | this.parseBigDecimalWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 128 | } 129 | def evaluateParsingBigDecimal(key: MessageKey, valueAsString: String, messages: Messages) 130 | { 131 | argumentIsNotNull(key) 132 | argumentIsNotNull(key) 133 | argumentIsNotNull(valueAsString) 134 | argumentIsNotNull(messages) 135 | 136 | try { BigDecimal(valueAsString); } 137 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[BigDecimal](valueAsString))} 138 | } 139 | def canParseBigDecimal(key: MessageKey, valueAsString: String) = 140 | { 141 | argumentIsNotNull(key) 142 | 143 | val messages = Messages.of 144 | this.evaluateParsingBigDecimal(key, valueAsString, messages) 145 | 146 | !messages.hasErrors 147 | } 148 | 149 | def tryParseLong(key: MessageKey, valueAsString: String, messages: Messages): Option[Long] = 150 | { 151 | argumentIsNotNull(key) 152 | argumentIsNotNull(valueAsString) 153 | argumentIsNotNull(messages) 154 | 155 | val localMessages = Messages.of(messages) 156 | this.evaluateParsingLong(key, valueAsString, localMessages) 157 | 158 | if(localMessages.hasErrors) { None } else { Some(this.parseLongWithErrorKey(key, valueAsString)) } 159 | } 160 | def parseLongWithErrorKey(key: MessageKey, valueAsString: String) = 161 | { 162 | argumentIsNotNull(key) 163 | argumentIsNotNull(valueAsString) 164 | argumentIsTrue(StringUtils.canParseLong(key, valueAsString)) 165 | 166 | valueAsString.toLong 167 | } 168 | def parseLong(valueAsString: String): Long = 169 | { 170 | this.parseLongWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 171 | } 172 | def evaluateParsingLong(key: MessageKey, valueAsString: String, messages: Messages) 173 | { 174 | argumentIsNotNull(key) 175 | argumentIsNotNull(valueAsString) 176 | argumentIsNotNull(messages) 177 | 178 | try { valueAsString.toLong } 179 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[Long](valueAsString))} 180 | } 181 | def canParseLong(key: MessageKey, valueAsString: String) = 182 | { 183 | argumentIsNotNull(key) 184 | 185 | val messages = Messages.of 186 | this.evaluateParsingLong(key, valueAsString, messages) 187 | 188 | !messages.hasErrors 189 | } 190 | 191 | def tryParseInt(key: MessageKey, valueAsString: String, messages: Messages): Option[Int] = 192 | { 193 | argumentIsNotNull(key) 194 | argumentIsNotNull(valueAsString) 195 | argumentIsNotNull(messages) 196 | 197 | val localMessages = Messages.of(messages) 198 | this.evaluateParsingInt(key, valueAsString, localMessages) 199 | 200 | if(localMessages.hasErrors) { None } else { Some(this.parseIntWithErrorKey(key, valueAsString)) } 201 | } 202 | def parseIntWithErrorKey(key: MessageKey, valueAsString: String) = 203 | { 204 | argumentIsNotNull(key) 205 | argumentIsNotNull(valueAsString) 206 | argumentIsTrue(StringUtils.canParseInt(key, valueAsString)) 207 | 208 | valueAsString.toInt 209 | } 210 | def parseInt(valueAsString: String): Int = 211 | { 212 | this.parseIntWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 213 | } 214 | def evaluateParsingInt(key: MessageKey, valueAsString: String, messages: Messages) 215 | { 216 | argumentIsNotNull(key) 217 | argumentIsNotNull(valueAsString) 218 | argumentIsNotNull(messages) 219 | 220 | try { valueAsString.toInt } 221 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[Int](valueAsString))} 222 | } 223 | def canParseInt(key: MessageKey, valueAsString: String) = 224 | { 225 | argumentIsNotNull(key) 226 | 227 | val messages = Messages.of 228 | this.evaluateParsingInt(key, valueAsString, messages) 229 | 230 | !messages.hasErrors 231 | } 232 | 233 | def tryParseBoolean(key: MessageKey, valueAsString: String, messages: Messages): Option[Boolean] = 234 | { 235 | argumentIsNotNull(key) 236 | argumentIsNotNull(valueAsString) 237 | argumentIsNotNull(messages) 238 | 239 | val localMessages = Messages.of(messages) 240 | this.evaluateParsingBoolean(key, valueAsString, localMessages) 241 | 242 | if(localMessages.hasErrors) { None } else { Some(this.parseBooleanWithErrorKey(key, valueAsString)) } 243 | } 244 | def parseBooleanWithErrorKey(key: MessageKey, valueAsString: String) = 245 | { 246 | argumentIsNotNull(key) 247 | argumentIsNotNull(valueAsString) 248 | argumentIsTrue(StringUtils.canParseBoolean(key, valueAsString)) 249 | 250 | valueAsString.toBoolean 251 | } 252 | def parseBoolean(valueAsString: String): Boolean = 253 | { 254 | this.parseBooleanWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 255 | } 256 | def evaluateParsingBoolean(key: MessageKey, valueAsString: String, messages: Messages) 257 | { 258 | argumentIsNotNull(key) 259 | argumentIsNotNull(valueAsString) 260 | argumentIsNotNull(messages) 261 | 262 | try { valueAsString.toBoolean } 263 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[Boolean](valueAsString))} 264 | } 265 | def canParseBoolean(key: MessageKey, valueAsString: String) = 266 | { 267 | argumentIsNotNull(key) 268 | 269 | val messages = Messages.of 270 | this.evaluateParsingBoolean(key, valueAsString, messages) 271 | 272 | !messages.hasErrors 273 | } 274 | 275 | def tryParseDateTime(key: MessageKey, valueAsString: String, messages: Messages): Option[DateTime] = 276 | { 277 | argumentIsNotNull(key) 278 | argumentIsNotNull(valueAsString) 279 | argumentIsNotNull(messages) 280 | 281 | val localMessages = Messages.of(messages) 282 | this.evaluateParsingDateTime(key, valueAsString, localMessages) 283 | 284 | if(localMessages.hasErrors) { None } else { Some(this.parseDateTimeWithErrorKey(key, valueAsString)) } 285 | } 286 | def parseDateTimeWithErrorKey(key: MessageKey, valueAsString: String): DateTime = 287 | { 288 | argumentIsNotNull(key) 289 | argumentIsNotNull(valueAsString) 290 | argumentIsTrue(StringUtils.canParseDateTime(key, valueAsString)) 291 | 292 | StringUtils.parseDateTimeWithErrorKey(key, valueAsString, DateUtils.dateTimeFormatterWithSeconds) 293 | } 294 | def parseDateTime(valueAsString: String): DateTime = 295 | { 296 | this.parseDateTimeWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 297 | } 298 | def evaluateParsingDateTime(key: MessageKey, valueAsString: String, messages: Messages) 299 | { 300 | argumentIsNotNull(key) 301 | argumentIsNotNull(valueAsString) 302 | argumentIsNotNull(messages) 303 | 304 | try { StringUtils.parseDateTimeWithErrorKey(key, valueAsString, DateUtils.dateTimeFormatterWithSeconds) } 305 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[DateTime](valueAsString))} 306 | } 307 | def canParseDateTime(key: MessageKey, valueAsString: String): Boolean = 308 | { 309 | argumentIsNotNull(key) 310 | 311 | val messages = Messages.of 312 | this.evaluateParsingDateTime(key, valueAsString, messages) 313 | 314 | !messages.hasErrors 315 | } 316 | 317 | def tryParseDateTime(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter, messages: Messages): Option[DateTime] = 318 | { 319 | argumentIsNotNull(key) 320 | argumentIsNotNull(valueAsString) 321 | argumentIsNotNull(formatter) 322 | argumentIsNotNull(messages) 323 | 324 | val localMessages = Messages.of(messages) 325 | this.evaluateParsingDateTime(key, valueAsString, formatter, localMessages) 326 | 327 | if(localMessages.hasErrors) { None } else { Some(this.parseDateTimeWithErrorKey(key, valueAsString, formatter)) } 328 | } 329 | def parseDateTimeWithErrorKey(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter): DateTime = 330 | { 331 | argumentIsNotNull(valueAsString) 332 | argumentIsNotNull(formatter) 333 | argumentIsTrue(StringUtils.canParseDateTime(key, valueAsString, formatter)) 334 | 335 | formatter.parseDateTime(valueAsString) 336 | } 337 | def parseDateTime(valueAsString: String, formatter: DateTimeFormatter): DateTime = 338 | { 339 | this.parseDateTimeWithErrorKey(ERROR_DEFAULT_KEY, valueAsString, formatter) 340 | } 341 | def evaluateParsingDateTime(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter, messages: Messages) 342 | { 343 | argumentIsNotNull(key) 344 | argumentIsNotNull(valueAsString) 345 | argumentIsNotNull(formatter) 346 | argumentIsNotNull(messages) 347 | 348 | try { formatter.parseDateTime(valueAsString) } 349 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[DateTime](valueAsString))} 350 | } 351 | def canParseDateTime(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter): Boolean = 352 | { 353 | argumentIsNotNull(key) 354 | argumentIsNotNull(formatter) 355 | 356 | val messages = Messages.of 357 | this.evaluateParsingDateTime(key, valueAsString, formatter, messages) 358 | 359 | !messages.hasErrors 360 | } 361 | 362 | def tryParseLocalDate(key: MessageKey, valueAsString: String, messages: Messages): Option[LocalDate] = 363 | { 364 | argumentIsNotNull(key) 365 | argumentIsNotNull(valueAsString) 366 | argumentIsNotNull(messages) 367 | 368 | val localMessages = Messages.of(messages) 369 | this.evaluateParsingLocalDate(key, valueAsString, localMessages) 370 | 371 | if(localMessages.hasErrors) { None } else { Some(this.parseLocalDateWithErrorKey(key, valueAsString)) } 372 | } 373 | def parseLocalDateWithErrorKey(key: MessageKey, valueAsString: String): LocalDate = 374 | { 375 | argumentIsNotNull(key) 376 | argumentIsNotNull(valueAsString) 377 | argumentIsTrue(StringUtils.canParseLocalDate(key, valueAsString)) 378 | 379 | StringUtils.parseLocalDateWithErrorKey(key, valueAsString, DateUtils.dateTimeFormatter) 380 | } 381 | def parseLocalDate(valueAsString: String): LocalDate = 382 | { 383 | this.parseLocalDateWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 384 | } 385 | def evaluateParsingLocalDate(key: MessageKey, valueAsString: String, messages: Messages) 386 | { 387 | argumentIsNotNull(key) 388 | argumentIsNotNull(valueAsString) 389 | argumentIsNotNull(messages) 390 | 391 | try { StringUtils.parseLocalDateWithErrorKey(key, valueAsString, DateUtils.dateTimeFormatter) } 392 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[LocalDate](valueAsString))} 393 | } 394 | def canParseLocalDate(key: MessageKey, valueAsString: String): Boolean = 395 | { 396 | argumentIsNotNull(key) 397 | 398 | val messages = Messages.of 399 | this.evaluateParsingLocalDate(key, valueAsString, messages) 400 | 401 | !messages.hasErrors 402 | } 403 | 404 | def tryParseLocalDate(key: MessageKey, valueAsString: Option[String], messages: Messages): Option[LocalDate] = 405 | { 406 | argumentIsNotNull(key) 407 | argumentIsNotNull(valueAsString) 408 | argumentIsTrue(valueAsString.isDefined) 409 | argumentIsNotNull(messages) 410 | 411 | val localMessages = Messages.of(messages) 412 | this.evaluateParsingLocalDate(key, valueAsString, localMessages) 413 | 414 | if(localMessages.hasErrors) { None } else { Some(this.parseLocalDateWithErrorKey(key, valueAsString)) } 415 | } 416 | def parseLocalDateWithErrorKey(key: MessageKey, valueAsString: Option[String]): LocalDate = 417 | { 418 | argumentIsNotNull(key) 419 | argumentIsNotNull(valueAsString) 420 | argumentIsTrue(valueAsString.isDefined) 421 | argumentIsTrue(StringUtils.canParseLocalDate(key, valueAsString)) 422 | 423 | StringUtils.parseLocalDateWithErrorKey(key, valueAsString.get, DateUtils.dateTimeFormatter) 424 | } 425 | def parseLocalDate(valueAsString: Option[String]): LocalDate = 426 | { 427 | this.parseLocalDateWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 428 | } 429 | def evaluateParsingLocalDate(key: MessageKey, valueAsString: Option[String], messages: Messages) 430 | { 431 | argumentIsNotNull(key) 432 | argumentIsNotNull(valueAsString) 433 | argumentIsNotNull(messages) 434 | 435 | try { StringUtils.parseLocalDateWithErrorKey(key, valueAsString.get, DateUtils.dateTimeFormatter) } 436 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[LocalDate](valueAsString.get))} 437 | } 438 | def canParseLocalDate(key: MessageKey, valueAsString: Option[String]): Boolean = 439 | { 440 | argumentIsNotNull(key) 441 | 442 | val messages = Messages.of 443 | this.evaluateParsingLocalDate(key, valueAsString, messages) 444 | 445 | !messages.hasErrors 446 | } 447 | 448 | def tryParseLocalDate(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter, messages: Messages): Option[LocalDate] = 449 | { 450 | argumentIsNotNull(key) 451 | argumentIsNotNull(valueAsString) 452 | argumentIsNotNull(formatter) 453 | argumentIsNotNull(messages) 454 | 455 | val localMessages = Messages.of(messages) 456 | this.evaluateParsingLocalDate(key, valueAsString, formatter, localMessages) 457 | 458 | if(localMessages.hasErrors) { None } else { Some(this.parseLocalDateWithErrorKey(key, valueAsString, formatter)) } 459 | } 460 | def parseLocalDateWithErrorKey(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter): LocalDate = 461 | { 462 | argumentIsNotNull(key) 463 | argumentIsNotNull(valueAsString) 464 | argumentIsNotNull(formatter) 465 | argumentIsTrue(StringUtils.canParseLocalDate(key, valueAsString, formatter)) 466 | 467 | formatter.parseLocalDate(valueAsString) 468 | } 469 | def parseLocalDate(valueAsString: String, formatter: DateTimeFormatter): LocalDate = 470 | { 471 | this.parseLocalDateWithErrorKey(ERROR_DEFAULT_KEY, valueAsString, formatter) 472 | } 473 | def evaluateParsingLocalDate(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter, messages: Messages) 474 | { 475 | argumentIsNotNull(key) 476 | argumentIsNotNull(valueAsString) 477 | argumentIsNotNull(formatter) 478 | argumentIsNotNull(messages) 479 | 480 | try { formatter.parseLocalDate(valueAsString) } 481 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[LocalDate](valueAsString))} 482 | } 483 | def canParseLocalDate(key: MessageKey, valueAsString: String, formatter: DateTimeFormatter) = 484 | { 485 | argumentIsNotNull(key) 486 | argumentIsNotNull(formatter) 487 | 488 | val messages = Messages.of 489 | this.evaluateParsingLocalDate(key, valueAsString, formatter, messages) 490 | 491 | !messages.hasErrors 492 | } 493 | 494 | def tryParseEnum[TEnum <: Enum[TEnum]](key: MessageKey, valueAsString: String, messages: Messages)(implicit classManifest: Manifest[TEnum]): Option[TEnum] = 495 | { 496 | argumentIsNotNull(key) 497 | argumentIsNotNull(valueAsString) 498 | argumentIsNotNull(messages) 499 | 500 | val localMessages = Messages.of(messages) 501 | this.evaluateParsingEnum[TEnum](key, valueAsString, localMessages) 502 | 503 | if(localMessages.hasErrors) { None } else { Some(this.parseEnumWithErrorKey[TEnum](key, valueAsString)) } 504 | } 505 | def parseEnumWithErrorKey[TEnum <: Enum[TEnum]](key: MessageKey, valueAsString: String)(implicit classManifest: Manifest[TEnum]): TEnum = 506 | { 507 | argumentIsNotNull(key) 508 | argumentIsNotNull(valueAsString) 509 | argumentIsTrue(StringUtils.canParseEnum(key, valueAsString)) 510 | argumentIsNotNull(classManifest) 511 | 512 | val enumClass = classManifest.runtimeClass.asInstanceOf[Class[TEnum]] 513 | Enum.valueOf[TEnum](enumClass, valueAsString) 514 | } 515 | def parseEnum[TEnum <: Enum[TEnum]](valueAsString: String)(implicit classManifest: Manifest[TEnum]): TEnum = 516 | { 517 | this.parseEnumWithErrorKey(ERROR_DEFAULT_KEY, valueAsString) 518 | } 519 | def evaluateParsingEnum[TEnum <: Enum[TEnum]](key: MessageKey, valueAsString: String, messages: Messages)(implicit classManifest: Manifest[TEnum]) 520 | { 521 | argumentIsNotNull(key) 522 | argumentIsNotNull(valueAsString) 523 | argumentIsNotNull(messages) 524 | argumentIsNotNull(classManifest) 525 | 526 | val enumClass = classManifest.runtimeClass.asInstanceOf[Class[TEnum]] 527 | 528 | try { Enum.valueOf[TEnum](enumClass, valueAsString) } 529 | catch { case _: Throwable => messages.putError(key, ErrorMessages.createDefaultParsingError[TEnum](valueAsString))} 530 | } 531 | def canParseEnum[TEnum <: Enum[TEnum]](key: MessageKey, valueAsString: String)(implicit classManifest: Manifest[TEnum]) = 532 | { 533 | argumentIsNotNull(key) 534 | argumentIsNotNull(classManifest) 535 | 536 | val messages = Messages.of 537 | this.evaluateParsingEnum[TEnum](key, valueAsString, messages) 538 | 539 | !messages.hasErrors 540 | } 541 | 542 | def canParseEnum[TEnum <: Enum[TEnum]](valueAsString: String)(implicit classManifest: Manifest[TEnum]) = 543 | { 544 | argumentIsNotNull(classManifest) 545 | 546 | val messages = Messages.of 547 | this.evaluateParsingEnum[TEnum](ERROR_DEFAULT_KEY, valueAsString, messages) 548 | 549 | !messages.hasErrors 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/actor/actionlog/ActionLogActor.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.actor.actionlog 2 | 3 | import akka.actor.{Actor, ActorLogging} 4 | import com.josip.reactiveluxury.core.utils.DateUtils 5 | import com.josip.reactiveluxury.module.domain.actionlog.ActionLogEntity 6 | import play.api.libs.json.Json 7 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 8 | import com.josip.reactiveluxury.configuration.DatabaseProvider 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | case class ActionLogCreateMsg(actionLog: ActionLogEntity) 13 | class ActionLogActor(databaseProvider: DatabaseProvider, implicit val ec : ExecutionContext) extends Actor with ActorLogging { 14 | 15 | def receive = { 16 | case ActionLogCreateMsg(actionLog) => 17 | log.debug(s"ActionLog received.") 18 | 19 | val action = ActionLogActor.INSERT_QUERY(actionLog).transactionally 20 | val actionInsertFuture = databaseProvider.db.run(action) 21 | 22 | actionInsertFuture.onSuccess { 23 | case e => 24 | log.info(s"Action logged. DomainType: '${actionLog.domainType.name}', DomainId: '${actionLog.domainId}', Action: '${actionLog.actionType.name}'") 25 | } 26 | actionInsertFuture.onFailure { 27 | case e => 28 | log.info(s"Action insert failed.Exception: '${e.toString}'") 29 | } 30 | 31 | case _ => log.error("ActionLogActor received invalid message.") 32 | } 33 | } 34 | 35 | object ActionLogActor { 36 | val NAME = "actionLogActorRouter" 37 | 38 | def INSERT_QUERY(actionLog: ActionLogEntity) = { 39 | sqlu""" 40 | INSERT INTO action_log 41 | ( 42 | user_id, 43 | domain_type, 44 | domain_id, 45 | action_type, 46 | before, 47 | after, 48 | created_on 49 | ) VALUES 50 | ( 51 | ${actionLog.userId}, 52 | ${actionLog.domainType.name}, 53 | ${actionLog.domainId}, 54 | ${actionLog.actionType.name}, 55 | ${actionLog.before.map(Json.stringify)}::JSON, 56 | ${actionLog.after.map(Json.stringify)}::JSON, 57 | ${DateUtils.jodaDateTimeToSqlDate(actionLog.createdOn)} 58 | ) 59 | """ 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/dao/actionlog/sql/ActionLogMapper.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.dao.actionlog.sql 2 | 3 | import com.josip.reactiveluxury.core.slick.custommapper.CustomSlickMapper.Postgres._ 4 | import com.josip.reactiveluxury.module.dao.user.sql.UserEntityMapper 5 | import com.josip.reactiveluxury.module.domain.actionlog.{ActionDomainType, ActionLogEntity, ActionType} 6 | import org.joda.time.DateTime 7 | import play.api.libs.json.JsValue 8 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 9 | 10 | object ActionLogMapper 11 | { 12 | final val ACTION_LOG_TABLE_NAME = "action_log" 13 | 14 | final val ID_COLUMN = "id" 15 | final val USER_ID_COLUMN = "user_id" 16 | final val DOMAIN_TYPE_COLUMN = "domain_type" 17 | final val DOMAIN_ID_COLUMN = "domain_id" 18 | final val ACTION_TYPE_COLUMN = "action_type" 19 | final val BEFORE_COLUMN = "before" 20 | final val AFTER_COLUMN = "after" 21 | final val CREATED_ON_COLUMN = "created_on" 22 | 23 | implicit val actionDomainTypeColumnType = enumColumnType[ActionDomainType] 24 | implicit val actionTypeColumnType = enumColumnType[ActionType] 25 | 26 | class ActionLogMapper(tag: Tag) extends Table[ActionLogEntity](tag, ACTION_LOG_TABLE_NAME) { 27 | def id = column[Long] (ID_COLUMN, O.PrimaryKey, O.AutoInc ) 28 | def userId = column[Option[Long]] (USER_ID_COLUMN) 29 | def domainType = column[ActionDomainType](DOMAIN_TYPE_COLUMN) 30 | def domainId = column[Long] (DOMAIN_ID_COLUMN) 31 | def actionType = column[ActionType] (ACTION_TYPE_COLUMN) 32 | def before = column[JsValue] (BEFORE_COLUMN) 33 | def after = column[JsValue] (AFTER_COLUMN) 34 | def createdOn = column[DateTime] (CREATED_ON_COLUMN) 35 | 36 | def user = foreignKey("id", userId, UserEntityMapper.UserTableDescriptor.user)(_.id.?) 37 | 38 | def * = ( 39 | id.?, 40 | userId, 41 | domainType, 42 | domainId, 43 | actionType, 44 | before.?, 45 | after.?, 46 | createdOn 47 | ) <> ((ActionLogEntity.apply _).tupled, ActionLogEntity.unapply) 48 | } 49 | object ActionLogMapper { 50 | def query = TableQuery[ActionLogMapper] 51 | def queryWithId = query returning query.map(_.id) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/dao/user/UserRepository.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.dao.user 2 | 3 | import com.google.inject.ImplementedBy 4 | import com.josip.reactiveluxury.core.Asserts 5 | import com.josip.reactiveluxury.core.json.customfomatters.CustomFormatter.Enum 6 | import com.josip.reactiveluxury.core.slick.generic.CrudRepository 7 | import com.josip.reactiveluxury.module.dao.user.sql.UserRepositoryImpl 8 | import com.josip.reactiveluxury.module.domain.user.{UserStatus, UserRole, User} 9 | import play.api.libs.json.Json 10 | import slick.jdbc.GetResult 11 | 12 | import scala.concurrent.Future 13 | 14 | @ImplementedBy(classOf[UserRepositoryImpl]) 15 | trait UserRepository extends CrudRepository[User] { 16 | def findByEmail(email: String): Future[Option[User]] 17 | def verify(userId: Long) 18 | 19 | def findByIdMinimalDetails(id: Long): Future[Option[UserRepository.UserMinimalDetails]] 20 | def findByEmailMinimalDetails(email: String): Future[Option[UserRepository.UserMinimalDetails]] 21 | def findByExternalIdMinimalDetails(externalId: String): Future[Option[UserRepository.UserMinimalDetails]] 22 | } 23 | 24 | object UserRepository { 25 | case class UserMinimalDetails 26 | ( 27 | id : Long, 28 | externalId : String, 29 | email : String, 30 | role : UserRole, 31 | password : String, 32 | status : UserStatus 33 | ) { 34 | Asserts.argumentIsNotNull(externalId) 35 | Asserts.argumentIsNotNull(email) 36 | } 37 | 38 | object UserMinimalDetails { 39 | import Enum.enumFormatByName 40 | 41 | implicit val jsonFormat = Json.format[UserMinimalDetails] 42 | 43 | implicit val getResult = GetResult( 44 | r => UserMinimalDetails( 45 | id = r.nextLong(), 46 | externalId = r.nextString(), 47 | email = r.nextString(), 48 | role = UserRole.valueOf(r.nextString()), 49 | password = r.nextString(), 50 | status = UserStatus.valueOf(r.nextString()) 51 | ) 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/dao/user/sql/UserEntityMapper.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.dao.user.sql 2 | 3 | import com.josip.reactiveluxury.module.domain.user._ 4 | import org.joda.time.{DateTime, LocalDate} 5 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 6 | import com.josip.reactiveluxury.core.slick.custommapper.CustomSlickMapper.Postgres._ 7 | import com.josip.reactiveluxury.core.slick.generic.IdentifiableTable 8 | 9 | object UserEntityMapper { 10 | final val USERS_TABLE_NAME = "app_user" 11 | 12 | implicit val userStatusMapper = enumColumnType[UserStatus] 13 | implicit val userRolwMapper = enumColumnType[UserRole] 14 | implicit val genderMapper = enumColumnType[Gender] 15 | 16 | class UserTableDescriptor(tag: Tag) extends Table[User](tag, USERS_TABLE_NAME) with IdentifiableTable 17 | { 18 | def id = column[Long] ("id", O.PrimaryKey, O.AutoInc ) 19 | def externalId = column[String] ("external_id") 20 | def createdOn = column[DateTime] ("created_on") 21 | def modifiedOn = column[DateTime] ("modified_on") 22 | 23 | def status = column[UserStatus]("status") 24 | def role = column[UserRole]("role") 25 | def firstName = column[Option[String]]("first_name") 26 | def lastName = column[Option[String]]("last_name") 27 | def email = column[String] ("email") 28 | def password = column[String] ("password") 29 | def dateOfBirth = column[LocalDate] ("date_of_birth") 30 | def gender = column[Gender] ("gender") 31 | def contactAddress = column[Option[String]]("contact__address") 32 | def contactCity = column[Option[String]]("contact__city") 33 | def contactZip = column[Option[String]]("contact__zip") 34 | def contactPhone = column[Option[String]]("contact__phone") 35 | def height = column[Option[Int]] ("height") 36 | def weight = column[Option[Int]] ("weight") 37 | 38 | 39 | def contactDetails = ( 40 | contactAddress, 41 | contactCity, 42 | contactZip, 43 | contactPhone 44 | ) <>((Contact.apply _).tupled, Contact.unapply) 45 | 46 | def * = ( 47 | id.?, 48 | externalId.?, 49 | createdOn.?, 50 | modifiedOn.?, 51 | status, 52 | role, 53 | firstName, 54 | lastName, 55 | email, 56 | password, 57 | dateOfBirth, 58 | gender, 59 | contactDetails, 60 | height, 61 | weight 62 | ) <> ((User.apply _).tupled, User.unapply) 63 | 64 | } 65 | 66 | object UserTableDescriptor { 67 | def user = TableQuery[UserTableDescriptor] 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/dao/user/sql/UserRepositoryImpl.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.dao.user.sql 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.josip.reactiveluxury.configuration.DatabaseProvider 6 | import com.josip.reactiveluxury.core.Asserts 7 | import com.josip.reactiveluxury.core.slick.generic.CrudRepositoryImpl 8 | import com.josip.reactiveluxury.module.dao.user.UserRepository 9 | import com.josip.reactiveluxury.module.dao.user.sql.UserEntityMapper._ 10 | import com.josip.reactiveluxury.module.domain.user.{User, UserStatus} 11 | import com.josip.reactiveluxury.core.slick.postgres.MyPostgresDriver.api._ 12 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | 16 | @Singleton() 17 | class UserRepositoryImpl @Inject() ( 18 | databaseProvider: DatabaseProvider, 19 | implicit override val ec : ExecutionContext 20 | ) extends CrudRepositoryImpl[UserTableDescriptor, User](databaseProvider, UserTableDescriptor.user, ec) with UserRepository { 21 | Asserts.argumentIsNotNull(databaseProvider) 22 | 23 | override def findByEmail(email: String): Future[Option[User]] = { 24 | Asserts.argumentIsNotNullNorEmpty(email) 25 | 26 | val action = UserTableDescriptor.user.filter(_.email === email).result.headOption 27 | databaseProvider.db.run(action) 28 | } 29 | 30 | override def verify(userId: Long) { 31 | val action = this.query.filter(_.id === userId) 32 | .map(_.status) 33 | .update(UserStatus.VERIFIED) 34 | .transactionally 35 | 36 | this.databaseProvider.db.run(action) 37 | } 38 | 39 | override def findByIdMinimalDetails(id: Long): Future[Option[UserRepository.UserMinimalDetails]] = { 40 | val selectQuery = 41 | sql""" 42 | SELECT id, external_id, email, role, password, status 43 | FROM public.app_user 44 | WHERE id = $id 45 | """.as[UserRepository.UserMinimalDetails] 46 | 47 | for { 48 | item <- databaseProvider.db.run(selectQuery.headOption) handleError() 49 | } yield item 50 | } 51 | 52 | override def findByEmailMinimalDetails(email: String): Future[Option[UserRepository.UserMinimalDetails]] = { 53 | Asserts.argumentIsNotNull(email) 54 | 55 | val selectQuery = 56 | sql""" 57 | SELECT id, external_id, email, role, password, status 58 | FROM public.app_user 59 | WHERE email = $email 60 | """.as[UserRepository.UserMinimalDetails] 61 | 62 | for { 63 | item <- databaseProvider.db.run(selectQuery.headOption) handleError() 64 | } yield item 65 | } 66 | 67 | override def findByExternalIdMinimalDetails(externalId: String): Future[Option[UserRepository.UserMinimalDetails]] = { 68 | Asserts.argumentIsNotNull(externalId) 69 | 70 | val selectQuery = 71 | sql""" 72 | SELECT id, external_id, email, role, password, status 73 | FROM public.app_user 74 | WHERE external_id = $externalId 75 | """.as[UserRepository.UserMinimalDetails] 76 | 77 | for { 78 | item <- databaseProvider.db.run(selectQuery.headOption) handleError() 79 | } yield item 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/actionlog/ActionDomainType.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.actionlog; 2 | 3 | import com.josip.reactiveluxury.core.Asserts; 4 | import com.josip.reactiveluxury.module.domain.user.User; 5 | 6 | public enum ActionDomainType { 7 | USER(User.class); 8 | 9 | private ActionDomainType(Class entityClass) { 10 | this.entityClass = entityClass; 11 | } 12 | private final Class entityClass; 13 | 14 | public Class getEntityClass() { 15 | return this.entityClass; 16 | } 17 | 18 | public static ActionDomainType getByEntityClass(Class entityClass) { 19 | Asserts.argumentIsNotNull(entityClass); 20 | 21 | for(ActionDomainType at: ActionDomainType.values()) { 22 | if(at.getEntityClass().equals(entityClass)) { 23 | return at; 24 | } 25 | } 26 | 27 | throw new IllegalArgumentException(String.format("There is not ActionDomainType with provided entityClass: '%s'", entityClass.getName())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/actionlog/ActionLogEntity.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.actionlog 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import org.joda.time.DateTime 5 | import play.api.libs.json.{JsValue, Json, Writes} 6 | 7 | case class ActionLogEntity( 8 | id : Option[Long] = None, 9 | userId : Option[Long], 10 | domainType : ActionDomainType, 11 | domainId : Long, 12 | actionType : ActionType, 13 | before : Option[JsValue], 14 | after : Option[JsValue], 15 | createdOn : DateTime = DateTime.now 16 | ) 17 | { 18 | Asserts.argumentIsNotNull(id) 19 | Asserts.argumentIsNotNull(domainType) 20 | Asserts.argumentIsNotNull(actionType) 21 | Asserts.argumentIsNotNull(before) 22 | Asserts.argumentIsNotNull(after) 23 | Asserts.argumentIsNotNull(createdOn) 24 | } 25 | 26 | object ActionLogEntity { 27 | def of[TBefore: Writes, TAfter: Writes]( 28 | userId : Long, 29 | domainType : ActionDomainType, 30 | domainId : Long, 31 | actionType : ActionType, 32 | before : Option[TBefore], 33 | after : Option[TAfter] 34 | ): ActionLogEntity = { 35 | Asserts.argumentIsNotNull(userId) 36 | Asserts.argumentIsNotNull(domainType) 37 | Asserts.argumentIsNotNull(actionType) 38 | Asserts.argumentIsNotNull(before) 39 | Asserts.argumentIsNotNull(after) 40 | 41 | ActionLogEntity( 42 | userId = Some(userId), 43 | domainType = domainType, 44 | domainId = domainId, 45 | actionType = actionType, 46 | before = if(before.isDefined) Some(Json.toJson(before.get)) else Option.empty[JsValue], 47 | after = if(after.isDefined) Some(Json.toJson(after.get)) else Option.empty[JsValue] 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/actionlog/ActionType.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.actionlog; 2 | 3 | public enum ActionType 4 | { 5 | CREATED("CREATED"), 6 | UPDATED("UPDATED"); 7 | 8 | private ActionType(String displayName) 9 | { 10 | this.displayName = displayName; 11 | } 12 | private final String displayName; 13 | 14 | public String displayName() 15 | { 16 | return this.displayName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/authentication/AuthenticationConfiguration.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.authentication 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | 5 | abstract class AuthenticationConfiguration( 6 | final val secret : String, 7 | final val tokenHoursToLive : Int 8 | ) { 9 | Asserts.argumentIsNotNullNorEmpty(secret) 10 | Asserts.argumentIsNotNull(tokenHoursToLive) 11 | Asserts.argumentIsTrue(tokenHoursToLive > 0) 12 | } 13 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/Contact.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import play.api.libs.json.Json 5 | 6 | case class Contact ( 7 | address: Option[String], 8 | city: Option[String], 9 | zip: Option[String], 10 | phone: Option[String] 11 | ) { 12 | Asserts.argumentIsNotNull(address) 13 | Asserts.argumentIsNotNull(city) 14 | Asserts.argumentIsNotNull(zip) 15 | Asserts.argumentIsNotNull(phone) 16 | } 17 | 18 | object Contact { 19 | implicit val jsonFormat = Json.format[Contact] 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/Gender.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user; 2 | 3 | public enum Gender { 4 | M, 5 | F; 6 | } 7 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/SystemUser.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user 2 | 3 | /** 4 | * Created by mozzer on 3/17/16. 5 | */ 6 | case object SystemUser { 7 | val id = 0L 8 | val optId = Some(this.id) 9 | } 10 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/User.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.josip.reactiveluxury.core.slick.generic.Entity 5 | import com.josip.reactiveluxury.core.utils.DateUtils 6 | import org.joda.time.{DateTime, LocalDate} 7 | import play.api.libs.json.Json 8 | 9 | case class User 10 | ( 11 | override val id : Option[Long] = Option.empty, 12 | override val externalId : Option[String] = Option.empty, 13 | override val createdOn : Option[DateTime] = Some(DateUtils.nowDateTimeUTC), 14 | override val modifiedOn : Option[DateTime] = Some(DateUtils.nowDateTimeUTC), 15 | status : UserStatus, 16 | role : UserRole, 17 | firstName : Option[String], 18 | lastName : Option[String], 19 | email : String, 20 | password : String, 21 | dateOfBirth : LocalDate, 22 | gender : Gender, 23 | contact : Contact, 24 | height : Option[Int], 25 | weight : Option[Int] 26 | ) extends Entity[User] { 27 | selfReference => 28 | 29 | Asserts.argumentIsNotNull(id) 30 | Asserts.argumentIsNotNull(externalId) 31 | Asserts.argumentIsNotNull(createdOn) 32 | Asserts.argumentIsNotNull(modifiedOn) 33 | Asserts.argumentIsNotNull(status) 34 | Asserts.argumentIsNotNull(role) 35 | Asserts.argumentIsNotNull(firstName) 36 | Asserts.argumentIsNotNull(lastName) 37 | Asserts.argumentIsNotNull(email) 38 | Asserts.argumentIsNotNull(password) 39 | Asserts.argumentIsNotNull(dateOfBirth) 40 | Asserts.argumentIsNotNull(gender) 41 | Asserts.argumentIsNotNull(contact) 42 | Asserts.argumentIsNotNull(height) 43 | Asserts.argumentIsNotNull(weight) 44 | 45 | lazy val withoutPassword = selfReference.copy(password = "n/a") 46 | 47 | lazy val displayName = if(firstName.isDefined & lastName.isDefined) { 48 | s"${firstName.get} ${lastName.get}" 49 | } else { 50 | email 51 | } 52 | } 53 | 54 | object User { 55 | 56 | import com.josip.reactiveluxury.core.json.customfomatters.CustomFormatter.Joda._ 57 | import com.josip.reactiveluxury.core.json.customfomatters.CustomFormatter.Enum._ 58 | 59 | implicit val jsonFormat = Json.format[User] 60 | 61 | def of(item: UserCreateModel) = { 62 | Asserts.argumentIsNotNull(item) 63 | 64 | val now = DateUtils.nowDateTimeUTC 65 | 66 | User( 67 | createdOn = Some(now), 68 | modifiedOn= Some(now), 69 | status = UserStatus.NOT_VERIFIED, 70 | role = item.role, 71 | firstName = Some(item.firstName), 72 | lastName = Some(item.lastName), 73 | email = item.email, 74 | password = item.password, 75 | dateOfBirth = item.dateOfBirth, 76 | gender = item.gender, 77 | contact = item.contact, 78 | height = item.height, 79 | weight = item.weight 80 | ) 81 | } 82 | } -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/UserCreateModel.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user 2 | 3 | import com.josip.reactiveluxury.core.Asserts 4 | import com.josip.reactiveluxury.core.messages.MessageKey 5 | import org.joda.time.LocalDate 6 | import play.api.libs.json.Json 7 | 8 | case class UserCreateModel( 9 | firstName : String, 10 | lastName : String, 11 | email : String, 12 | password : String, 13 | dateOfBirth : LocalDate, 14 | gender : Gender, 15 | contact : Contact, 16 | height : Option[Int], 17 | weight : Option[Int] 18 | ) { 19 | Asserts.argumentIsNotNull(firstName) 20 | Asserts.argumentIsNotNull(lastName) 21 | Asserts.argumentIsNotNull(email) 22 | Asserts.argumentIsNotNull(password) 23 | Asserts.argumentIsNotNull(dateOfBirth) 24 | Asserts.argumentIsNotNull(gender) 25 | Asserts.argumentIsNotNull(contact) 26 | Asserts.argumentIsNotNull(height) 27 | Asserts.argumentIsNotNull(weight) 28 | 29 | val role = UserRole.COMPETITOR 30 | } 31 | 32 | object UserCreateModel { 33 | 34 | import com.josip.reactiveluxury.core.json.customfomatters.CustomFormatter.Joda._ 35 | import com.josip.reactiveluxury.core.json.customfomatters.CustomFormatter.Enum._ 36 | 37 | implicit val jsonFormat = Json.format[UserCreateModel] 38 | 39 | val FIRST_NAME_FORM_ID = MessageKey("firstName") 40 | val LAST_NAME_FORM_ID = MessageKey("lastName") 41 | val EMAIL_FORM_ID = MessageKey("email") 42 | val PASSWORD_FORM_ID = MessageKey("password") 43 | val DATE_OF_BIRTH_FORM_ID = MessageKey("dateOfBirth") 44 | val GENDER_FORM_ID = MessageKey("gender") 45 | val CONTACT_FORM_ID = MessageKey("contact") 46 | val HEIGHT_FORM_ID = MessageKey("height") 47 | val WEIGHT_FORM_ID = MessageKey("weight") 48 | } 49 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user; 2 | 3 | public enum UserRole { 4 | ADMIN, 5 | COMPETITOR, 6 | SYSTEM_USER; 7 | 8 | public boolean isSystemUser() { 9 | return this == UserRole.SYSTEM_USER; 10 | } 11 | 12 | public boolean isAdmin() { 13 | return this == UserRole.ADMIN; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/domain/user/UserStatus.java: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.domain.user; 2 | 3 | import com.josip.reactiveluxury.core.Asserts; 4 | 5 | public enum UserStatus { 6 | VERIFIED(1L), 7 | NOT_VERIFIED(2L); 8 | UserStatus(Long id){ 9 | this.id = id; 10 | } 11 | private final Long id; 12 | 13 | public Long getId() { 14 | return this.id; 15 | } 16 | 17 | public boolean isVerified() { 18 | return this == UserStatus.VERIFIED; 19 | } 20 | 21 | public static UserStatus of(Long idCandidate) { 22 | Asserts.argumentIsNotNull(idCandidate); 23 | 24 | if(UserStatus.VERIFIED.getId().equals(idCandidate)) 25 | return UserStatus.VERIFIED; 26 | else if(UserStatus.NOT_VERIFIED.getId().equals(idCandidate)) 27 | return UserStatus.NOT_VERIFIED; 28 | else 29 | throw new IllegalArgumentException(String.format("There is not UserStatus with provided id: '%s'", idCandidate)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/service/domain/authentication/AuthenticationService.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.service.domain.authentication 2 | 3 | import com.google.inject.ImplementedBy 4 | import com.josip.reactiveluxury.configuration.AuthenticationConfigurationSetup 5 | import com.josip.reactiveluxury.core.jwt.ResponseToken 6 | import com.josip.reactiveluxury.core.messages.Messages 7 | import com.josip.reactiveluxury.module.domain.user.User 8 | 9 | import scala.concurrent.Future 10 | 11 | @ImplementedBy(classOf[AuthenticationServiceImpl]) 12 | trait AuthenticationService { 13 | def authenticate(email: String, password: String): Future[(Option[ResponseToken], Messages)] 14 | def refreshToken(token : String) : Future[Option[ResponseToken]] 15 | def validateToken(token: String): Future[Boolean] 16 | def getUserFromToken(token : String) : Future[User] 17 | 18 | def authenticationConfiguration(): AuthenticationConfigurationSetup 19 | } 20 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/service/domain/authentication/AuthenticationServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.service.domain.authentication 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.josip.reactiveluxury.configuration.AuthenticationConfigurationSetup 6 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 7 | import com.josip.reactiveluxury.core.Asserts 8 | import com.josip.reactiveluxury.core.jwt._ 9 | import com.josip.reactiveluxury.core.messages.Messages 10 | import com.josip.reactiveluxury.core.response.ResponseTools 11 | import com.josip.reactiveluxury.core.utils.{DateUtils, HashUtils, StringUtils} 12 | import com.josip.reactiveluxury.module.dao.user.UserRepository 13 | import com.josip.reactiveluxury.module.domain.user.User 14 | import com.josip.reactiveluxury.module.service.domain.user.UserDomainService 15 | import play.api.Logger 16 | import play.api.libs.json.Json 17 | 18 | import scala.concurrent.{ExecutionContext, Future} 19 | 20 | @Singleton() 21 | class AuthenticationServiceImpl @Inject() 22 | ( 23 | override val authenticationConfiguration : AuthenticationConfigurationSetup, 24 | val userDomainService : UserDomainService, 25 | implicit val ec : ExecutionContext 26 | ) extends AuthenticationService { 27 | Asserts.argumentIsNotNull(authenticationConfiguration) 28 | Asserts.argumentIsNotNull(userDomainService) 29 | 30 | implicit private final val SECRET = JwtSecret(this.authenticationConfiguration.secret) 31 | 32 | private final val AUTHORIZATION_TOKEN_VALID_START = "Bearer " 33 | 34 | override def authenticate(email: String, password: String): Future[(Option[ResponseToken], Messages)] = { 35 | Asserts.argumentIsNotNull(email) 36 | Asserts.argumentIsNotNull(password) 37 | 38 | for { 39 | user <- this.userDomainService.tryGetByEmailMinimalDetails(email) handleError() 40 | result <- { 41 | isUserValidForAuthentication(user, email, password) match { 42 | case true => 43 | Future.successful(Some(createAuthenticationToken(user.get)), Messages.of) 44 | case false => 45 | val messages = Messages.of 46 | if(user.isDefined) { 47 | if(!user.get.status.isVerified) { 48 | messages.putWarning(ResponseTools.GLOBAL_MESSAGE_KEY, "User is not verified.") 49 | } else { 50 | messages.putError(ResponseTools.GLOBAL_MESSAGE_KEY, "Bad email or password") 51 | } 52 | } else { 53 | messages.putWarning(ResponseTools.GLOBAL_MESSAGE_KEY, "Bad email or password") 54 | } 55 | 56 | Future.successful((None, messages)) 57 | } 58 | } 59 | } yield result 60 | } 61 | 62 | override def validateToken(token: String): Future[Boolean] = { 63 | Asserts.argumentIsNotNull(token) 64 | 65 | if(token.startsWith(AUTHORIZATION_TOKEN_VALID_START)) { 66 | JwtUtil.getPayloadIfValidToken[TokenPayload](token.replaceFirst(AUTHORIZATION_TOKEN_VALID_START, StringUtils.EMPTY_STRING)).map { 67 | case Some(tp) => tp.expiration.isAfterNow 68 | case None => false 69 | } 70 | } else { 71 | Future.successful(false) 72 | } 73 | } 74 | 75 | override def refreshToken(token : String) : Future[Option[ResponseToken]] = { 76 | Asserts.argumentIsNotNullNorEmpty(token) 77 | 78 | JwtUtil.getPayloadIfValidToken[TokenPayload](token).flatMap{ 79 | case Some(tp) => 80 | for { 81 | user <- this.userDomainService.tryGetByIdMinimalDetails(tp.userId) handleError() 82 | } yield Some(createAuthenticationToken(user.get)) 83 | case None => 84 | Future.successful(None) 85 | } 86 | } 87 | 88 | def getUserFromToken(token : String) : Future[User] = { 89 | Asserts.argumentIsNotNullNorEmpty(token) 90 | 91 | for { 92 | payloadCandidate <- JwtUtil.getPayloadIfValidToken[TokenPayload](token.replaceFirst(AUTHORIZATION_TOKEN_VALID_START, StringUtils.EMPTY_STRING)) handleError() 93 | user <- { 94 | payloadCandidate match { 95 | case Some(payload) => this.userDomainService.getById(payload.userId) 96 | case None => throw new IllegalStateException(s"TokenPayload must exist at this point. Token: '$token'") 97 | } 98 | } 99 | } yield user 100 | } 101 | 102 | private def createAuthenticationToken(user : UserRepository.UserMinimalDetails) : ResponseToken = { 103 | Asserts.argumentIsNotNull(user) 104 | 105 | val tokenPayload = TokenPayload( 106 | userId = user.id, 107 | email = s"${user.email}", 108 | expiration = DateUtils.nowDateTimeUTC.plusHours(this.authenticationConfiguration.tokenHoursToLive) 109 | ) 110 | 111 | ResponseToken(JwtUtil.signJwtPayload(tokenPayload)) 112 | } 113 | 114 | private def isUserValidForAuthentication(user: Option[UserRepository.UserMinimalDetails], email: String, password: String): Boolean = { 115 | val messages = Messages.of 116 | 117 | if(user.isEmpty) { 118 | messages.putError(s"No existing user with provided credentials. email: '$email'") 119 | } 120 | 121 | if(!messages.hasErrors()) { 122 | if(!user.get.password.equals(HashUtils.sha1(password))) { 123 | messages.putError(s"Password do not match. email: '$email'") 124 | } 125 | 126 | if(!user.get.status.isVerified) { 127 | messages.putError(s"User is not verified. user: '${Json.toJson(user.get)}'") 128 | } 129 | 130 | if(user.get.role.isSystemUser) { 131 | messages.putError(s"User is system user. user: '${Json.toJson(user.get)}'") 132 | } 133 | } 134 | 135 | if(messages.hasErrors()) { 136 | Logger.logger.info(s"Authentication failed. Reason: '${messages.errors().map(_.text).mkString(StringUtils.COMMA_SEPARATOR)}'") 137 | } 138 | 139 | !messages.hasErrors() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/service/domain/user/UserDomainService.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.service.domain.user 2 | 3 | import com.google.inject.ImplementedBy 4 | import com.josip.reactiveluxury.core.Asserts 5 | import com.josip.reactiveluxury.core.service.EntityService 6 | import com.josip.reactiveluxury.module.dao.user.UserRepository 7 | import com.josip.reactiveluxury.module.domain.user.User 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | 11 | @ImplementedBy(classOf[UserDomainServiceImpl]) 12 | trait UserDomainService extends EntityService[User] { 13 | def tryGetByEmail(email: String): Future[Option[User]] 14 | 15 | def doesExistByByEmail(email: String)(implicit ec : ExecutionContext): Future[Boolean] = { 16 | Asserts.argumentIsNotNull(email) 17 | 18 | this.tryGetByEmail(email).map(_.isDefined) 19 | } 20 | 21 | def verify(userId: Long) 22 | 23 | def tryGetByIdMinimalDetails(id: Long): Future[Option[UserRepository.UserMinimalDetails]] 24 | def tryGetByEmailMinimalDetails(email: String): Future[Option[UserRepository.UserMinimalDetails]] 25 | def tryGetByExternalIdMinimalDetails(externalId: String): Future[Option[UserRepository.UserMinimalDetails]] 26 | } 27 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/service/domain/user/UserDomainServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.service.domain.user 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.ActorSystem 6 | import com.josip.reactiveluxury.core.Asserts 7 | import com.josip.reactiveluxury.core.service.EntityServiceImpl 8 | import com.josip.reactiveluxury.module.dao.user.UserRepository 9 | import com.josip.reactiveluxury.module.domain.user.User 10 | 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | @Singleton() 14 | class UserDomainServiceImpl @Inject() ( 15 | override val entityRepository: UserRepository, 16 | actorSystem: ActorSystem, 17 | implicit override val ec : ExecutionContext 18 | ) extends EntityServiceImpl[User, UserRepository](entityRepository, actorSystem) with UserDomainService { 19 | Asserts.argumentIsNotNull(entityRepository) 20 | 21 | override def tryGetByEmail(email: String): Future[Option[User]] = { 22 | Asserts.argumentIsNotNull(email) 23 | 24 | this.entityRepository.findByEmail(email) 25 | } 26 | 27 | override def verify(userId: Long) { 28 | this.entityRepository.verify(userId) 29 | } 30 | 31 | override def tryGetByIdMinimalDetails(id: Long): Future[Option[UserRepository.UserMinimalDetails]] = { 32 | this.entityRepository.findByIdMinimalDetails(id) 33 | } 34 | 35 | override def tryGetByEmailMinimalDetails(email: String): Future[Option[UserRepository.UserMinimalDetails]] = { 36 | Asserts.argumentIsNotNull(email) 37 | 38 | this.entityRepository.findByEmailMinimalDetails(email) 39 | } 40 | 41 | override def tryGetByExternalIdMinimalDetails(externalId: String): Future[Option[UserRepository.UserMinimalDetails]] = { 42 | Asserts.argumentIsNotNull(externalId) 43 | 44 | this.entityRepository.findByExternalIdMinimalDetails(externalId) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/com/josip/reactiveluxury/module/validation/user/UserCreateValidator.scala: -------------------------------------------------------------------------------- 1 | package com.josip.reactiveluxury.module.validation.user 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 6 | import com.josip.reactiveluxury.core.messages.Messages 7 | import com.josip.reactiveluxury.core.utils.ReactiveValidateUtils 8 | import com.josip.reactiveluxury.core.utils.ReactiveValidateUtils.continueIfMessagesHaveErrors 9 | import com.josip.reactiveluxury.core.{Asserts, ValidationResult, Validator} 10 | import com.josip.reactiveluxury.module.domain.user.UserCreateModel 11 | import com.josip.reactiveluxury.module.service.domain.user.UserDomainService 12 | 13 | import scala.concurrent.{ExecutionContext, Future} 14 | 15 | @Singleton() 16 | class UserCreateValidator @Inject() 17 | ( 18 | private val entityDomainService : UserDomainService, 19 | implicit val ec : ExecutionContext 20 | ) extends Validator[UserCreateModel] { 21 | Asserts.argumentIsNotNull(entityDomainService) 22 | 23 | override def validate(item: UserCreateModel, userId: Option[Long]): Future[ValidationResult[UserCreateModel]] = { 24 | Asserts.argumentIsNotNull(item) 25 | 26 | val validationMessages = Messages.of 27 | for { 28 | _ <- validateFirstName (item, validationMessages) handleError() 29 | _ <- validateLastName (item, validationMessages) handleError() 30 | _ <- validatePassword (item, validationMessages) handleError() 31 | _ <- validateEmail(item, validationMessages) handleError() 32 | _ <- validateContact(item, validationMessages) handleError() 33 | } yield ValidationResult( 34 | validatedItem = item, 35 | messages = validationMessages 36 | ) 37 | } 38 | 39 | private def validateFirstName(item: UserCreateModel, validationMessages: Messages): Future[Messages] = { 40 | val localMessages = Messages.of(validationMessages) 41 | val fieldValue = item.firstName 42 | 43 | for { 44 | _ <- ReactiveValidateUtils.isNotNullNorEmpty( 45 | fieldValue, 46 | localMessages, 47 | UserCreateModel.FIRST_NAME_FORM_ID.value, 48 | "must be defined" 49 | ) 50 | _ <- continueIfMessagesHaveErrors(localMessages) ( 51 | ReactiveValidateUtils.validateLengthIsLessThanOrEqual( 52 | fieldValue, 53 | 255, 54 | localMessages, 55 | UserCreateModel.FIRST_NAME_FORM_ID.value, 56 | "must be less than or equal to 255 character" 57 | ) 58 | ) 59 | 60 | } yield localMessages 61 | } 62 | 63 | private def validateLastName(item: UserCreateModel, validationMessages: Messages): Future[Messages] = { 64 | val localMessages = Messages.of(validationMessages) 65 | val fieldValue = item.lastName 66 | 67 | for { 68 | _ <- ReactiveValidateUtils.isNotNullNorEmpty( 69 | fieldValue, 70 | localMessages, 71 | UserCreateModel.LAST_NAME_FORM_ID.value, 72 | "must be defined" 73 | ) 74 | _ <- continueIfMessagesHaveErrors(localMessages) ( 75 | ReactiveValidateUtils.validateLengthIsLessThanOrEqual( 76 | fieldValue, 77 | 255, 78 | localMessages, 79 | UserCreateModel.LAST_NAME_FORM_ID.value, 80 | "must be less than or equal to 255 character" 81 | ) 82 | ) 83 | } yield localMessages 84 | } 85 | 86 | private def validateEmail(item: UserCreateModel, validationMessages: Messages): Future[Messages] = { 87 | val fieldValue = item.email 88 | val localMessages = Messages.of(validationMessages) 89 | 90 | for { 91 | _ <- ReactiveValidateUtils.isNotNullNorEmpty( 92 | fieldValue, 93 | localMessages, 94 | UserCreateModel.EMAIL_FORM_ID.value, 95 | "Email must be defined" 96 | ) 97 | _ <- continueIfMessagesHaveErrors(localMessages) ( 98 | ReactiveValidateUtils.validateEmail( 99 | fieldValue.toLowerCase, 100 | UserCreateModel.EMAIL_FORM_ID, 101 | localMessages 102 | ) 103 | ) 104 | doesExistWithEmail <- this.entityDomainService.doesExistByByEmail(fieldValue) handleError() 105 | _ <- ReactiveValidateUtils.isFalse( 106 | doesExistWithEmail, 107 | localMessages, 108 | UserCreateModel.EMAIL_FORM_ID.value, 109 | "User already exists with this email" 110 | ) 111 | _ <- ReactiveValidateUtils.validateLengthIsLessThanOrEqual( 112 | fieldValue, 113 | 80, 114 | localMessages, 115 | UserCreateModel.EMAIL_FORM_ID.value, 116 | "must be less than or equal to 80 character" 117 | ) 118 | } yield localMessages 119 | } 120 | 121 | private def validatePassword(item: UserCreateModel, validationMessages: Messages): Future[Messages] = { 122 | val localMessages = Messages.of(validationMessages) 123 | val fieldValue = item.password 124 | 125 | for { 126 | _ <- ReactiveValidateUtils.isNotNullNorEmpty( 127 | fieldValue, 128 | localMessages, 129 | UserCreateModel.PASSWORD_FORM_ID.value, 130 | "must be defined" 131 | ) 132 | _ <- continueIfMessagesHaveErrors(localMessages) ( 133 | ReactiveValidateUtils.isTrue( 134 | fieldValue.length >= 6, 135 | localMessages, 136 | UserCreateModel.PASSWORD_FORM_ID.value, 137 | "password length must not be less than 6" 138 | ) 139 | ) 140 | 141 | _ <- continueIfMessagesHaveErrors(localMessages) ( 142 | ReactiveValidateUtils.isTrue( 143 | fieldValue.length <=255, 144 | localMessages, 145 | UserCreateModel.PASSWORD_FORM_ID.value, 146 | "password length must not be bigger than 255" 147 | ) 148 | ) 149 | } yield localMessages 150 | } 151 | 152 | private def validateContact(item: UserCreateModel, validationMessages: Messages): Future[Messages] = { 153 | val localMessages = Messages.of(validationMessages) 154 | 155 | val fieldValue = item.contact 156 | 157 | for { 158 | _ <- ReactiveValidateUtils.isNotNull( 159 | fieldValue, 160 | localMessages, 161 | UserCreateModel.CONTACT_FORM_ID.value, 162 | "must be defined" 163 | ) 164 | } yield localMessages 165 | } 166 | } -------------------------------------------------------------------------------- /app/controllers/AuthenticationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import com.josip.reactiveluxury.core.response.ResponseTools 6 | import com.josip.reactiveluxury.module.service.domain.authentication.AuthenticationService 7 | import com.josip.reactiveluxury.module.service.domain.user.UserDomainService 8 | import com.josip.reactiveluxury.core.Asserts 9 | import play.api.libs.json.Json 10 | import play.api.mvc.{Action, Controller} 11 | import com.josip.reactiveluxury.core.authentication.Credentials 12 | import com.josip.reactiveluxury.core.jwt.ResponseToken 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 16 | 17 | class AuthenticationController @Inject() 18 | ( 19 | private val userDomainService : UserDomainService, 20 | private val authenticationService : AuthenticationService, 21 | implicit val ec : ExecutionContext 22 | ) extends Controller { 23 | Asserts.argumentIsNotNull(userDomainService) 24 | Asserts.argumentIsNotNull(authenticationService) 25 | 26 | private final val BAD_EMAIL_OR_PASSWORD_ERROR = "Bad email or password" 27 | 28 | def authenticate = Action.async(parse.json) { 29 | implicit request => 30 | request.body.validate[Credentials].map { 31 | case credentials: Credentials => 32 | authenticationService.authenticate(credentials.email.toLowerCase, credentials.password).map { 33 | case (Some(token), messages) => Ok(Json.toJson(ResponseTools.of[ResponseToken](token, Some(messages) ).json)) 34 | case (None, messages) => Unauthorized(ResponseTools.noData(messages).json) 35 | } handleError() 36 | }.recoverTotal { 37 | error => 38 | Future.successful(BadRequest(ResponseTools.errorToRestResponse(error.errors.flatMap(_._2).map(_.message).head).json)) 39 | } 40 | } 41 | 42 | def refreshToken = Action.async(parse.json) { 43 | implicit request => 44 | request.body.validate[ResponseToken].map { 45 | case authenticationToken: ResponseToken => 46 | authenticationService.refreshToken(authenticationToken.token).map{ 47 | case Some(token) => Ok(Json.toJson(ResponseTools.data(token))) 48 | case None => Unauthorized(ResponseTools.errorToRestResponse(BAD_EMAIL_OR_PASSWORD_ERROR).json) 49 | } handleError() 50 | }.recoverTotal { 51 | error => Future.successful(BadRequest(ResponseTools.errorToRestResponse(error.errors.flatMap(_._2).map(_.message).head).json)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/controllers/api/v1/UserController.scala: -------------------------------------------------------------------------------- 1 | package controllers.api.v1 2 | 3 | import javax.inject.Inject 4 | 5 | import com.josip.reactiveluxury.core.Asserts 6 | import com.josip.reactiveluxury.core.response.ResponseTools 7 | import com.josip.reactiveluxury.core.utils.HashUtils 8 | import com.josip.reactiveluxury.module.domain.user.{User, UserCreateModel} 9 | import com.josip.reactiveluxury.module.service.domain.authentication.AuthenticationService 10 | import com.josip.reactiveluxury.module.service.domain.user.UserDomainService 11 | import com.josip.reactiveluxury.module.validation.user.UserCreateValidator 12 | import controllers.core.SecuredController 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 16 | import com.josip.reactiveluxury.configuration.actor.ActorFactory 17 | 18 | class UserController @Inject() 19 | ( 20 | private val userDomainService : UserDomainService, 21 | private val userCreateValidator : UserCreateValidator, 22 | private val actorFactory : ActorFactory 23 | ) 24 | ( 25 | implicit private val authenticationService: AuthenticationService, 26 | implicit override val ec : ExecutionContext 27 | ) extends SecuredController 28 | { 29 | Asserts.argumentIsNotNull(userDomainService) 30 | Asserts.argumentIsNotNull(userCreateValidator) 31 | Asserts.argumentIsNotNull(authenticationService) 32 | 33 | def read(id: Long) = AuthenticatedActionWithPayload { 34 | (request, tokenPayload) => 35 | for { 36 | itemCandidate <- this.userDomainService.tryGetById(id) 37 | result <- { 38 | if(itemCandidate.isEmpty) { 39 | Future.successful(NotFound(ResponseTools.errorToRestResponse("User with this id does not exist.").json)) 40 | } else { 41 | Future.successful(Ok(ResponseTools.data(itemCandidate.get.withoutPassword).json)) 42 | } 43 | } 44 | } yield result 45 | } 46 | 47 | def create = MutateJsonAction[UserCreateModel](userCreateValidator) { 48 | (request, validationResult) => 49 | val validatedItem = validationResult.validatedItem 50 | val userCreateEntityAfterModification = validatedItem.copy( 51 | password = HashUtils.sha1(validatedItem.password), 52 | email = validatedItem.email.toLowerCase 53 | ) 54 | for { 55 | createdItem <- this.userDomainService.create(User.of(userCreateEntityAfterModification))(None) handleError() 56 | result <- Future.successful(Ok(ResponseTools.of(createdItem.withoutPassword, Some(validationResult.messages)).json)) 57 | } yield result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/controllers/core/SecuredController.scala: -------------------------------------------------------------------------------- 1 | package controllers.core 2 | 3 | import com.josip.reactiveluxury.core.jwt.{JwtSecret, JwtUtil, TokenPayload} 4 | import com.josip.reactiveluxury.core.utils.StringUtils 5 | import com.josip.reactiveluxury.core.{Asserts, ValidationResult, Validator} 6 | import com.josip.reactiveluxury.core.response.ResponseTools 7 | import com.josip.reactiveluxury.module.domain.user.User 8 | import com.josip.reactiveluxury.module.service.domain.authentication.AuthenticationService 9 | import play.api.Logger 10 | import play.api.libs.Files.TemporaryFile 11 | import play.api.libs.json.{Format, JsValue} 12 | import play.api.mvc._ 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | import com.josip.reactiveluxury.configuration.CustomExecutionContext._ 16 | 17 | abstract class SecuredController( 18 | implicit private val authenticationService : AuthenticationService, 19 | implicit val ec: ExecutionContext 20 | ) extends Controller { 21 | Asserts.argumentIsNotNull(authenticationService) 22 | 23 | implicit private final val SECRET = JwtSecret(this.authenticationService.authenticationConfiguration().secret) 24 | 25 | private final val INVALID_TOKEN_ERROR = "Invalid authentication token" 26 | private final val MISSING_TOKEN_ERROR = "Missing authentication token" 27 | private final val AUTH_TOKEN_NOT_FOUND_ERROR = "Authorization token not found in secured endpoint" 28 | private final val AUTHORIZATION_TOKEN_VALID_START = "Bearer " 29 | 30 | def AuthenticatedActionWithPayload(block: (Request[AnyContent], TokenPayload) => Future[Result]): Action[AnyContent] = { 31 | AuthenticatedActionWithPayload(parse.anyContent)(block) 32 | } 33 | 34 | def AuthenticatedActionWithPayload[A](bodyParser: BodyParser[A])(block: (Request[A], TokenPayload) => Future[Result]): Action[A] = { 35 | Action.async(bodyParser) { 36 | request => 37 | request.headers.get(AUTHORIZATION).map(token => { 38 | authenticationService.validateToken(token).flatMap(validationResult => 39 | if (!validationResult) 40 | Future.successful( 41 | Unauthorized(ResponseTools.errorToRestResponse(INVALID_TOKEN_ERROR).json) 42 | ) 43 | else for { 44 | tokenPayload <- JwtUtil.getPayloadIfValidToken[TokenPayload](token.replaceFirst(AUTHORIZATION_TOKEN_VALID_START, StringUtils.EMPTY_STRING)) 45 | response: Result <- block(request, tokenPayload.get) handleError() 46 | } yield response 47 | ) 48 | }).getOrElse( 49 | Future.successful( 50 | Unauthorized(ResponseTools.errorToRestResponse(MISSING_TOKEN_ERROR).json) 51 | )) 52 | } 53 | } 54 | 55 | def FileUploadAuthenticatedAction(block: Request[MultipartFormData[TemporaryFile]] => Future[Result]): Action[MultipartFormData[TemporaryFile]] = { 56 | Action.async(parse.multipartFormData) { 57 | request => 58 | request.headers.get(AUTHORIZATION).map(token => { 59 | authenticationService.validateToken(token).flatMap(validationResult => 60 | if (!validationResult) 61 | Future.successful( 62 | Unauthorized(ResponseTools.errorToRestResponse(INVALID_TOKEN_ERROR).json) 63 | ) 64 | else block(request) handleError() 65 | ) 66 | }).getOrElse( 67 | Future.successful( 68 | Unauthorized(ResponseTools.errorToRestResponse(MISSING_TOKEN_ERROR).json) 69 | )) 70 | } 71 | } 72 | 73 | def MutateJsonAction[T: Format: Manifest](validator: Validator[T])(mutateBlock: (Request[JsValue], ValidationResult[T]) => Future[Result]): Action[JsValue] = { 74 | Action.async(parse.json) { 75 | request => 76 | Logger.logger.info(s"MutateJsonAction called with body: '${request.body}'") 77 | request.body.validate[T].map { 78 | case item if item.getClass == manifest.runtimeClass => 79 | Logger.logger.info(s"MutateJsonAction called with model: '${item.toString}'") 80 | validator.validate(item, None).flatMap( 81 | validationResult => 82 | if(validationResult.isValid) { 83 | mutateBlock(request, validationResult) handleError() 84 | } else { 85 | Future.successful(BadRequest(validationResult.errorsRestResponse.json)) 86 | } 87 | )}.recoverTotal { 88 | error => 89 | Future.successful( 90 | BadRequest(ResponseTools.jsErrorToRestResponse[T](error).json) 91 | ) 92 | } 93 | } 94 | } 95 | 96 | def MutateJsonAuthenticatedActionWithUser[T: Format: Manifest](validator: Validator[T])(mutateBlock: (Request[JsValue], ValidationResult[T], User) => Future[Result]): Action[JsValue] = { 97 | AuthenticatedActionWithPayload(parse.json) { 98 | (request, tokenPayload) => 99 | Logger.logger.info(s"MutateJsonAuthenticatedActionWithUser called with body: '${request.body}'") 100 | request.body.validate[T].map { 101 | case item if item.getClass == manifest.runtimeClass => 102 | Logger.logger.info(s"MutateJsonAuthenticatedActionWithUser called with model: '${item.toString}'") 103 | for { 104 | requestUser <- userFromSecuredRequest(request) 105 | validationResult <- validator.validate(item, requestUser.id) 106 | response: Result <- { 107 | if(validationResult.isValid) { 108 | mutateBlock(request, validationResult, requestUser) handleError() 109 | } else { 110 | Future.successful(BadRequest(validationResult.errorsRestResponse.json)) 111 | } 112 | } 113 | } yield response 114 | }.recoverTotal { 115 | error => 116 | Future.successful(BadRequest(ResponseTools.jsErrorToRestResponse[T](error).json)) 117 | } 118 | } 119 | } 120 | 121 | def MutateJsonAuthenticatedActionWithPayload[T: Format: Manifest](validator: Validator[T])(mutateBlock: (Request[JsValue], ValidationResult[T], TokenPayload) => Future[Result]): Action[JsValue] = { 122 | AuthenticatedActionWithPayload(parse.json) { 123 | (request, tokenPayload) => 124 | Logger.logger.info(s"MutateJsonAuthenticatedActionWithPayload called with body: '${request.body}'") 125 | request.body.validate[T].map { 126 | case item if item.getClass == manifest.runtimeClass => 127 | Logger.logger.info(s"MutateJsonAuthenticatedActionWithPayload called with model: '${item.toString}'") 128 | for { 129 | validationResult <- validator.validate(item, Some(tokenPayload.userId)) 130 | response: Result <- { 131 | if(validationResult.isValid) { 132 | mutateBlock(request, validationResult, tokenPayload) handleError() 133 | } else { 134 | Future.successful(BadRequest(validationResult.errorsRestResponse.json)) 135 | } 136 | } 137 | } yield response 138 | }.recoverTotal { 139 | error => 140 | Future.successful(BadRequest(ResponseTools.jsErrorToRestResponse[T](error).json)) 141 | } 142 | } 143 | } 144 | 145 | implicit def userFromSecuredRequest(implicit request : Request[_]) : Future[User] = { 146 | Asserts.argumentIsNotNull(request) 147 | 148 | val token = request.headers.get(AUTHORIZATION) 149 | .getOrElse(throw new IllegalStateException(AUTH_TOKEN_NOT_FOUND_ERROR)) 150 | 151 | this.authenticationService.getUserFromToken(token) 152 | } 153 | } -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import play.routes.compiler.InjectedRoutesGenerator 2 | import play.sbt.PlayScala 3 | 4 | name := """luxury-akka""" 5 | 6 | version := "1.0-SNAPSHOT" 7 | 8 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 9 | 10 | scalaVersion := "2.11.8" 11 | 12 | libraryDependencies ++= Seq( 13 | jdbc, 14 | cache, 15 | ws, 16 | filters, 17 | "org.postgresql" % "postgresql" % "42.0.0", 18 | "com.nimbusds" % "nimbus-jose-jwt" % "4.34.2", 19 | "com.typesafe.slick" %% "slick" % "3.2.0", 20 | "org.flywaydb" %% "flyway-play" % "3.0.1", 21 | "org.apache.commons" % "commons-email" % "1.4", 22 | "com.github.tminglei" %% "slick-pg" % "0.15.0-RC", 23 | "org.scalaz" %% "scalaz-core" % "7.2.10" 24 | ) 25 | 26 | routesGenerator := InjectedRoutesGenerator 27 | 28 | fork in run := false -------------------------------------------------------------------------------- /conf/api.v1.routes: -------------------------------------------------------------------------------- 1 | 2 | # Users routes 3 | GET /user/:id @controllers.api.v1.UserController.read(id: Long) 4 | POST /user @controllers.api.v1.UserController.create 5 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | app { 2 | version = "0.1.0" , 3 | name = reactive-luxury 4 | } 5 | 6 | db { 7 | default{ 8 | driver = org.postgresql.Driver 9 | url = "jdbc:postgresql://127.0.0.1/luxuryakka" 10 | username ="luxuryakka" 11 | password ="luxuryakka" 12 | migration { 13 | initOnMigrate = true 14 | } 15 | } 16 | } 17 | 18 | play { 19 | modules { 20 | enabled += "org.flywaydb.play.PlayModule" 21 | enabled += "Module" 22 | } 23 | } 24 | 25 | application { 26 | secret: "XK[x5_2s_XOf 2 | 3 | 4 | 5 | %coloredLevel - %logger - %message%n%xException 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Authenticate 6 | POST /api/authenticate @controllers.AuthenticationController.authenticate 7 | POST /api/refresh-token @controllers.AuthenticationController.refreshToken 8 | 9 | 10 | # API, v1 11 | -> /api/v1 api.v1.Routes 12 | -------------------------------------------------------------------------------- /db_init/Inital_db_creation_script.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS "luxuryakka"; 2 | 3 | DROP ROLE IF EXISTS luxuryakka; 4 | 5 | CREATE ROLE luxuryakka LOGIN 6 | ENCRYPTED PASSWORD 'b3a7bff724584262b6340aa97263eda9' 7 | SUPERUSER INHERIT CREATEDB CREATEROLE REPLICATION; 8 | 9 | CREATE DATABASE "luxuryakka" 10 | WITH OWNER = luxuryakka 11 | ENCODING = 'UTF8' 12 | TABLESPACE = pg_default 13 | CONNECTION LIMIT = -1; -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Tue Sep 30 14:21:20 GMT 2014 3 | template.uuid=9d0f021d-ca8f-4a88-992f-f6468442419e 4 | sbt.version=0.13.13 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 2 | 3 | // The Play plugin 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.13") 5 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-josip/reactive-play-scala-akka-slick-guice-domain_validation-seed/253aae3ddb4587084f658767f6161857907244f6/public/images/favicon.png -------------------------------------------------------------------------------- /public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-josip/reactive-play-scala-akka-slick-guice-domain_validation-seed/253aae3ddb4587084f658767f6161857907244f6/public/stylesheets/main.css -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 |

restful scala reactive backend

2 | 3 |

4 | DDD DOMAIN VALIDATION: http://codemozzer.me/domain,validation,action,composable,messages/2015/09/26/domain_validation.html 5 |

6 | 7 |

This is rest application seed using Scala 2.11.8, PlayFramework 2.5.13, Slick 3.2, Postgres 9.4, Akka Actors, FlyWay DB migrations, JWT Token Authentication and Deep Domain validation

8 | 9 |
    10 |
  • Seed is having multiple developed tools for deep domain validation. Deep Domain Validation is representing custom validation of Domain (or any others) objects that have simple or complex validation rules, from simple ones like notNullNorEmpty, lengthIsBiggerOrEqualTo, validEmail to complex ones like unique in DB some other complex dependencies between objects. It is providing simple solution of how to write structured ItemValidators[T] .
  • 11 |
  • Domain Validation is populating Messages which can have INFO, WARNING or ERROR messages, which can later be presented to API user, also populated Messages can be used to decide what to do if i.e. WARNING is present, then we can decide to go in some direction like retry our attempt, or if ERROR is present then we will revert multiple actions.
  • 12 |
  • in application is implementing deep validation where all ERRORS, WARNING and INFO messages are collected and returned in unified response

  • 13 |
  • all rest responses are unified and response has same structure every time so it is easier to handle errors, warning and information messages and also it is easier to handle specific data on pages. 14 | Response is structured to have GLOBAL and LOCAL messages. LOCAL messages are messages that are coupled to some field i.e. = "username is too log. Allowed length is 80 chars". Global messages are messages that are reflecting state of whole data on page, i.e. "User will not be active until is approved". Local and Global messages are having three levels: ERROR, WARNING and INFORMATION. 15 | example response:

  • 16 |
  • GLOBAL messages:

  • 17 |
18 | 19 |
{
20 |     "messages" : {
21 |         "global" : {
22 |             "info": ["User successfully created."],
23 |             "warnings": ["User will not be available for login until is activated"],
24 |             "errors": []
25 |         },
26 |         "local" : []
27 |     },
28 |     "data":{
29 |         "id": 2,
30 |         "firstName": "Mister",
31 |         "lastName": "Sir",
32 |         "username": "mistersir",
33 |         "email": "mistersir@example.com"
34 |     }
35 | }
36 | 37 |
    38 |
  • LOCAL messages:
  • 39 |
40 | 41 |
{
42 |     "messages" : {
43 |         "global" : {
44 |             "info": [],
45 |             "warnings": [],
46 |             "errors": []
47 |         },
48 |         "local" : [
49 |             {
50 |                 "formId" : "username",
51 |                 "errors" : ["User with this username already exists."],
52 |                 "warnings" : [],
53 |                 "info" : []
54 |             }
55 |         ]
56 |     },
57 |     "data":{
58 |         "id": 2,
59 |         "firstName": "Mister",
60 |         "lastName": "Sir",
61 |         "username": "mistersir",
62 |         "email": "mistersir@example.com"
63 |     }
64 | }
65 | 66 |
    67 |
  • JSON Web Tokens (JWT) is used for user identification and authentication

  • 68 |
  • application is divided into modules i.e. user module, user module etc. Each module have dao, domain, validation, service packages.

  • 69 |
  • Database migrations:

    70 | 71 |
      72 |
    • in db_init directory is initial postgres script that will create database luxuryakka with user luxuryakka and password luxuryakka . That can be easily done manually.
    • 73 |
    • when application is started db migrations are available on : http://localhost:9000/@flyway/default where pending migrations can be applied as described here: Play 2.4 FlayWay
    • 74 |
  • 75 |
76 | 77 | --------------------------------------------------------------------------------