├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── build.gradle ├── docker-compose.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── setup ├── db_create.sql ├── db_drop.sql └── docker-compose.local.yml └── src ├── main ├── kotlin │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.kt │ │ ├── api │ │ ├── bookstore │ │ │ └── domain │ │ │ │ └── entities │ │ │ │ └── Book.kt │ │ ├── common │ │ │ ├── Pagination.kt │ │ │ ├── exceptions.kt │ │ │ └── validation │ │ │ │ ├── annotations │ │ │ │ ├── EmailOrNull.kt │ │ │ │ └── NotBlankOrNull.kt │ │ │ │ └── requestValidation.kt │ │ ├── realestate │ │ │ ├── domain │ │ │ │ └── jpa │ │ │ │ │ ├── entities │ │ │ │ │ ├── Broker.kt │ │ │ │ │ ├── Property.kt │ │ │ │ │ ├── PropertyCluster.kt │ │ │ │ │ ├── PropertyLink.kt │ │ │ │ │ └── QueryDslEntity.kt │ │ │ │ │ ├── repositories │ │ │ │ │ ├── BrokerRepository.kt │ │ │ │ │ ├── PropertyClusterRepository.kt │ │ │ │ │ ├── PropertyLinksRepository.kt │ │ │ │ │ └── PropertyRepository.kt │ │ │ │ │ └── services │ │ │ │ │ ├── JpaBrokerService.kt │ │ │ │ │ ├── JpaPropertyClusterService.kt │ │ │ │ │ ├── JpaPropertyLinksService.kt │ │ │ │ │ └── JpaPropertyService.kt │ │ │ ├── handler │ │ │ │ ├── brokers │ │ │ │ │ └── crud │ │ │ │ │ │ ├── create │ │ │ │ │ │ ├── CreateBrokerHandler.kt │ │ │ │ │ │ └── request.kt │ │ │ │ │ │ ├── getbyid │ │ │ │ │ │ └── GetBrokerByIdHandler.kt │ │ │ │ │ │ ├── search │ │ │ │ │ │ ├── SearchBrokersHandler.kt │ │ │ │ │ │ ├── request.kt │ │ │ │ │ │ └── response.kt │ │ │ │ │ │ └── update │ │ │ │ │ │ ├── UpdateBrokerHandler.kt │ │ │ │ │ │ └── request.kt │ │ │ │ ├── common │ │ │ │ │ ├── request │ │ │ │ │ │ └── queryDsl.kt │ │ │ │ │ └── response │ │ │ │ │ │ ├── broker.kt │ │ │ │ │ │ ├── property.kt │ │ │ │ │ │ └── queryDsl.kt │ │ │ │ └── properties │ │ │ │ │ ├── crud │ │ │ │ │ ├── create │ │ │ │ │ │ ├── CreatePropertyHandler.kt │ │ │ │ │ │ └── request.kt │ │ │ │ │ ├── getbyid │ │ │ │ │ │ └── GetPropertyByIdHandler.kt │ │ │ │ │ ├── search │ │ │ │ │ │ ├── SearchPropertiesHandler.kt │ │ │ │ │ │ ├── request.kt │ │ │ │ │ │ └── response.kt │ │ │ │ │ └── update │ │ │ │ │ │ ├── UpdatePropertyHandler.kt │ │ │ │ │ │ └── request.kt │ │ │ │ │ └── links │ │ │ │ │ ├── create_links │ │ │ │ │ ├── LinkPropertiesHandler.kt │ │ │ │ │ ├── request.kt │ │ │ │ │ └── response.kt │ │ │ │ │ ├── duplicates │ │ │ │ │ ├── ListDuplicatePropertiesHandler.kt │ │ │ │ │ └── response.kt │ │ │ │ │ ├── linked_by │ │ │ │ │ ├── PropertyLinkedByHandler.kt │ │ │ │ │ └── response.kt │ │ │ │ │ ├── linked_to │ │ │ │ │ ├── PropertyLinksToHandler.kt │ │ │ │ │ └── response.kt │ │ │ │ │ └── unlink │ │ │ │ │ ├── UnlinkPropertiesHandler.kt │ │ │ │ │ ├── request.kt │ │ │ │ │ └── response.kt │ │ │ └── routing │ │ │ │ ├── brokers │ │ │ │ └── BrokersCrudController.kt │ │ │ │ └── properties │ │ │ │ ├── PropertiesCrudController.kt │ │ │ │ └── PropertiesLinksController.kt │ │ └── tweeter │ │ │ ├── domain │ │ │ ├── auditing │ │ │ │ ├── JpaAuthorListener.kt │ │ │ │ └── JpaTweetListener.kt │ │ │ ├── entities │ │ │ │ └── jpaEntities.kt │ │ │ ├── repositories │ │ │ │ └── jpaRepositories.kt │ │ │ └── services │ │ │ │ ├── esServices.kt │ │ │ │ └── jpaServices.kt │ │ │ └── routing │ │ │ ├── AuthorController.kt │ │ │ └── TweetController.kt │ │ ├── configuration │ │ ├── beanValidation.kt │ │ ├── elasticSearch.kt │ │ ├── jackson.kt │ │ └── queryDsl.kt │ │ ├── es │ │ └── EsClientService.kt │ │ ├── jpa │ │ └── JpaTypes.kt │ │ ├── logging │ │ └── AppLogger.kt │ │ ├── main.kt │ │ ├── querydsl │ │ └── queryDslExtensions.kt │ │ └── util │ │ ├── defer │ │ └── kdefer.kt │ │ ├── fp │ │ └── pipeTo.kt │ │ └── optionals │ │ └── optionals.kt └── resources │ ├── application.yml │ └── db │ └── migration │ ├── V1__Initialize_Tables.sql │ ├── V2__Tables_RealEstate.sql │ ├── V3__Table_RealEstate_PropertyLinks.sql │ ├── V4__Table_RealEstate_PropertyCluster.sql │ └── V5__Table_RealEstate_Broker.sql └── test └── resources └── testdata └── cleanup.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # os 2 | .DS_Store 3 | 4 | # gradle 5 | .gradle 6 | !gradle/wrapper/gradle-wrapper.jar 7 | 8 | # idea 9 | *.iml 10 | .idea 11 | out/ 12 | 13 | # project 14 | build 15 | 16 | # elastic-search (embedded) 17 | data/ 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Seb Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #db.local.up: 2 | # docker-compose -f setup/docker-compose.local.yml up -d 3 | #db.local.down: 4 | # docker-compose -f setup/docker-compose.local.yml down 5 | 6 | db.local.create: 7 | psql -p 5432 -h 127.0.0.1 -U postgres -f setup/db_create.sql 8 | db.local.drop: 9 | psql -p 5432 -h 127.0.0.1 -U postgres -f setup/db_drop.sql 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-spring-jpa-examples 2 | - playground for kotlin + spring-boot + bean-validation + jpa + hibernate + querydsl 3 | - realworld examples (just a little simplified): check api.realestate 4 | 5 | # status: 6 | - its an active playground ,therefore in progress ... 7 | 8 | # requirements 9 | 10 | - postgres 11 | 12 | ## create an example db 13 | $ make db.local.create 14 | 15 | 16 | ## additional resources 17 | 18 | ### bean validation 19 | 20 | - https://stackoverflow.com/questions/44320678/jsr-349-bean-validation-for-spring-restcontroller-with-spring-boot 21 | - http://apprize.info/javascript/wrox/16.html 22 | - https://github.com/gothinkster/kotlin-spring-realworld-example-app/blob/master/src/main/kotlin/io/realworld/ApiApplication.kt 23 | - https://github.com/arawn/kotlin-spring-example/blob/master/src/main/kotlin/org/ksug/forum/web/ForumRestController.kt 24 | 25 | 26 | ## findings 27 | 28 | ### hibernate: 29 | - if you have to use if for whatever reason, check the logs ;) ... 30 | ### hibernate: immutable data class as @Entity 31 | - in jpa/hibernate context it gets proxied by cglib & bean validation works 32 | - entity.copy() -> triggers init() method, if there is one 33 | ### hibernate: auditing 34 | - Auditing classes (Listeners) are no spring beans 35 | - @inject will not work out-of-the-box 36 | - thefore its pretty useless 37 | ### jetbrains gradle plugin: 38 | 39 | - https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/Module.md 40 | 41 | ### querydsl: is awesome :) 42 | 43 | - https://github.com/JetBrains/kotlin-examples/blob/master/gradle/kotlin-querydsl/build.gradle 44 | - https://github.com/eugenp/tutorials/tree/master/querydsl 45 | - example implementation: 46 | 47 | com.example.demo.api.realestate.PropertiesCrudController.search() 48 | 49 | ### elasticsearch 50 | - https://www.mkyong.com/spring-boot/spring-boot-spring-data-elasticsearch-example/ 51 | - spring-data-elasticsearch: dependency hell: 52 | - boot 1.5.* with spring-data-elasticsearch does not work with es 5.5 53 | - es 5.5 requires spring-data-elasticsearch 2.* (boot 2.*) 54 | - using es transportclient (native protocol) is not recommended by es 55 | - es recommends using rest-api - which is aware of future versions of es 56 | - es is currently working on a high level restapi client that should make things easy 57 | - most cloud providers (hosted-es-as-a-service) just allow using rest api 58 | - several issues related to jackson-json serialization (e.g.: supports JodaTime but not Java 8 Time Api out-of-the-box) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | 3 | buildscript { 4 | ext { 5 | kotlin_version = '1.1.4-2' 6 | springBootVersion = '1.5.6.RELEASE' 7 | jacksonVersion = '2.9.0'//'2.8.9' 8 | mockitoVersion = '2.8.47' 9 | queryDslVersion = '4.1.4' 10 | swaggerVersion = '2.7.0' // '2.6.1' 11 | esVersion = '6.0.0-beta1' 12 | } 13 | repositories { 14 | mavenCentral() 15 | jcenter() 16 | maven { url 'https://repo.spring.io/libs-snapshot' } 17 | maven { url 'http://repo.spring.io/milestone/' } 18 | } 19 | dependencies { 20 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 21 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 22 | } 23 | } 24 | 25 | plugins { 26 | id "org.jetbrains.kotlin.plugin.spring" version '1.1.4-2' 27 | id "org.jetbrains.kotlin.plugin.noarg" version '1.1.4-2' 28 | id "org.jetbrains.kotlin.plugin.allopen" version '1.1.4-2' 29 | } 30 | 31 | 32 | group 'com.example' 33 | version '1.0-SNAPSHOT' 34 | 35 | apply plugin: 'java' 36 | apply plugin: 'kotlin' 37 | apply plugin: 'org.springframework.boot' 38 | apply plugin: 'org.jetbrains.kotlin.plugin.spring' 39 | apply plugin: 'kotlin-jpa' 40 | 41 | apply plugin: 'kotlin-kapt' 42 | apply plugin: 'idea' 43 | 44 | repositories { 45 | mavenCentral() 46 | jcenter() 47 | maven { url 'https://repo.spring.io/libs-snapshot' } 48 | maven { url 'http://repo.spring.io/milestone/' } 49 | } 50 | 51 | 52 | compileKotlin { 53 | kotlinOptions.jvmTarget = "1.8" 54 | } 55 | compileTestKotlin { 56 | kotlinOptions.jvmTarget = "1.8" 57 | } 58 | 59 | idea { 60 | module { 61 | def kaptMain = file('build/generated/source/kapt/main') 62 | sourceDirs += kaptMain 63 | generatedSourceDirs += kaptMain 64 | } 65 | } 66 | 67 | dependencies { 68 | // kotlin 69 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 70 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 71 | 72 | // spring boot 73 | compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" 74 | compile "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}" 75 | compile "org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}" 76 | 77 | //compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" 78 | //compile "org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}" 79 | 80 | // jackson 81 | compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion" 82 | compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" 83 | compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" 84 | 85 | // swagger 86 | compile "io.springfox:springfox-swagger2:$swaggerVersion" 87 | compile "io.springfox:springfox-swagger-ui:$swaggerVersion" 88 | 89 | // db: hibernate, flyway, postgres 90 | compile "org.hibernate:hibernate-core:5.2.7.Final" 91 | compile "org.hibernate:hibernate-entitymanager:5.2.7.Final" 92 | compile "org.hibernate:hibernate-java8:5.2.7.Final" 93 | compile "org.flywaydb:flyway-core:4.1.1" 94 | compile "org.postgresql:postgresql:42.1.3" 95 | 96 | //querydsl 97 | compile "com.querydsl:querydsl-jpa:${queryDslVersion}" 98 | kapt "com.querydsl:querydsl-apt:${queryDslVersion}:jpa" 99 | 100 | 101 | // JSR-330 javax.inject annotations - required by querydsl 102 | compile group: 'javax.inject', name: 'javax.inject', version: '1' 103 | 104 | // spring-elastic-search 105 | //compile group: 'org.springframework.data', name: 'spring-data-elasticsearch', version: '2.1.6.RELEASE' 106 | 107 | //compile 'org.elasticsearch:elasticsearch:5.4.1' 108 | //compile 'org.elasticsearch.client:transport:5.4.1' 109 | //compile group: 'org.elasticsearch.client', name: 'transport', version: '5.5.1' 110 | //compile group: 'org.springframework.data', name: 'spring-data-elasticsearch', version: '3.0.0.RC2' 111 | 112 | // https://mvnrepository.com/artifact/com.github.vanroy/spring-data-jest 113 | //compile group: 'com.github.vanroy', name: 'spring-data-jest', version: '2.3.1.RELEASE' 114 | 115 | 116 | // embedded elastic-search 117 | // https://mvnrepository.com/artifact/net.java.dev.jna/jna 118 | //runtime group: 'net.java.dev.jna', name: 'jna', version: '4.4.0' 119 | 120 | // ES REST 121 | // https://mvnrepository.com/artifact/org.elasticsearch.client/sniffer 122 | // compile group: 'org.elasticsearch.client', name: 'sniffer', version: '5.5.1' 123 | // https://mvnrepository.com/artifact/org.elasticsearch.client/elasticsearch-rest-high-level-client 124 | 125 | compile group: 'org.elasticsearch.client', name: 'elasticsearch-rest-high-level-client', version: esVersion 126 | compile group: 'org.elasticsearch.client', name: 'elasticsearch-rest-client-sniffer', version: esVersion 127 | //compile group: 'org.elasticsearch', name: 'elasticsearch', version: esVersion 128 | 129 | // https://mvnrepository.com/artifact/org.elasticsearch/elasticsearch 130 | compile group: 'org.elasticsearch', name: 'elasticsearch', version: '5.5.1' 131 | 132 | 133 | 134 | // tests 135 | 136 | // spring (without outdated mockito) 137 | testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}", { 138 | exclude group: "org.mockito", module: "mockito-core" 139 | } 140 | // mockito 2.8.*, 141 | testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion 142 | testCompile group: 'org.mockito', name: 'mockito-inline', version: mockitoVersion 143 | // kluent assertions & mockito-kotlin 144 | testCompile "org.amshove.kluent:kluent:1.23", { 145 | exclude group: "com.nhaarman", module: "mockito-kotlin" 146 | } 147 | testCompile group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.5.0' 148 | 149 | 150 | } 151 | 152 | 153 | task wrapper(type: Wrapper) { 154 | gradleVersion = '4.1' 155 | } 156 | 157 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | elasticsearch1: 4 | image: docker.elastic.co/elasticsearch/elasticsearch:5.5.2 5 | container_name: elasticsearch1 6 | environment: 7 | - cluster.name=docker-cluster 8 | - bootstrap.memory_lock=true 9 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 10 | - xpack.security.enabled=false 11 | ulimits: 12 | memlock: 13 | soft: -1 14 | hard: -1 15 | mem_limit: 1g 16 | volumes: 17 | - esdata1:/usr/share/elasticsearch/data 18 | ports: 19 | - 9200:9200 20 | - 9300:9300 21 | networks: 22 | - esnet 23 | elasticsearch2: 24 | image: docker.elastic.co/elasticsearch/elasticsearch:5.5.2 25 | environment: 26 | - cluster.name=docker-cluster 27 | - bootstrap.memory_lock=true 28 | - xpack.security.enabled=false 29 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 30 | - "discovery.zen.ping.unicast.hosts=elasticsearch1" 31 | ulimits: 32 | memlock: 33 | soft: -1 34 | hard: -1 35 | mem_limit: 1g 36 | volumes: 37 | - esdata2:/usr/share/elasticsearch/data 38 | networks: 39 | - esnet 40 | 41 | volumes: 42 | esdata1: 43 | driver: local 44 | esdata2: 45 | driver: local 46 | 47 | networks: 48 | esnet: -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastman/kotlin-spring-jpa-examples/cde355a9ebb110fa76abb5183b935d6b76c99348/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 21 06:51:59 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | 3 | -------------------------------------------------------------------------------- /setup/db_create.sql: -------------------------------------------------------------------------------- 1 | -- create example db. 2 | CREATE ROLE example WITH LOGIN PASSWORD 'example'; 3 | 4 | CREATE DATABASE example OWNER example; 5 | CREATE DATABASE example_test OWNER example; -------------------------------------------------------------------------------- /setup/db_drop.sql: -------------------------------------------------------------------------------- 1 | -- drop example db. 2 | 3 | DROP DATABASE example; 4 | DROP DATABASE example_test; 5 | DROP ROLE example; -------------------------------------------------------------------------------- /setup/docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres:latest 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: postgres 8 | ports: 9 | #- "5433:5433" 10 | - "5432:5432" 11 | 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/DemoApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import org.springframework.boot.CommandLineRunner 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories 7 | import org.springframework.transaction.annotation.EnableTransactionManagement 8 | import springfox.documentation.swagger2.annotations.EnableSwagger2 9 | 10 | 11 | @SpringBootApplication 12 | @EnableSwagger2 13 | 14 | @EnableTransactionManagement 15 | @EnableJpaRepositories 16 | @EnableJpaAuditing 17 | class DemoApplication( 18 | 19 | ) : CommandLineRunner { 20 | 21 | override fun run(vararg args: String?) { 22 | /* 23 | val r=restClient.performRequest("GET", "/", 24 | Collections.singletonMap("pretty", "true")) 25 | */ 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/bookstore/domain/entities/Book.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.bookstore.domain.entities 2 | 3 | import org.springframework.data.annotation.Id 4 | import java.util.* 5 | 6 | 7 | data class Book( 8 | @Id 9 | var id: String? = null, 10 | 11 | // https://stackoverflow.com/questions/32042430/elasticsearch-spring-data-date-format-always-is-long 12 | // http://www.baeldung.com/jackson-serialize-dates 13 | 14 | //@field: JsonFormat(shape = JsonFormat.Shape.STRING) 15 | //@field: Temporal(TemporalType.TIMESTAMP) 16 | //@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ") 17 | 18 | //@Field(type = FieldType.Date, format = DateFormat.date_optional_time) 19 | //@JsonProperty(value = "@timestamp") 20 | //@Field(type = FieldType.Date, index = FieldIndex.not_analyzed, store = true, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'hh:mm:ss.SSS'Z'") 21 | 22 | //@JsonSerialize(using = InstantSerializer::class) 23 | //@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ") 24 | //@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ") 25 | //@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ") 26 | //@Field(type = FieldType.Date, index = FieldIndex.not_analyzed, store = true, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'hh:mm:ss.SSS'Z'") 27 | 28 | //var modifiedAt:Instant?=null, 29 | 30 | 31 | //@Field(type = FieldType.Date) 32 | var modifiedAt: Date? = null, 33 | 34 | var title: String? = null, 35 | 36 | var author: String? = null, 37 | 38 | var releaseDate: String? = null 39 | ) { 40 | 41 | 42 | //constructor() {} 43 | 44 | 45 | /* 46 | override fun toString(): String { 47 | return "Book{" + 48 | "id='" + id + '\'' + 49 | ", title='" + title + '\'' + 50 | ", author='" + author + '\'' + 51 | ", releaseDate='" + releaseDate + '\'' + 52 | '}' 53 | } 54 | */ 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/common/Pagination.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.common 2 | 3 | import org.springframework.data.domain.Page 4 | 5 | data class Pagination( 6 | val page: Int, 7 | val pageSize: Int, 8 | val totalPages: Int, 9 | val totalItems: Long 10 | ) { 11 | companion object { 12 | fun ofPageResult(pageResult: Page<*>): Pagination { 13 | return Pagination( 14 | pageSize = pageResult.size, 15 | totalPages = pageResult.totalPages, 16 | page = pageResult.number, 17 | totalItems = pageResult.totalElements 18 | ) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/common/exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.common 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.ResponseStatus 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | class EntityNotFoundException(message: String) : RuntimeException(message) 8 | 9 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 10 | class BadRequestException(message: String) : RuntimeException(message) 11 | 12 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) 13 | class EntityAlreadyExistException(message: String) : RuntimeException(message) 14 | 15 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) 16 | class DomainException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/common/validation/annotations/EmailOrNull.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.common.validation.annotations 2 | 3 | import org.hibernate.validator.constraints.CompositionType.OR 4 | import org.hibernate.validator.constraints.ConstraintComposition 5 | import javax.validation.Constraint 6 | import javax.validation.Payload 7 | import javax.validation.constraints.Null 8 | import kotlin.reflect.KClass 9 | import org.hibernate.validator.constraints.Email as HibernateEmail 10 | 11 | @ConstraintComposition(OR) 12 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | @Constraint(validatedBy = arrayOf()) 15 | @MustBeDocumented 16 | 17 | @Null 18 | @EmailComposition 19 | annotation class EmailOrNull( 20 | // see: https://github.com/lukaszguz/optional-field-validation/blob/master/src/main/java/pl/guz/domain/validation/OptionalSpecialField.java 21 | val message: String = "{}", 22 | val groups: Array> = arrayOf(), 23 | val payload: Array> = arrayOf() 24 | ) 25 | 26 | @ConstraintComposition 27 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS) 28 | @Retention(AnnotationRetention.RUNTIME) 29 | @Constraint(validatedBy = arrayOf()) 30 | @MustBeDocumented 31 | @HibernateEmail 32 | annotation class EmailComposition( 33 | // see: // see: https://github.com/lukaszguz/optional-field-validation/blob/master/src/main/java/pl/guz/domain/validation/OptionalSpecialField.java 34 | 35 | val message: String = "{}", 36 | val groups: Array> = arrayOf(), 37 | val payload: Array> = arrayOf() 38 | ) 39 | 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/common/validation/annotations/NotBlankOrNull.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.common.validation.annotations 2 | 3 | import org.hibernate.validator.constraints.CompositionType.OR 4 | import org.hibernate.validator.constraints.ConstraintComposition 5 | import javax.validation.Constraint 6 | import javax.validation.Payload 7 | import javax.validation.constraints.Null 8 | import kotlin.reflect.KClass 9 | import org.hibernate.validator.constraints.NotBlank as HibernateNotBlank 10 | 11 | @ConstraintComposition(OR) 12 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | @Constraint(validatedBy = arrayOf()) 15 | @MustBeDocumented 16 | 17 | @Null 18 | @NotBlankComposition 19 | annotation class NotBlankOrNull( 20 | // see: https://github.com/lukaszguz/optional-field-validation/blob/master/src/main/java/pl/guz/domain/validation/OptionalSpecialField.java 21 | val message: String = "{}", 22 | val groups: Array> = arrayOf(), 23 | val payload: Array> = arrayOf() 24 | ) 25 | 26 | @ConstraintComposition 27 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS) 28 | @Retention(AnnotationRetention.RUNTIME) 29 | @Constraint(validatedBy = arrayOf()) 30 | @MustBeDocumented 31 | @HibernateNotBlank 32 | annotation class NotBlankComposition( 33 | // see: // see: https://github.com/lukaszguz/optional-field-validation/blob/master/src/main/java/pl/guz/domain/validation/OptionalSpecialField.java 34 | 35 | val message: String = "{}", 36 | val groups: Array> = arrayOf(), 37 | val payload: Array> = arrayOf() 38 | ) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/common/validation/requestValidation.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.common.validation 2 | 3 | import com.example.demo.api.common.BadRequestException 4 | import org.springframework.validation.DataBinder 5 | import org.springframework.validation.Validator 6 | 7 | fun notNull(value: T?, field: String): T { 8 | return value ?: throw BadRequestException("field=$field must be not null") 9 | } 10 | 11 | fun notBlank(value: String, field: String): String { 12 | return if (value.isBlank()) { 13 | throw BadRequestException("$field must not be blank!") 14 | } else value 15 | } 16 | 17 | fun Validator.validateRequest(value: T, beanName: String?): T { 18 | return validateBean(validator = this, value = value, beanName = beanName) 19 | } 20 | 21 | fun validateBean(validator: Validator, value: T, beanName: String?): T { 22 | val binder = if (beanName == null) { 23 | DataBinder(value) 24 | } else { 25 | DataBinder(value, beanName) 26 | } 27 | binder.validator = validator 28 | binder.validate() 29 | val result = binder.bindingResult 30 | if (!result.hasErrors()) { 31 | 32 | return value 33 | } 34 | 35 | val errors = result.fieldErrors.map { 36 | mapOf( 37 | "field" to "${it.objectName}.${it.field}", 38 | "message" to it.defaultMessage 39 | ) 40 | } 41 | 42 | val classname: String = value::class.qualifiedName ?: value::class.toString() 43 | 44 | val msg = "Failed to validate bean=$classname objectName: ${result.objectName} nestedPath: ${result.nestedPath} allErrors:${errors.joinToString("\n")}" 45 | 46 | throw BadRequestException(msg) 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/entities/Broker.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.entities 2 | 3 | import com.example.demo.jpa.JpaTypes 4 | import com.example.demo.logging.AppLogger 5 | import org.hibernate.annotations.Type 6 | import org.hibernate.validator.constraints.Email 7 | import org.hibernate.validator.constraints.NotBlank 8 | import org.springframework.validation.annotation.Validated 9 | import java.time.Instant 10 | import java.util.* 11 | import javax.persistence.* 12 | import javax.validation.constraints.NotNull 13 | 14 | @Entity 15 | data class Broker( 16 | @Id @Type(type = JpaTypes.UUID) 17 | @Column(name = "id", nullable = false) 18 | @get: [NotNull] 19 | val id: UUID, 20 | 21 | @Version 22 | @Column(name = "version", nullable = false) 23 | @get: [NotNull] 24 | val version: Int = -1, 25 | 26 | @Column(name = "created_at", nullable = false) 27 | private var created: Instant, 28 | @Column(name = "modified_at", nullable = false) 29 | private var modified: Instant, 30 | 31 | @Column(name = "company_name", nullable = false) 32 | @get:[NotNull NotBlank] 33 | val companyName: String, 34 | 35 | @Column(name = "first_name", nullable = false) 36 | @get:[NotNull NotBlank] 37 | val firstName: String, 38 | 39 | @Column(name = "last_name", nullable = false) 40 | @get:[NotNull NotBlank] 41 | val lastName: String, 42 | 43 | @Column(name = "email", nullable = false) 44 | @get:[NotNull Email] 45 | val email: String, 46 | 47 | @Column(name = "phone_number", nullable = false) 48 | @get:[NotNull] // can be blank 49 | val phoneNumber: String, 50 | 51 | @Column(name = "comment", nullable = false) 52 | @get:[NotNull] // can be blank 53 | val comment: String, 54 | 55 | @Embedded 56 | @get:[NotNull] 57 | val address: BrokerAddress 58 | ) { 59 | fun getCreatedAt(): Instant = created 60 | fun getModifiedAt(): Instant = modified 61 | 62 | @PreUpdate @Validated 63 | private fun beforeUpdate() { 64 | this.modified = Instant.now() 65 | LOG.info("beforeUpdate $this") 66 | } 67 | 68 | @PrePersist @Validated 69 | private fun beforeInsert() { 70 | created = Instant.now() 71 | modified = Instant.now() 72 | LOG.info("beforeInsert $this") 73 | } 74 | 75 | companion object { 76 | private val LOG = AppLogger(this::class) 77 | } 78 | } 79 | 80 | @Embeddable 81 | data class BrokerAddress( 82 | @Column(name = "address_country", nullable = false) 83 | @get:[NotNull] 84 | val country: String, 85 | 86 | @Column(name = "address_state", nullable = false) 87 | @get:[NotNull] 88 | val state: String, 89 | 90 | @Column(name = "address_city", nullable = false) 91 | @get:[NotNull] 92 | val city: String, 93 | 94 | @Column(name = "address_street", nullable = false) 95 | @get:[NotNull] 96 | val street: String, 97 | 98 | @Column(name = "address_number", nullable = false) 99 | @get:[NotNull] 100 | val number: String 101 | ) { 102 | companion object { 103 | val EMPTY = BrokerAddress(country = "", state = "", city = "", street = "", number = "") 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/entities/Property.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.entities 2 | 3 | import com.example.demo.jpa.JpaTypes 4 | import com.example.demo.logging.AppLogger 5 | import org.hibernate.annotations.Type 6 | import org.hibernate.validator.constraints.NotBlank 7 | import org.springframework.validation.annotation.Validated 8 | import java.time.Instant 9 | import java.util.* 10 | import javax.persistence.* 11 | import javax.validation.Valid 12 | import javax.validation.constraints.NotNull 13 | 14 | @Entity 15 | data class Property( 16 | @Id @Type(type = JpaTypes.UUID) 17 | @Column(name = "id", nullable = false) 18 | @get: [NotNull] 19 | val id: UUID, 20 | 21 | @Version 22 | @Column(name = "version", nullable = false) 23 | @get: [NotNull] 24 | val version: Int = -1, 25 | 26 | @Column(name = "created_at", nullable = false) 27 | private var created: Instant, 28 | @Column(name = "modified_at", nullable = false) 29 | private var modified: Instant, 30 | 31 | @Column(name = "type", nullable = false) 32 | @Enumerated(EnumType.STRING) 33 | @get:[NotNull] 34 | val type: PropertyType, 35 | 36 | @get:[NotNull] 37 | val name: String, // can be blank 38 | 39 | @Embedded 40 | @get:[NotNull Valid] 41 | val address: PropertyAddress, 42 | 43 | @Column(name = "fk_property_cluster_id", nullable = true) 44 | @Type(type = JpaTypes.UUID) 45 | val clusterId: UUID? 46 | ) { 47 | fun getCreatedAt(): Instant = created 48 | fun getModifiedAt(): Instant = modified 49 | 50 | @PreUpdate @Validated 51 | private fun beforeUpdate() { 52 | this.modified = Instant.now() 53 | LOG.info("beforeUpdate $this") 54 | } 55 | 56 | @PrePersist @Validated 57 | private fun beforeInsert() { 58 | created = Instant.now() 59 | modified = Instant.now() 60 | LOG.info("beforeInsert $this") 61 | } 62 | 63 | companion object { 64 | private val LOG = AppLogger(this::class) 65 | } 66 | } 67 | 68 | enum class PropertyType { 69 | APARTMENT, 70 | HOUSE 71 | } 72 | 73 | @Embeddable 74 | data class PropertyAddress( 75 | 76 | @Column(name = "address_country", nullable = false) 77 | @get:[NotNull NotBlank] 78 | val country: String, 79 | 80 | @Column(name = "address_city", nullable = false) 81 | @get:[NotNull NotBlank] 82 | val city: String, 83 | 84 | @Column(name = "address_zip", nullable = false) 85 | @get:[NotNull] // can be blank 86 | val zip: String, 87 | 88 | @Column(name = "address_street", nullable = false) 89 | @get:[NotNull] // can be blank 90 | val street: String, 91 | 92 | @Column(name = "address_number", nullable = false) 93 | @get:[NotNull] // can be blank 94 | val number: String, 95 | 96 | @Column(name = "address_district", nullable = false) 97 | @get:[NotNull] // can be blank 98 | val district: String, 99 | 100 | @Column(name = "address_neighborhood", nullable = false) 101 | @get:[NotNull] // can be blank 102 | val neighborhood: String 103 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/entities/PropertyCluster.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.entities 2 | 3 | import com.example.demo.jpa.JpaTypes 4 | import com.example.demo.logging.AppLogger 5 | import org.hibernate.annotations.Type 6 | import org.springframework.validation.annotation.Validated 7 | import java.time.Instant 8 | import java.util.* 9 | import javax.persistence.* 10 | import javax.validation.constraints.NotNull 11 | 12 | @Entity 13 | @Table(name = "propertycluster") 14 | data class PropertyCluster( 15 | @Id @Type(type = JpaTypes.UUID) 16 | @Column(name = "id", nullable = false) 17 | @get: [NotNull] 18 | val id: UUID, 19 | 20 | @Version 21 | @Column(name = "version", nullable = false) 22 | @get: [NotNull] 23 | val version: Int = -1, 24 | 25 | @Column(name = "created_at", nullable = false) 26 | private var created: Instant, 27 | @Column(name = "modified_at", nullable = false) 28 | private var modified: Instant 29 | ) { 30 | fun getCreatedAt(): Instant = created 31 | fun getModifiedAt(): Instant = modified 32 | 33 | @PreUpdate @Validated 34 | private fun beforeUpdate() { 35 | this.modified = Instant.now() 36 | LOG.info("beforeUpdate $this") 37 | } 38 | 39 | @PrePersist @Validated 40 | private fun beforeInsert() { 41 | created = Instant.now() 42 | modified = Instant.now() 43 | LOG.info("beforeInsert $this") 44 | } 45 | 46 | companion object { 47 | private val LOG = AppLogger(this::class) 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/entities/PropertyLink.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.entities 2 | 3 | import com.example.demo.api.common.DomainException 4 | import com.example.demo.jpa.JpaTypes 5 | import com.example.demo.logging.AppLogger 6 | import org.hibernate.annotations.Type 7 | import org.springframework.validation.annotation.Validated 8 | import java.time.Instant 9 | import java.util.* 10 | import javax.persistence.* 11 | import javax.validation.constraints.NotNull 12 | 13 | @Entity 14 | @Table( 15 | name = "propertylinks", 16 | uniqueConstraints = arrayOf( 17 | UniqueConstraint( 18 | columnNames = arrayOf("fk_from", "fk_to") 19 | ) 20 | ) 21 | 22 | ) 23 | data class PropertyLink( 24 | @Id @Type(type = JpaTypes.UUID) 25 | @Column(name = "id", nullable = false) 26 | @get: [NotNull] 27 | val id: UUID, 28 | 29 | @Version 30 | @Column(name = "version", nullable = false) 31 | @get: [NotNull] 32 | val version: Int = -1, 33 | 34 | @Column(name = "created_at", nullable = false) 35 | private var created: Instant, 36 | @Column(name = "modified_at", nullable = false) 37 | private var modified: Instant, 38 | 39 | @Column(name = "fk_from", nullable = false) 40 | @get:[NotNull] 41 | val fromPropertyId: UUID, 42 | 43 | @Column(name = "fk_to", nullable = false) 44 | @get:[NotNull] 45 | val toPropertyId: UUID 46 | ) { 47 | fun getCreatedAt(): Instant = created 48 | fun getModifiedAt(): Instant = modified 49 | 50 | @PreUpdate @Validated 51 | private fun beforeUpdate() { 52 | validateBeforeSave() 53 | this.modified = Instant.now() 54 | LOG.info("beforeUpdate $this") 55 | } 56 | 57 | @PrePersist @Validated 58 | private fun beforeInsert() { 59 | validateBeforeSave() 60 | created = Instant.now() 61 | modified = Instant.now() 62 | LOG.info("beforeInsert $this") 63 | } 64 | 65 | private fun validateBeforeSave() { 66 | if (fromPropertyId == toPropertyId) { 67 | throw DomainException( 68 | "It makes no sense to link a property to itself!" + 69 | "PropertyLink.fromId must not equal PropertyLink.toId !" 70 | ) 71 | } 72 | } 73 | 74 | companion object { 75 | private val LOG = AppLogger(this::class) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/entities/QueryDslEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.entities 2 | 3 | object QueryDslEntity { 4 | val qProperty = QProperty.property 5 | val qPropertyLink = QPropertyLink.propertyLink 6 | val qBroker = QBroker.broker 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/repositories/BrokerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.repositories 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.stereotype.Repository 6 | import java.util.* 7 | 8 | @Repository 9 | interface BrokerRepository : JpaRepository { 10 | fun getById(id: UUID): Optional 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/repositories/PropertyClusterRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.repositories 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyCluster 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.stereotype.Repository 6 | import java.util.* 7 | 8 | @Repository 9 | interface PropertyClusterRepository : JpaRepository { 10 | fun getById(id: UUID): Optional 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/repositories/PropertyLinksRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.repositories 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyLink 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.stereotype.Repository 6 | import java.util.* 7 | 8 | @Repository 9 | interface PropertyLinksRepository : JpaRepository { 10 | fun getById(id: UUID): Optional 11 | fun getByFromPropertyIdAndToPropertyId(fromPropertyId: UUID, toPropertyId: UUID): Optional 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/repositories/PropertyRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.repositories 2 | 3 | 4 | import com.example.demo.api.realestate.domain.jpa.entities.Property 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.stereotype.Repository 7 | import java.util.* 8 | 9 | @Repository 10 | interface PropertyRepository : JpaRepository { 11 | fun getById(id: UUID): Optional 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/services/JpaBrokerService.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.services 2 | 3 | import com.example.demo.api.common.EntityAlreadyExistException 4 | import com.example.demo.api.common.EntityNotFoundException 5 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 6 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qBroker 7 | import com.example.demo.api.realestate.domain.jpa.repositories.BrokerRepository 8 | import com.example.demo.util.fp.pipe 9 | import com.example.demo.util.optionals.toNullable 10 | import com.querydsl.jpa.impl.JPAQuery 11 | import org.springframework.stereotype.Component 12 | import java.util.* 13 | import javax.persistence.EntityManager 14 | import javax.validation.Valid 15 | 16 | @Component 17 | class JpaBrokerService( 18 | private val brokerRepository: BrokerRepository, 19 | private val entityManager: EntityManager 20 | ) { 21 | fun exists(brokerId: UUID): Boolean = brokerRepository.exists(brokerId) 22 | 23 | fun findById(brokerId: UUID): Broker? = 24 | brokerRepository 25 | .getById(brokerId) 26 | .toNullable() 27 | 28 | fun getById(brokerId: UUID): Broker = 29 | findById(brokerId) ?: throw EntityNotFoundException( 30 | "ENTITY NOT FOUND! query: Broker.id=$brokerId" 31 | ) 32 | 33 | fun requireExists(brokerId: UUID): UUID = 34 | if (exists(brokerId)) { 35 | brokerId 36 | } else throw EntityNotFoundException( 37 | "ENTITY NOT FOUND! query: Broker.id=$brokerId" 38 | ) 39 | 40 | fun requireDoesNotExist(brokerId: UUID): UUID = 41 | if (!exists(brokerId)) { 42 | brokerId 43 | } else throw EntityAlreadyExistException( 44 | "ENTITY ALREADY EXIST! query: Broker.id=$brokerId" 45 | ) 46 | 47 | fun insert(@Valid broker: Broker): Broker = 48 | requireDoesNotExist(broker.id) pipe { brokerRepository.save(broker) } 49 | 50 | fun update(@Valid broker: Broker): Broker = 51 | requireExists(broker.id) pipe { brokerRepository.save(broker) } 52 | 53 | fun findByIdList(brokerIdList: List): List { 54 | val query = JPAQuery(entityManager) 55 | val resultSet = query.from(qBroker) 56 | .where( 57 | qBroker.id.`in`(brokerIdList) 58 | ) 59 | .fetchResults() 60 | 61 | return resultSet.results 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/services/JpaPropertyClusterService.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.services 2 | 3 | import com.example.demo.api.common.EntityAlreadyExistException 4 | import com.example.demo.api.common.EntityNotFoundException 5 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyCluster 6 | import com.example.demo.api.realestate.domain.jpa.repositories.PropertyClusterRepository 7 | import com.example.demo.util.optionals.toNullable 8 | import org.springframework.stereotype.Component 9 | import java.util.* 10 | import javax.validation.Valid 11 | 12 | @Component 13 | class JpaPropertyClusterService( 14 | private val propertyClusterRepository: PropertyClusterRepository 15 | ) { 16 | fun exists(clusterId: UUID): Boolean = propertyClusterRepository.exists(clusterId) 17 | 18 | fun findById(clusterId: UUID): PropertyCluster? = 19 | propertyClusterRepository 20 | .getById(clusterId) 21 | .toNullable() 22 | 23 | fun getById(clusterId: UUID): PropertyCluster = 24 | findById(clusterId) ?: throw EntityNotFoundException( 25 | "ENTITY NOT FOUND! query: PropertyCluster.id=$clusterId" 26 | ) 27 | 28 | fun requireExists(clusterId: UUID): UUID = 29 | if (exists(clusterId)) { 30 | clusterId 31 | } else throw EntityNotFoundException( 32 | "ENTITY NOT FOUND! query: PropertyCluster.id=$clusterId" 33 | ) 34 | 35 | fun requireDoesNotExist(clusterId: UUID): UUID = 36 | if (!exists(clusterId)) { 37 | clusterId 38 | } else throw EntityAlreadyExistException( 39 | "ENTITY ALREADY EXIST! query: PropertyCluster.id=$clusterId" 40 | ) 41 | 42 | fun insert(@Valid propertyCluster: PropertyCluster): PropertyCluster { 43 | requireDoesNotExist(propertyCluster.id) 44 | return propertyClusterRepository.save(propertyCluster) 45 | } 46 | 47 | fun update(@Valid propertyCluster: PropertyCluster): PropertyCluster { 48 | requireExists(propertyCluster.id) 49 | return propertyClusterRepository.save(propertyCluster) 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/services/JpaPropertyLinksService.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.services 2 | 3 | import com.example.demo.api.common.EntityAlreadyExistException 4 | import com.example.demo.api.common.EntityNotFoundException 5 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyLink 6 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qPropertyLink 7 | import com.example.demo.api.realestate.domain.jpa.repositories.PropertyLinksRepository 8 | import com.example.demo.querydsl.and 9 | import com.example.demo.util.optionals.toNullable 10 | import com.querydsl.core.QueryResults 11 | import com.querydsl.jpa.impl.JPAQueryFactory 12 | import org.springframework.stereotype.Component 13 | import java.util.* 14 | import javax.validation.Valid 15 | 16 | 17 | @Component 18 | class JpaPropertyLinksService( 19 | private val propertyLinksRepository: PropertyLinksRepository, 20 | private val queryFactory: JPAQueryFactory 21 | ) { 22 | 23 | fun exists(propertyLinkId: UUID): Boolean = propertyLinksRepository.exists(propertyLinkId) 24 | 25 | fun findById(linkId: UUID): PropertyLink? = 26 | propertyLinksRepository 27 | .getById(linkId) 28 | .toNullable() 29 | 30 | fun getById(linkId: UUID): PropertyLink = 31 | findById(linkId) ?: throw EntityNotFoundException( 32 | "ENTITY NOT FOUND! query: propertyLink.id=$linkId" 33 | ) 34 | 35 | fun requireExists(propertyLinkId: UUID): UUID = 36 | if (exists(propertyLinkId)) { 37 | propertyLinkId 38 | } else throw EntityNotFoundException( 39 | "ENTITY NOT FOUND! query: PropertyLink.id=$propertyLinkId" 40 | ) 41 | 42 | fun requireDoesNotExist(propertyLinkId: UUID): UUID = 43 | if (!exists(propertyLinkId)) { 44 | propertyLinkId 45 | } else throw EntityAlreadyExistException( 46 | "ENTITY ALREADY EXIST! query: PropertyLink.id=$propertyLinkId" 47 | ) 48 | 49 | fun insert(@Valid link: PropertyLink): PropertyLink { 50 | requireDoesNotExist(link.id) 51 | return propertyLinksRepository.save(link) 52 | } 53 | 54 | fun delete(link: PropertyLink) = propertyLinksRepository.delete(link) 55 | 56 | fun findByFromPropertyIdAndToPropertyId(fromPropertyId: UUID, toPropertyId: UUID): PropertyLink? = 57 | propertyLinksRepository 58 | .getByFromPropertyIdAndToPropertyId(fromPropertyId, toPropertyId) 59 | .toNullable() 60 | 61 | fun selectFromPropertyIdsWhereToPropertyIdEquals( 62 | toPropertyId: UUID, offset: Long = 0, limit: Long 63 | ): List { 64 | val resultSet: QueryResults = queryFactory 65 | .select(qPropertyLink.fromPropertyId) 66 | .from(qPropertyLink) 67 | .where( 68 | qPropertyLink.toPropertyId.eq(toPropertyId) and 69 | qPropertyLink.fromPropertyId.ne(toPropertyId) 70 | ) 71 | .orderBy(qPropertyLink.modified.desc()) 72 | .offset(offset) 73 | .limit(limit) 74 | .fetchResults() 75 | 76 | return resultSet.results 77 | } 78 | 79 | fun selectToPropertyIdsWhereFromPropertyIdEquals( 80 | fromPropertyId: UUID, offset: Long = 0, limit: Long 81 | ): List { 82 | val resultSet: QueryResults = queryFactory 83 | .select(qPropertyLink.toPropertyId) 84 | .from(qPropertyLink) 85 | .where( 86 | qPropertyLink.fromPropertyId.eq(fromPropertyId) and 87 | qPropertyLink.toPropertyId.ne(fromPropertyId) 88 | ) 89 | .orderBy(qPropertyLink.modified.desc()) 90 | .offset(offset) 91 | .limit(limit) 92 | .fetchResults() 93 | 94 | return resultSet.results 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/domain/jpa/services/JpaPropertyService.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.domain.jpa.services 2 | 3 | import com.example.demo.api.common.EntityAlreadyExistException 4 | import com.example.demo.api.common.EntityNotFoundException 5 | import com.example.demo.api.realestate.domain.jpa.entities.Property 6 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qProperty 7 | import com.example.demo.api.realestate.domain.jpa.repositories.PropertyRepository 8 | import com.example.demo.util.optionals.toNullable 9 | import com.querydsl.jpa.impl.JPAQuery 10 | import org.springframework.stereotype.Component 11 | import java.util.* 12 | import javax.persistence.EntityManager 13 | import javax.validation.Valid 14 | 15 | @Component 16 | class JpaPropertyService( 17 | private val propertyRepository: PropertyRepository, 18 | private val entityManager: EntityManager 19 | ) { 20 | fun exists(propertyId: UUID): Boolean = propertyRepository.exists(propertyId) 21 | 22 | fun findById(propertyId: UUID): Property? = 23 | propertyRepository 24 | .getById(propertyId) 25 | .toNullable() 26 | 27 | fun getById(propertyId: UUID): Property = 28 | findById(propertyId) ?: throw EntityNotFoundException( 29 | "ENTITY NOT FOUND! query: property.id=$propertyId" 30 | ) 31 | 32 | fun requireExists(propertyId: UUID): UUID = 33 | if (exists(propertyId)) { 34 | propertyId 35 | } else throw EntityNotFoundException( 36 | "ENTITY NOT FOUND! query: property.id=$propertyId" 37 | ) 38 | 39 | fun requireDoesNotExist(propertyId: UUID): UUID = 40 | if (!exists(propertyId)) { 41 | propertyId 42 | } else throw EntityAlreadyExistException( 43 | "ENTITY ALREADY EXIST! query: property.id=$propertyId" 44 | ) 45 | 46 | fun insert(@Valid property: Property): Property { 47 | requireDoesNotExist(property.id) 48 | return propertyRepository.save(property) 49 | } 50 | 51 | fun update(@Valid property: Property): Property { 52 | requireExists(property.id) 53 | return propertyRepository.save(property) 54 | } 55 | 56 | fun findByIdList(propertyIdList: List): List { 57 | val query = JPAQuery(entityManager) 58 | val resultSet = query.from(qProperty) 59 | .where( 60 | qProperty.id.`in`(propertyIdList) 61 | ) 62 | .fetchResults() 63 | 64 | return resultSet.results 65 | } 66 | 67 | fun findByClusterId(clusterId: UUID): List { 68 | val query = JPAQuery(entityManager) 69 | val resultSet = query.from(qProperty) 70 | .where( 71 | qProperty.clusterId.eq(clusterId) 72 | ) 73 | .fetchResults() 74 | 75 | return resultSet.results 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/create/CreateBrokerHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.create 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 5 | import com.example.demo.api.realestate.domain.jpa.entities.BrokerAddress 6 | import com.example.demo.api.realestate.domain.jpa.services.JpaBrokerService 7 | import com.example.demo.api.realestate.handler.common.response.BrokerResponse 8 | import org.springframework.stereotype.Component 9 | import org.springframework.validation.Validator 10 | import java.time.Instant 11 | import java.util.* 12 | 13 | @Component 14 | class CreateBrokerHandler( 15 | private val validator: Validator, 16 | private val jpaBrokerService: JpaBrokerService 17 | ) { 18 | fun handle(request: CreateBrokerRequest): BrokerResponse = 19 | execute(validator.validateRequest(request, "request")) 20 | 21 | private fun execute(request: CreateBrokerRequest): BrokerResponse { 22 | val newBroker = Broker( 23 | id = UUID.randomUUID(), 24 | created = Instant.now(), 25 | modified = Instant.now(), 26 | companyName = request.companyName?.trim() ?: "", 27 | firstName = request.firstName?.trim() ?: "", 28 | lastName = request.lastName?.trim() ?: "", 29 | email = request.email.trim(), 30 | phoneNumber = request.phoneNumber?.trim() ?: "", 31 | comment = request.comment?.trim() ?: "", 32 | address = BrokerAddress( 33 | country = request.address.country.trim(), 34 | city = request.address.city.trim(), 35 | state = request.address.state?.trim() ?: "", 36 | street = request.address.street?.trim() ?: "", 37 | number = request.address.number?.trim() ?: "" 38 | ) 39 | ) 40 | return BrokerResponse.of( 41 | broker = jpaBrokerService.insert(newBroker) 42 | ) 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/create/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.create 2 | 3 | import io.swagger.annotations.ApiModel 4 | import org.hibernate.validator.constraints.Email 5 | import org.hibernate.validator.constraints.NotBlank 6 | import javax.validation.Valid 7 | 8 | data class CreateBrokerRequest( 9 | @get: [Email] val email: String, 10 | val companyName: String?, 11 | val firstName: String?, 12 | val lastName: String?, 13 | val phoneNumber: String?, 14 | val comment: String?, 15 | @get: [Valid] val address: BrokerAddress 16 | ) { 17 | @ApiModel("CreateBrokerRequest.BrokerAddress") 18 | data class BrokerAddress( 19 | @get: [NotBlank] val country: String, 20 | @get: [NotBlank] val city: String, 21 | val state: String?, 22 | val street: String?, 23 | val number: String? 24 | ) 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/getbyid/GetBrokerByIdHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.getbyid 2 | 3 | import com.example.demo.api.realestate.domain.jpa.services.JpaBrokerService 4 | import com.example.demo.api.realestate.handler.common.response.BrokerResponse 5 | import org.springframework.stereotype.Component 6 | import java.util.* 7 | 8 | @Component 9 | class GetBrokerByIdHandler( 10 | private val jpaBrokerService: JpaBrokerService 11 | ) { 12 | fun handle(brokerId: UUID): BrokerResponse = 13 | BrokerResponse.of(broker = jpaBrokerService.getById(brokerId)) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/search/SearchBrokersHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.search 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 5 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qBroker 6 | import com.example.demo.querydsl.andAllOf 7 | import com.example.demo.querydsl.andAnyOf 8 | import com.example.demo.querydsl.orderBy 9 | import com.querydsl.jpa.impl.JPAQuery 10 | import org.springframework.stereotype.Component 11 | import org.springframework.validation.Validator 12 | import javax.persistence.EntityManager 13 | 14 | @Component 15 | class SearchBrokersHandler( 16 | private val validator: Validator, 17 | private val entityManager: EntityManager 18 | ) { 19 | fun handle(request: SearchBrokersRequest): SearchBrokersResponse = 20 | execute(request = validator.validateRequest(request, "request")) 21 | 22 | private fun execute(request: SearchBrokersRequest): SearchBrokersResponse { 23 | val filters = request.filter.toWhereExpressionDsl() 24 | val search = request.search.toWhereExpressionDsl() 25 | val order = request.orderBy.toOrderByExpressionDsl() 26 | 27 | val query = JPAQuery(entityManager) 28 | val resultSet = query.from(qBroker) 29 | .where( 30 | qBroker.isNotNull 31 | .andAllOf(filters) 32 | .andAnyOf(search) 33 | ) 34 | .orderBy(order) 35 | .offset(request.offset) 36 | .limit(request.limit) 37 | .fetchResults() 38 | 39 | return SearchBrokersResponse.of(resultSet) 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/search/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.search 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qBroker 4 | import com.example.demo.api.realestate.handler.common.request.QueryDslRequestParser 5 | import com.fasterxml.jackson.annotation.JsonValue 6 | import com.querydsl.core.types.OrderSpecifier 7 | import com.querydsl.core.types.dsl.BooleanExpression 8 | import io.swagger.annotations.ApiModel 9 | import java.time.Instant 10 | import java.util.* 11 | import javax.validation.constraints.Max 12 | import javax.validation.constraints.Min 13 | import com.example.demo.api.realestate.handler.common.request.QueryDslOperation as Op 14 | 15 | typealias Filters = List 16 | typealias Sorting = List 17 | 18 | private object FieldNames { 19 | const val id = "id" 20 | const val createdAt = "createdAt" 21 | const val modifiedAt = "modifiedAt" 22 | const val email = "email" 23 | const val address_country = "address.country" 24 | const val address_city = "address.city" 25 | } 26 | 27 | data class SearchBrokersRequest( 28 | @get:[Min(0)] val offset: Long = 0, 29 | @get:[Min(1) Max(100)] val limit: Long = 100, 30 | val filter: Filters? = null, 31 | val search: Filters? = null, 32 | val orderBy: Sorting? = null 33 | ) 34 | 35 | @ApiModel("SearchBrokersRequest.FieldAndValues") 36 | data class FieldAndValues(val field: FilterField, val values: List) 37 | 38 | @ApiModel("SearchBrokersRequest.SortableField") 39 | enum class SortField( 40 | fieldName: String, 41 | fieldOperation: String, 42 | val orderSpecifierSupplier: () -> OrderSpecifier<*> 43 | ) { 44 | modifiedAt_asc(FieldNames.modifiedAt, Op.ASC, { qBroker.modified.asc() }), 45 | modifiedAt_desc(FieldNames.modifiedAt, Op.DESC, { qBroker.modified.desc() }), 46 | 47 | createdAt_asc(FieldNames.createdAt, Op.ASC, { qBroker.created.asc() }), 48 | createdAt_desc(FieldNames.createdAt, Op.DESC, { qBroker.created.desc() }), 49 | 50 | email_asc(FieldNames.email, Op.ASC, { qBroker.email.asc() }), 51 | email_desc(FieldNames.email, Op.DESC, { qBroker.email.desc() }), 52 | 53 | ; 54 | 55 | @get:JsonValue val fieldExpression: String = "$fieldName-$fieldOperation" 56 | override fun toString(): String = fieldExpression 57 | fun toOrderSpecifier(): OrderSpecifier<*> = orderSpecifierSupplier() 58 | } 59 | 60 | @ApiModel("SearchPropertiesRequest.FilterField") 61 | enum class FilterField( 62 | fieldName: String, 63 | fieldOperation: String, 64 | val predicateSupplier: (FilterField, String) -> BooleanExpression 65 | ) { 66 | id_eq(FieldNames.id, Op.LIKE, { 67 | field, value: String -> 68 | qBroker.id.eq(value.toUUID(field)) 69 | }), 70 | 71 | modifiedAt_goe(FieldNames.modifiedAt, Op.GOE, { 72 | field, value: String -> 73 | qBroker.modified.goe(value.toInstant(field)) 74 | }), 75 | modifiedAt_loe(FieldNames.modifiedAt, Op.GOE, { 76 | field, value: String -> 77 | qBroker.modified.loe(value.toInstant(field)) 78 | }), 79 | 80 | email_like(FieldNames.email, Op.LIKE, { 81 | _, value: String -> 82 | qBroker.email.likeIgnoreCase(value) 83 | }), 84 | 85 | address_country_like(FieldNames.address_country, Op.LIKE, { 86 | _, value: String -> 87 | qBroker.address.country.likeIgnoreCase(value) 88 | }), 89 | address_city_like(FieldNames.address_city, Op.LIKE, { 90 | _, value: String -> 91 | qBroker.address.city.likeIgnoreCase(value) 92 | }), 93 | 94 | ; 95 | 96 | @get:JsonValue val fieldExpression: String = "$fieldName-$fieldOperation" 97 | override fun toString(): String = fieldExpression 98 | fun toBooleanExpression(values: List): List = 99 | values.map { value -> predicateSupplier(this, value) }.toList() 100 | } 101 | 102 | fun Filters?.toWhereExpressionDsl(): List = 103 | this?.flatMap { 104 | it.field.toBooleanExpression(it.values) 105 | } ?: emptyList() 106 | 107 | fun Sorting?.toOrderByExpressionDsl(): List> = 108 | this?.map { 109 | it.toOrderSpecifier() 110 | } ?: emptyList() 111 | 112 | private fun String.toInstant(field: FilterField): Instant = 113 | QueryDslRequestParser.asInstant( 114 | fieldValue = this, fieldExpression = field.fieldExpression 115 | ) 116 | 117 | private fun String.toUUID(field: FilterField): UUID = 118 | QueryDslRequestParser.asUUID( 119 | fieldValue = this, fieldExpression = field.fieldExpression 120 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/search/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.search 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 4 | import com.example.demo.api.realestate.handler.common.response.BrokerDto 5 | import com.example.demo.api.realestate.handler.common.response.ResponsePaging 6 | import com.querydsl.core.QueryResults 7 | 8 | data class SearchBrokersResponse( 9 | val paging: ResponsePaging, 10 | val brokers: List 11 | ) { 12 | companion object { 13 | fun of(resultSet: QueryResults): SearchBrokersResponse = 14 | SearchBrokersResponse( 15 | paging = ResponsePaging.ofResultSet(resultSet), 16 | brokers = resultSet.results.map { it.toDto() } 17 | ) 18 | } 19 | } 20 | 21 | private fun Broker.toDto() = BrokerDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/update/UpdateBrokerHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.update 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 5 | import com.example.demo.api.realestate.domain.jpa.entities.BrokerAddress 6 | import com.example.demo.api.realestate.domain.jpa.services.JpaBrokerService 7 | import com.example.demo.api.realestate.handler.common.response.BrokerResponse 8 | import com.example.demo.util.fp.pipe 9 | import org.springframework.stereotype.Component 10 | import org.springframework.validation.Validator 11 | import java.util.* 12 | 13 | @Component 14 | class UpdateBrokerHandler( 15 | private val validator: Validator, 16 | private val jpaBrokerService: JpaBrokerService 17 | ) { 18 | fun handle(brokerId: UUID, request: UpdateBrokerRequest): BrokerResponse = 19 | execute( 20 | brokerId = brokerId, 21 | request = validator.validateRequest(request, "request") 22 | ) 23 | 24 | private fun execute(brokerId: UUID, request: UpdateBrokerRequest): BrokerResponse { 25 | val property = jpaBrokerService.getById(brokerId) 26 | .copyWithUpdateRequest(request) 27 | 28 | return BrokerResponse.of( 29 | broker = jpaBrokerService.update(property) 30 | ) 31 | } 32 | } 33 | 34 | private fun Broker.copyWithUpdateRequest(req: UpdateBrokerRequest): Broker = 35 | this.pipe { 36 | if (req.email != null) it.copy(email = req.email) else it 37 | }.pipe { 38 | if (req.companyName != null) it.copy(companyName = req.companyName.trim()) else it 39 | }.pipe { 40 | if (req.comment != null) it.copy(comment = req.comment.trim()) else it 41 | }.pipe { 42 | if (req.firstName != null) it.copy(firstName = req.firstName.trim()) else it 43 | }.pipe { 44 | if (req.lastName != null) it.copy(firstName = req.lastName.trim()) else it 45 | }.pipe { 46 | if (req.phoneNumber != null) it.copy(phoneNumber = req.phoneNumber.trim()) else it 47 | }.pipe { 48 | if (req.address != null) { 49 | it.copy(address = it.address.copyWithUpdateRequest(req.address)) 50 | } else { 51 | it 52 | } 53 | } 54 | 55 | private fun BrokerAddress.copyWithUpdateRequest( 56 | req: UpdateBrokerRequest.UpdateBrokerAddress 57 | ): BrokerAddress = 58 | this.pipe { 59 | if (req.country != null) it.copy(country = req.country.trim()) else it 60 | }.pipe { 61 | if (req.city != null) it.copy(city = req.city.trim()) else it 62 | }.pipe { 63 | if (req.state != null) it.copy(state = req.state.trim()) else it 64 | }.pipe { 65 | if (req.street != null) it.copy(street = req.street.trim()) else it 66 | }.pipe { 67 | if (req.number != null) it.copy(number = req.number.trim()) else it 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/brokers/crud/update/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.brokers.crud.update 2 | 3 | import com.example.demo.api.common.validation.annotations.EmailOrNull 4 | import com.example.demo.api.common.validation.annotations.NotBlankOrNull 5 | import io.swagger.annotations.ApiModel 6 | import javax.validation.Valid 7 | 8 | data class UpdateBrokerRequest( 9 | @get: [EmailOrNull] val email: String?, 10 | val companyName: String?, 11 | val firstName: String?, 12 | val lastName: String?, 13 | val phoneNumber: String?, 14 | val comment: String?, 15 | @get: [Valid] val address: UpdateBrokerAddress? 16 | ) { 17 | @ApiModel("UpdateBrokerRequest.BrokerAddress") 18 | data class UpdateBrokerAddress( 19 | @get: [NotBlankOrNull] val country: String?, 20 | @get: [NotBlankOrNull] val city: String?, 21 | val state: String?, 22 | val street: String?, 23 | val number: String? 24 | ) 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/common/request/queryDsl.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.common.request 2 | 3 | import com.example.demo.api.common.BadRequestException 4 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyType 5 | import java.time.Instant 6 | import java.util.* 7 | 8 | object QueryDslOperation { 9 | const val ASC = "asc" 10 | const val DESC = "desc" 11 | const val LIKE = "like" 12 | const val EQ = "eq" 13 | const val GOE = "goe" 14 | const val LOE = "loe" 15 | } 16 | 17 | object QueryDslRequestParser { 18 | fun asInstant(fieldValue: String, fieldExpression: String): Instant { 19 | return try { 20 | Instant.parse(fieldValue) 21 | } catch (all: Exception) { 22 | throw BadRequestException( 23 | "Failed to parse field.value" 24 | + " provided by field.expression=$fieldExpression" 25 | + " as Instant!" 26 | + " reason=${all.message} !" 27 | + " example=$INSTANT_EXAMPLE" 28 | ) 29 | } 30 | } 31 | 32 | fun asUUID(fieldValue: String, fieldExpression: String): UUID { 33 | return try { 34 | UUID.fromString(fieldValue) 35 | } catch (all: Exception) { 36 | throw BadRequestException( 37 | "Failed to parse field.value" 38 | + " provided by field.expression=$fieldExpression" 39 | + " as UUID!" 40 | + " reason=${all.message} !" 41 | + " example=$UUID_EXAMPLE" 42 | ) 43 | } 44 | } 45 | 46 | fun asPropertyType(fieldValue: String, fieldExpression: String): PropertyType { 47 | return try { 48 | PropertyType.valueOf(fieldValue) 49 | } catch (all: Exception) { 50 | throw BadRequestException( 51 | "Failed to parse field.value" 52 | + " provided by field.expression=$fieldExpression" 53 | + " as PropertyType!" 54 | + " reason=${all.message} !" 55 | + " examples=$PROPERTY_TYPES_ALLOWED" 56 | ) 57 | } 58 | } 59 | 60 | private val INSTANT_EXAMPLE = Instant.ofEpochSecond(1501595115) 61 | private val UUID_EXAMPLE = UUID.randomUUID() 62 | private val PROPERTY_TYPES_ALLOWED = PropertyType.values() 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/common/response/broker.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.common.response 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Broker 4 | import com.example.demo.api.realestate.domain.jpa.entities.BrokerAddress 5 | import java.time.Instant 6 | import java.util.* 7 | 8 | data class BrokerResponse(val broker: BrokerDto) { 9 | companion object { 10 | fun of(broker: Broker): BrokerResponse = BrokerResponse(broker = broker.toDto()) 11 | } 12 | } 13 | 14 | data class BrokerDto( 15 | val id: UUID, 16 | val version: Int, 17 | val createdAt: Instant, 18 | val modified: Instant, 19 | 20 | val companyName: String, 21 | val firstName: String, 22 | val lastName: String, 23 | val email: String, 24 | val phoneNumber: String, 25 | val comment: String, 26 | 27 | val address: BrokerAddressDto 28 | ) { 29 | companion object { 30 | fun of(source: Broker): BrokerDto = 31 | BrokerDto( 32 | id = source.id, 33 | version = source.version, 34 | createdAt = source.getCreatedAt(), 35 | modified = source.getModifiedAt(), 36 | companyName = source.companyName, 37 | email = source.email, 38 | phoneNumber = source.phoneNumber, 39 | firstName = source.firstName, 40 | lastName = source.lastName, 41 | comment = source.comment, 42 | address = BrokerAddressDto.of(source.address) 43 | ) 44 | } 45 | } 46 | 47 | data class BrokerAddressDto( 48 | val country: String, 49 | val city: String, 50 | val state: String, 51 | val street: String, 52 | val number: String 53 | ) { 54 | companion object { 55 | fun of(source: BrokerAddress): BrokerAddressDto = 56 | BrokerAddressDto( 57 | country = source.country, 58 | city = source.city, 59 | state = source.state, 60 | street = source.street, 61 | number = source.number 62 | ) 63 | } 64 | } 65 | 66 | private fun Broker.toDto() = BrokerDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/common/response/property.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.common.response 2 | 3 | 4 | import com.example.demo.api.realestate.domain.jpa.entities.Property 5 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyAddress 6 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyType 7 | import com.fasterxml.jackson.annotation.JsonInclude 8 | import java.time.Instant 9 | import java.util.* 10 | 11 | data class PropertyResponse(val property: PropertyDto) { 12 | companion object { 13 | fun of(property: Property): PropertyResponse = PropertyResponse(property = property.toDto()) 14 | } 15 | } 16 | 17 | data class PropertiesResponse( 18 | @JsonInclude(JsonInclude.Include.NON_NULL) val paging: ResponsePaging?, 19 | val properties: List 20 | ) { 21 | companion object { 22 | fun of(properties: List): PropertiesResponse = 23 | PropertiesResponse( 24 | properties = properties.map { it.toDto() }, 25 | paging = null 26 | ) 27 | } 28 | } 29 | 30 | data class PropertyDto( 31 | val id: UUID, 32 | val version: Int, 33 | val createdAt: Instant, 34 | val modified: Instant, 35 | 36 | val clusterId: UUID?, 37 | val type: PropertyType, 38 | val name: String, 39 | val address: PropertyAddressDto 40 | ) { 41 | companion object { 42 | fun of(source: Property): PropertyDto = 43 | PropertyDto( 44 | id = source.id, 45 | version = source.version, 46 | createdAt = source.getCreatedAt(), 47 | modified = source.getModifiedAt(), 48 | clusterId = source.clusterId, 49 | type = source.type, 50 | name = source.name, 51 | address = PropertyAddressDto.of(source.address) 52 | ) 53 | } 54 | } 55 | 56 | data class PropertyAddressDto( 57 | val country: String, 58 | val city: String, 59 | val zip: String, 60 | val street: String, 61 | val number: String, 62 | val district: String, 63 | val neighborhood: String 64 | ) { 65 | companion object { 66 | fun of(source: PropertyAddress): PropertyAddressDto = 67 | PropertyAddressDto( 68 | country = source.country, 69 | city = source.city, 70 | zip = source.zip, 71 | street = source.street, 72 | number = source.number, 73 | district = source.district, 74 | neighborhood = source.neighborhood 75 | ) 76 | } 77 | } 78 | 79 | private fun Property.toDto() = PropertyDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/common/response/queryDsl.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.common.response 2 | 3 | import com.querydsl.core.QueryResults 4 | import io.swagger.annotations.ApiModel 5 | 6 | @ApiModel("QueryDslResponsePageMeta") 7 | data class ResponsePaging( 8 | val offset: Long, 9 | val limit: Long, 10 | val total: Long 11 | ) { 12 | companion object { 13 | fun ofResultSet(resultSet: QueryResults<*>): ResponsePaging { 14 | return ResponsePaging( 15 | offset = resultSet.offset, 16 | limit = resultSet.limit, 17 | total = resultSet.total 18 | ) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/create/CreatePropertyHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.create 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Property 5 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyAddress 6 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 7 | import com.example.demo.api.realestate.handler.common.response.PropertyResponse 8 | import org.springframework.stereotype.Component 9 | import org.springframework.validation.Validator 10 | import java.time.Instant 11 | import java.util.* 12 | 13 | @Component 14 | class CreatePropertyHandler( 15 | private val validator: Validator, 16 | private val jpaPropertyService: JpaPropertyService 17 | ) { 18 | 19 | fun handle(request: CreatePropertyRequest): PropertyResponse = 20 | execute(validator.validateRequest(request, "request")) 21 | 22 | private fun execute(request: CreatePropertyRequest): PropertyResponse { 23 | val newPropertyId = UUID.randomUUID() 24 | val property = Property( 25 | id = newPropertyId, 26 | created = Instant.now(), 27 | modified = Instant.now(), 28 | clusterId = null, 29 | type = request.type, 30 | name = request.name, 31 | address = PropertyAddress( 32 | country = request.address.country.trim(), 33 | city = request.address.city.trim(), 34 | zip = request.address.zip.trim(), 35 | street = request.address.street.trim(), 36 | number = request.address.number.trim(), 37 | district = request.address.district.trim(), 38 | neighborhood = request.address.neighborhood.trim() 39 | ) 40 | ) 41 | 42 | return PropertyResponse.of( 43 | property = jpaPropertyService.insert(property) 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/create/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.create 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyType 4 | import io.swagger.annotations.ApiModel 5 | import org.hibernate.validator.constraints.NotBlank 6 | import javax.validation.Valid 7 | 8 | data class CreatePropertyRequest( 9 | val type: PropertyType, 10 | @get: [NotBlank] val name: String, 11 | @get: [Valid] val address: PropertyAddress 12 | 13 | ) { 14 | @ApiModel("CreatePropertyRequest.PropertyAddress") 15 | data class PropertyAddress( 16 | @get: [NotBlank] val country: String, 17 | @get: [NotBlank] val city: String, 18 | val zip: String, 19 | val street: String, 20 | val number: String, 21 | val district: String, 22 | val neighborhood: String 23 | ) 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/getbyid/GetPropertyByIdHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.getbyid 2 | 3 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 4 | import com.example.demo.api.realestate.handler.common.response.PropertyResponse 5 | import org.springframework.stereotype.Component 6 | import java.util.* 7 | 8 | @Component 9 | class GetPropertyByIdHandler(private val jpaPropertyService: JpaPropertyService) { 10 | 11 | fun handle(propertyId: UUID): PropertyResponse = 12 | PropertyResponse.of(property = jpaPropertyService.getById(propertyId)) 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/search/SearchPropertiesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.search 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Property 5 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qProperty 6 | import com.example.demo.querydsl.andAllOf 7 | import com.example.demo.querydsl.andAnyOf 8 | import com.example.demo.querydsl.orderBy 9 | import com.querydsl.jpa.impl.JPAQuery 10 | import org.springframework.stereotype.Component 11 | import org.springframework.validation.Validator 12 | import javax.persistence.EntityManager 13 | 14 | @Component 15 | class SearchPropertiesHandler( 16 | private val validator: Validator, 17 | private val entityManager: EntityManager 18 | ) { 19 | fun handle(request: SearchPropertiesRequest): SearchPropertiesResponse = 20 | execute(request = validator.validateRequest(request, "request")) 21 | 22 | private fun execute(request: SearchPropertiesRequest): SearchPropertiesResponse { 23 | val filters = request.filter.toWhereExpressionDsl() 24 | val search = request.search.toWhereExpressionDsl() 25 | val order = request.orderBy.toOrderByExpressionDsl() 26 | 27 | val query = JPAQuery(entityManager) 28 | val resultSet = query.from(qProperty) 29 | .where( 30 | qProperty.isNotNull 31 | .andAllOf(filters) 32 | .andAnyOf(search) 33 | ) 34 | .orderBy(order) 35 | .offset(request.offset) 36 | .limit(request.limit) 37 | .fetchResults() 38 | 39 | return SearchPropertiesResponse.of(resultSet) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/search/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.search 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyType 4 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qProperty 5 | import com.example.demo.api.realestate.handler.common.request.QueryDslRequestParser 6 | import com.fasterxml.jackson.annotation.JsonValue 7 | 8 | import com.querydsl.core.types.OrderSpecifier 9 | import com.querydsl.core.types.dsl.BooleanExpression 10 | import io.swagger.annotations.ApiModel 11 | import java.time.Instant 12 | import java.util.* 13 | import javax.validation.constraints.Max 14 | import javax.validation.constraints.Min 15 | import com.example.demo.api.realestate.handler.common.request.QueryDslOperation as Op 16 | 17 | typealias Filters = List 18 | typealias Sorting = List 19 | 20 | private object FieldNames { 21 | const val id = "id" 22 | const val createdAt = "createdAt" 23 | const val modifiedAt = "modifiedAt" 24 | const val name = "name" 25 | const val type = "type" 26 | const val address_country = "address.country" 27 | const val address_city = "address.city" 28 | const val address_zip = "address.zip" 29 | const val address_street = "address.street" 30 | const val address_number = "address.street" 31 | const val address_district = "address.number" 32 | const val address_neighborhood = "address.neighborhood" 33 | } 34 | 35 | data class SearchPropertiesRequest( 36 | @get:[Min(0)] val offset: Long = 0, 37 | @get:[Min(1) Max(100)] val limit: Long = 100, 38 | val filter: Filters? = null, 39 | val search: Filters? = null, 40 | val orderBy: Sorting? = null 41 | ) 42 | 43 | @ApiModel("SearchPropertiesRequest.FieldAndValues") 44 | data class FieldAndValues(val field: FilterField, val values: List) 45 | 46 | 47 | @ApiModel("SearchPropertiesRequest.SortableField") 48 | enum class SortField( 49 | fieldName: String, 50 | fieldOperation: String, 51 | val orderSpecifierSupplier: () -> OrderSpecifier<*> 52 | ) { 53 | modifiedAt_asc(FieldNames.modifiedAt, Op.ASC, { qProperty.modified.asc() }), 54 | modifiedAt_desc(FieldNames.modifiedAt, Op.DESC, { qProperty.modified.desc() }), 55 | 56 | createdAt_asc(FieldNames.createdAt, Op.ASC, { qProperty.created.asc() }), 57 | createdAt_desc(FieldNames.createdAt, Op.DESC, { qProperty.created.desc() }), 58 | 59 | name_asc(FieldNames.name, Op.ASC, { qProperty.name.asc() }), 60 | name_desc(FieldNames.name, Op.DESC, { qProperty.name.desc() }), 61 | 62 | type_asc(FieldNames.type, Op.ASC, { qProperty.type.asc() }), 63 | type_desc(FieldNames.type, Op.DESC, { qProperty.type.desc() }), 64 | 65 | 66 | ; 67 | 68 | @get:JsonValue val fieldExpression: String = "$fieldName-$fieldOperation" 69 | 70 | override fun toString(): String { 71 | return fieldExpression 72 | } 73 | 74 | fun toOrderSpecifier(): OrderSpecifier<*> { 75 | return orderSpecifierSupplier() 76 | } 77 | } 78 | 79 | @ApiModel("SearchPropertiesRequest.FilterField") 80 | enum class FilterField( 81 | fieldName: String, 82 | fieldOperation: String, 83 | val predicateSupplier: (FilterField, String) -> BooleanExpression 84 | ) { 85 | 86 | modifiedAt_goe(FieldNames.modifiedAt, Op.GOE, { 87 | field, value: String -> 88 | qProperty.modified.goe(value.toInstant(field)) 89 | }), 90 | modifiedAt_loe(FieldNames.modifiedAt, Op.GOE, { 91 | field, value: String -> 92 | qProperty.modified.loe(value.toInstant(field)) 93 | }), 94 | 95 | createdAt_goe(FieldNames.createdAt, Op.GOE, { 96 | field, value: String -> 97 | qProperty.created.goe(value.toInstant(field)) 98 | }), 99 | createdAt_loe(FieldNames.createdAt, Op.GOE, { 100 | field, value: String -> 101 | qProperty.created.loe(value.toInstant(field)) 102 | }), 103 | 104 | name_like(FieldNames.name, Op.LIKE, { 105 | _, value: String -> 106 | qProperty.name.likeIgnoreCase(value) 107 | }), 108 | 109 | id_eq(FieldNames.id, Op.LIKE, { 110 | field, value: String -> 111 | qProperty.id.eq(value.toUUID(field)) 112 | }), 113 | 114 | type_eq(FieldNames.type, Op.LIKE, { 115 | field, value: String -> 116 | qProperty.type.eq(value.toPropertyType(field)) 117 | }), 118 | 119 | address_country_like(FieldNames.address_country, Op.LIKE, { 120 | _, value: String -> 121 | qProperty.address.country.likeIgnoreCase(value) 122 | }), 123 | address_city_like(FieldNames.address_city, Op.LIKE, { 124 | _, value: String -> 125 | qProperty.address.city.likeIgnoreCase(value) 126 | }), 127 | address_zip_like(FieldNames.address_zip, Op.LIKE, { 128 | _, value: String -> 129 | qProperty.address.zip.likeIgnoreCase(value) 130 | }), 131 | address_street_like(FieldNames.address_street, Op.LIKE, { 132 | _, value: String -> 133 | qProperty.address.street.likeIgnoreCase(value) 134 | }), 135 | address_number_like(FieldNames.address_number, Op.LIKE, { 136 | _, value: String -> 137 | qProperty.address.number.likeIgnoreCase(value) 138 | }), 139 | address_district_like(FieldNames.address_district, Op.LIKE, { 140 | _, value: String -> 141 | qProperty.address.district.likeIgnoreCase(value) 142 | }), 143 | address_neighborhood_like(FieldNames.address_neighborhood, Op.LIKE, { 144 | _, value: String -> 145 | qProperty.address.neighborhood.likeIgnoreCase(value) 146 | }), 147 | ; 148 | 149 | @get:JsonValue val fieldExpression: String = "$fieldName-$fieldOperation" 150 | 151 | override fun toString(): String = fieldExpression 152 | 153 | fun toBooleanExpression(values: List): List { 154 | return values.map { value -> predicateSupplier(this, value) }.toList() 155 | } 156 | } 157 | 158 | 159 | fun Filters?.toWhereExpressionDsl(): List { 160 | return this?.flatMap { 161 | it.field.toBooleanExpression(it.values) 162 | } ?: emptyList() 163 | } 164 | 165 | fun Sorting?.toOrderByExpressionDsl(): List> { 166 | return this?.map { 167 | it.toOrderSpecifier() 168 | } ?: emptyList() 169 | } 170 | 171 | private fun String.toInstant(field: FilterField): Instant { 172 | return QueryDslRequestParser.asInstant( 173 | fieldValue = this, fieldExpression = field.fieldExpression 174 | ) 175 | } 176 | 177 | private fun String.toUUID(field: FilterField): UUID { 178 | return QueryDslRequestParser.asUUID( 179 | fieldValue = this, fieldExpression = field.fieldExpression 180 | ) 181 | } 182 | 183 | private fun String.toPropertyType(field: FilterField): PropertyType { 184 | return QueryDslRequestParser.asPropertyType( 185 | fieldValue = this, fieldExpression = field.fieldExpression 186 | ) 187 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/search/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.search 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Property 4 | import com.example.demo.api.realestate.handler.common.response.PropertyDto 5 | import com.example.demo.api.realestate.handler.common.response.ResponsePaging 6 | import com.querydsl.core.QueryResults 7 | 8 | data class SearchPropertiesResponse( 9 | val paging: ResponsePaging, 10 | val properties: List 11 | ) { 12 | companion object { 13 | fun of(resultSet: QueryResults): SearchPropertiesResponse = 14 | SearchPropertiesResponse( 15 | paging = ResponsePaging.ofResultSet(resultSet), 16 | properties = resultSet.results.map { it.toDto() } 17 | ) 18 | } 19 | } 20 | 21 | private fun Property.toDto() = PropertyDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/update/UpdatePropertyHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.update 2 | 3 | import com.example.demo.api.common.validation.validateRequest 4 | import com.example.demo.api.realestate.domain.jpa.entities.Property 5 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyAddress 6 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 7 | import com.example.demo.api.realestate.handler.common.response.PropertyResponse 8 | import com.example.demo.util.fp.pipe 9 | import org.springframework.stereotype.Component 10 | import org.springframework.validation.Validator 11 | import java.util.* 12 | 13 | @Component 14 | class UpdatePropertyHandler( 15 | private val validator: Validator, 16 | private val jpaPropertyService: JpaPropertyService 17 | ) { 18 | fun handle(propertyId: UUID, request: UpdatePropertyRequest): PropertyResponse = 19 | execute( 20 | propertyId = propertyId, 21 | request = validator.validateRequest(request, "request") 22 | ) 23 | 24 | private fun execute(propertyId: UUID, request: UpdatePropertyRequest): PropertyResponse { 25 | val property = jpaPropertyService.getById(propertyId) 26 | .copyWithUpdateRequest(request) 27 | 28 | return PropertyResponse.of( 29 | property = jpaPropertyService.update(property) 30 | ) 31 | } 32 | } 33 | 34 | private fun Property.copyWithUpdateRequest(req: UpdatePropertyRequest): Property = 35 | this.pipe { 36 | if (req.type != null) it.copy(type = req.type) else it 37 | }.pipe { 38 | if (req.name != null) it.copy(name = req.name.trim()) else it 39 | }.pipe { 40 | if (req.address != null) { 41 | it.copy(address = it.address.copyWithUpdateRequest(req.address)) 42 | } else { 43 | it 44 | } 45 | } 46 | 47 | private fun PropertyAddress.copyWithUpdateRequest( 48 | req: UpdatePropertyRequest.UpdatePropertyAddressRequest 49 | ): PropertyAddress = 50 | this.pipe { 51 | if (req.country != null) it.copy(country = req.country.trim()) else it 52 | }.pipe { 53 | if (req.city != null) it.copy(city = req.city.trim()) else it 54 | }.pipe { 55 | if (req.zip != null) it.copy(zip = req.zip.trim()) else it 56 | }.pipe { 57 | if (req.street != null) it.copy(street = req.street.trim()) else it 58 | }.pipe { 59 | if (req.number != null) it.copy(number = req.number.trim()) else it 60 | }.pipe { 61 | if (req.district != null) it.copy(district = req.district.trim()) else it 62 | }.pipe { 63 | if (req.neighborhood != null) it.copy(neighborhood = req.neighborhood.trim()) else it 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/crud/update/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.crud.update 2 | 3 | import com.example.demo.api.common.validation.annotations.NotBlankOrNull 4 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyType 5 | import io.swagger.annotations.ApiModel 6 | import javax.validation.Valid 7 | 8 | data class UpdatePropertyRequest( 9 | val type: PropertyType?, 10 | @get: [NotBlankOrNull] val name: String?, 11 | @get: [Valid] val address: UpdatePropertyAddressRequest? 12 | 13 | ) { 14 | @ApiModel("UpdatePropertyRequest.PropertyAddress") 15 | data class UpdatePropertyAddressRequest( 16 | @get: [NotBlankOrNull] val country: String?, 17 | @get: [NotBlankOrNull] val city: String?, 18 | val zip: String?, 19 | val street: String?, 20 | val number: String?, 21 | val district: String?, 22 | val neighborhood: String? 23 | ) 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/create_links/LinkPropertiesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.create_links 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyLink 4 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyLinksService 5 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 6 | import org.springframework.stereotype.Component 7 | import java.time.Instant 8 | import java.util.* 9 | 10 | @Component 11 | class LinkPropertiesHandler( 12 | private val jpaPropertyService: JpaPropertyService, 13 | private val jpaPropertyLinksService: JpaPropertyLinksService 14 | ) { 15 | 16 | fun handle(request: LinkPropertiesRequest): LinkPropertiesResponse { 17 | return execute(request.validated()) 18 | } 19 | 20 | private fun execute(request: LinkPropertiesRequest): LinkPropertiesResponse { 21 | val propertyId1: UUID = jpaPropertyService.requireExists(request.propertyId1) 22 | val propertyId2: UUID = jpaPropertyService.requireExists(request.propertyId2) 23 | 24 | val links = markAsDuplicates(propertyId1 = propertyId1, propertyId2 = propertyId2) 25 | 26 | return LinkPropertiesResponse( 27 | links = links 28 | ) 29 | } 30 | 31 | private fun markAsDuplicates( 32 | propertyId1: UUID, propertyId2: UUID 33 | ): List { 34 | val property1ToProperty2Link = link(fromPropertyId = propertyId1, toPropertyId = propertyId2) 35 | val property2ToProperty1Link = link(fromPropertyId = propertyId2, toPropertyId = propertyId1) 36 | return listOf(property1ToProperty2Link, property2ToProperty1Link) 37 | } 38 | 39 | private fun link(fromPropertyId: UUID, toPropertyId: UUID): PropertyLink { 40 | val link = jpaPropertyLinksService.findByFromPropertyIdAndToPropertyId( 41 | fromPropertyId = fromPropertyId, toPropertyId = toPropertyId 42 | ) 43 | 44 | if (link != null) { 45 | // already linked 46 | return link 47 | } 48 | 49 | val newLink = PropertyLink( 50 | id = UUID.randomUUID(), 51 | created = Instant.now(), 52 | modified = Instant.now(), 53 | fromPropertyId = fromPropertyId, 54 | toPropertyId = toPropertyId 55 | ) 56 | 57 | return jpaPropertyLinksService.insert(newLink) 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/create_links/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.create_links 2 | 3 | import com.example.demo.api.common.BadRequestException 4 | import java.util.* 5 | 6 | data class LinkPropertiesRequest( 7 | val propertyId1: UUID, 8 | val propertyId2: UUID 9 | ) { 10 | fun validated(): LinkPropertiesRequest { 11 | if (propertyId1 == propertyId2) { 12 | throw BadRequestException("request.propertyId1 must not equal request.propertyId2 !") 13 | } 14 | return this 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/create_links/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.create_links 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.PropertyLink 4 | 5 | data class LinkPropertiesResponse( 6 | val links: List 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/duplicates/ListDuplicatePropertiesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.duplicates 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Property 4 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qProperty 5 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity.qPropertyLink 6 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 7 | import com.example.demo.querydsl.and 8 | import com.querydsl.core.QueryResults 9 | import com.querydsl.jpa.impl.JPAQuery 10 | import com.querydsl.jpa.impl.JPAQueryFactory 11 | import org.springframework.stereotype.Component 12 | import java.util.* 13 | import javax.persistence.EntityManager 14 | 15 | @Component 16 | class ListDuplicatePropertiesHandler( 17 | private val jpaPropertyService: JpaPropertyService, 18 | private val queryFactory: JPAQueryFactory, 19 | private val entityManager: EntityManager 20 | ) { 21 | 22 | fun handle(propertyId: UUID): ListDuplicatePropertiesResponse { 23 | val property = jpaPropertyService.getById(propertyId) 24 | val limit = 1000L 25 | val offset = 0L 26 | 27 | val linkedFromPropertyIdList: List = loadLinksFromProperty( 28 | propertyId = propertyId, limit = limit, offset = offset 29 | ) 30 | val linkedToPropertyIdList: List = loadLinksToProperty( 31 | propertyId = propertyId, limit = limit, offset = offset 32 | ) 33 | 34 | val mergedPropertyIdList = (linkedFromPropertyIdList + linkedToPropertyIdList) 35 | .distinct() 36 | 37 | val duplicateProperties: List = findPropertyIdsByIdList(mergedPropertyIdList) 38 | 39 | return ListDuplicatePropertiesResponse( 40 | linksTo = linkedToPropertyIdList, 41 | linksFrom = linkedFromPropertyIdList, 42 | duplicateProperties = duplicateProperties, 43 | property = property 44 | ) 45 | } 46 | 47 | private fun loadLinksFromProperty( 48 | propertyId: UUID, offset: Long, limit: Long 49 | ): List { 50 | val resultSet: QueryResults = queryFactory 51 | .select(qPropertyLink.toPropertyId) 52 | .from(qPropertyLink) 53 | .where( 54 | qPropertyLink.fromPropertyId.eq(propertyId) and 55 | qPropertyLink.toPropertyId.ne(propertyId) 56 | ) 57 | .orderBy(qPropertyLink.modified.desc()) 58 | .offset(offset) 59 | .limit(limit) 60 | .fetchResults() 61 | 62 | return resultSet.results 63 | } 64 | 65 | private fun loadLinksToProperty( 66 | propertyId: UUID, offset: Long, limit: Long 67 | ): List { 68 | val resultSet: QueryResults = queryFactory 69 | .select(qPropertyLink.fromPropertyId) 70 | .from(qPropertyLink) 71 | .where( 72 | qPropertyLink.toPropertyId.eq(propertyId) and 73 | qPropertyLink.fromPropertyId.ne(propertyId) 74 | ) 75 | .orderBy(qPropertyLink.modified.desc()) 76 | .offset(offset) 77 | .limit(limit) 78 | .fetchResults() 79 | 80 | return resultSet.results 81 | } 82 | 83 | private fun findPropertyIdsByIdList( 84 | propertyIdList: List 85 | ): List { 86 | val query = JPAQuery(entityManager) 87 | val resultSet = query.from(qProperty) 88 | .where( 89 | qProperty.id.`in`(propertyIdList) 90 | ) 91 | .fetchResults() 92 | 93 | return resultSet.results 94 | } 95 | } 96 | 97 | data class LinkIdAndPropertyId( 98 | val linkId: UUID, 99 | val propertyId: UUID 100 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/duplicates/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.duplicates 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Property 4 | import java.util.* 5 | 6 | data class ListDuplicatePropertiesResponse( 7 | val linksTo: List, 8 | val linksFrom: List, 9 | val duplicateProperties: List, 10 | val property: Property 11 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/linked_by/PropertyLinkedByHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.linked_by 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Property 4 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyLinksService 5 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 6 | import com.example.demo.api.realestate.handler.common.response.PropertyDto 7 | import org.springframework.stereotype.Component 8 | import java.util.* 9 | 10 | @Component 11 | class PropertyLinkedByHandler( 12 | private val jpaPropertyService: JpaPropertyService, 13 | private val jpaPropertyLinksService: JpaPropertyLinksService 14 | ) { 15 | 16 | fun handle(propertyId: UUID, limit: Long = 1000): PropertyLinkedByResponse { 17 | val property = jpaPropertyService.getById(propertyId) 18 | 19 | val propertyIdList: List = jpaPropertyLinksService 20 | .selectFromPropertyIdsWhereToPropertyIdEquals( 21 | toPropertyId = propertyId, 22 | limit = limit 23 | ) 24 | 25 | val properties: List = jpaPropertyService 26 | .findByIdList(propertyIdList) 27 | 28 | return PropertyLinkedByResponse( 29 | linkedBy = properties.map { it.toDto() }, 30 | property = property.toDto() 31 | ) 32 | } 33 | } 34 | 35 | private fun Property.toDto() = PropertyDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/linked_by/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.linked_by 2 | 3 | import com.example.demo.api.realestate.handler.common.response.PropertyDto 4 | 5 | data class PropertyLinkedByResponse( 6 | val linkedBy: List, 7 | val property: PropertyDto 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/linked_to/PropertyLinksToHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.linked_to 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.Property 4 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyLinksService 5 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 6 | import com.example.demo.api.realestate.handler.common.response.PropertyDto 7 | import org.springframework.stereotype.Component 8 | import java.util.* 9 | 10 | @Component 11 | class PropertyLinksToHandler( 12 | private val jpaPropertyService: JpaPropertyService, 13 | private val jpaPropertyLinksService: JpaPropertyLinksService 14 | ) { 15 | 16 | fun handle(propertyId: UUID, limit: Long = 1000): PropertyLinksToResponse { 17 | val property = jpaPropertyService.getById(propertyId) 18 | 19 | val propertyIdList: List = jpaPropertyLinksService 20 | .selectToPropertyIdsWhereFromPropertyIdEquals( 21 | fromPropertyId = propertyId, 22 | limit = limit 23 | ) 24 | 25 | val properties: List = jpaPropertyService 26 | .findByIdList(propertyIdList) 27 | 28 | return PropertyLinksToResponse( 29 | linksTo = properties.map { it.toDto() }, 30 | property = property.toDto() 31 | ) 32 | } 33 | } 34 | 35 | private fun Property.toDto() = PropertyDto.of(this) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/linked_to/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.linked_to 2 | 3 | import com.example.demo.api.realestate.handler.common.response.PropertyDto 4 | 5 | data class PropertyLinksToResponse( 6 | val linksTo: List, 7 | val property: PropertyDto 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/unlink/UnlinkPropertiesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.unlink 2 | 3 | import com.example.demo.api.realestate.domain.jpa.entities.QueryDslEntity 4 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyLinksService 5 | import com.example.demo.api.realestate.domain.jpa.services.JpaPropertyService 6 | import com.example.demo.querydsl.and 7 | import com.querydsl.core.QueryResults 8 | import com.querydsl.jpa.impl.JPAQueryFactory 9 | import org.springframework.stereotype.Component 10 | import java.util.* 11 | 12 | @Component 13 | class UnlinkPropertiesHandler( 14 | private val jpaPropertyService: JpaPropertyService, 15 | private val jpaPropertyLinksService: JpaPropertyLinksService, 16 | private val queryFactory: JPAQueryFactory 17 | ) { 18 | 19 | fun handle(request: UnlinkPropertiesRequest): UnlinkPropertiesResponse { 20 | return execute(request.validated()) 21 | } 22 | 23 | private fun execute(request: UnlinkPropertiesRequest): UnlinkPropertiesResponse { 24 | val propertyId: UUID = jpaPropertyService.requireExists(request.fromPropertyId) 25 | val toPropertyId: UUID = jpaPropertyService.requireExists(request.toPropertyId) 26 | 27 | unlinkFrom(fromPropertyId = propertyId, toPropertyId = toPropertyId) 28 | 29 | jpaPropertyLinksService.findByFromPropertyIdAndToPropertyId( 30 | fromPropertyId = propertyId, toPropertyId = toPropertyId 31 | ) 32 | 33 | val linkedToPropertyIds = loadLinkedToPropertyIds( 34 | fromPropertyId = propertyId, offset = 0, limit = 1000 35 | ) 36 | 37 | val linkedByPropertyIds = loadLinkedByPropertyIds( 38 | toPropertyId = propertyId, offset = 0, limit = 1000 39 | ) 40 | 41 | return UnlinkPropertiesResponse( 42 | linkedToPropertyIds = linkedToPropertyIds, 43 | linkedByPropertyIds = linkedByPropertyIds 44 | ) 45 | } 46 | 47 | private fun unlinkFrom(fromPropertyId: UUID, toPropertyId: UUID) { 48 | val link = jpaPropertyLinksService.findByFromPropertyIdAndToPropertyId( 49 | fromPropertyId, toPropertyId 50 | ) 51 | if (link != null) { 52 | jpaPropertyLinksService.delete(link) 53 | } 54 | } 55 | 56 | private fun loadLinkedToPropertyIds( 57 | fromPropertyId: UUID, offset: Long, limit: Long 58 | ): List { 59 | val resultSet: QueryResults = queryFactory 60 | .select(QueryDslEntity.qPropertyLink.toPropertyId) 61 | .from(QueryDslEntity.qPropertyLink) 62 | .where( 63 | QueryDslEntity.qPropertyLink.fromPropertyId.eq(fromPropertyId) and 64 | QueryDslEntity.qPropertyLink.toPropertyId.ne(fromPropertyId) 65 | ) 66 | .orderBy(QueryDslEntity.qPropertyLink.modified.desc()) 67 | .offset(offset) 68 | .limit(limit) 69 | .fetchResults() 70 | 71 | return resultSet.results 72 | } 73 | 74 | private fun loadLinkedByPropertyIds( 75 | toPropertyId: UUID, offset: Long, limit: Long 76 | ): List { 77 | val resultSet: QueryResults = queryFactory 78 | .select(QueryDslEntity.qPropertyLink.fromPropertyId) 79 | .from(QueryDslEntity.qPropertyLink) 80 | .where( 81 | QueryDslEntity.qPropertyLink.toPropertyId.eq(toPropertyId) and 82 | QueryDslEntity.qPropertyLink.fromPropertyId.ne(toPropertyId) 83 | ) 84 | .orderBy(QueryDslEntity.qPropertyLink.modified.desc()) 85 | .offset(offset) 86 | .limit(limit) 87 | .fetchResults() 88 | 89 | return resultSet.results 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/unlink/request.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.unlink 2 | 3 | 4 | import com.example.demo.api.common.BadRequestException 5 | import java.util.* 6 | 7 | data class UnlinkPropertiesRequest( 8 | val fromPropertyId: UUID, 9 | val toPropertyId: UUID 10 | ) { 11 | fun validated(): UnlinkPropertiesRequest { 12 | if (fromPropertyId == toPropertyId) { 13 | throw BadRequestException("request.propertyId1 must not equal request.propertyId2 !") 14 | } 15 | return this 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/handler/properties/links/unlink/response.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.handler.properties.links.unlink 2 | 3 | import java.util.* 4 | 5 | data class UnlinkPropertiesResponse( 6 | val linkedToPropertyIds: List, 7 | val linkedByPropertyIds: List 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/routing/brokers/BrokersCrudController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.routing.brokers 2 | 3 | import com.example.demo.api.realestate.handler.brokers.crud.create.CreateBrokerHandler 4 | import com.example.demo.api.realestate.handler.brokers.crud.create.CreateBrokerRequest 5 | import com.example.demo.api.realestate.handler.brokers.crud.getbyid.GetBrokerByIdHandler 6 | import com.example.demo.api.realestate.handler.brokers.crud.search.SearchBrokersHandler 7 | import com.example.demo.api.realestate.handler.brokers.crud.search.SearchBrokersRequest 8 | import com.example.demo.api.realestate.handler.brokers.crud.search.SearchBrokersResponse 9 | import com.example.demo.api.realestate.handler.brokers.crud.update.UpdateBrokerHandler 10 | import com.example.demo.api.realestate.handler.brokers.crud.update.UpdateBrokerRequest 11 | import com.example.demo.api.realestate.handler.common.response.BrokerResponse 12 | import org.springframework.web.bind.annotation.* 13 | import java.util.* 14 | 15 | @RestController 16 | @CrossOrigin(origins = arrayOf("*")) 17 | class BrokersCrudController( 18 | private val getByIdHandler: GetBrokerByIdHandler, 19 | private val createHandler: CreateBrokerHandler, 20 | private val updateHandler: UpdateBrokerHandler, 21 | private val searchHandler: SearchBrokersHandler 22 | ) { 23 | @GetMapping("/brokers/{brokerId}") 24 | fun getById(@PathVariable brokerId: UUID): BrokerResponse = 25 | getByIdHandler.handle(brokerId) 26 | 27 | @PostMapping("/brokers") 28 | fun create(@RequestBody request: CreateBrokerRequest): BrokerResponse = 29 | createHandler.handle(request) 30 | 31 | @PostMapping("/brokers/{brokerId}") 32 | fun update(@PathVariable brokerId: UUID, @RequestBody request: UpdateBrokerRequest): BrokerResponse = 33 | updateHandler.handle(brokerId, request) 34 | 35 | @PostMapping("/brokers/search") 36 | fun search(@RequestBody request: SearchBrokersRequest): SearchBrokersResponse = 37 | searchHandler.handle(request) 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/routing/properties/PropertiesCrudController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.routing.properties 2 | 3 | import com.example.demo.api.realestate.handler.common.response.PropertyResponse 4 | import com.example.demo.api.realestate.handler.properties.crud.create.CreatePropertyHandler 5 | import com.example.demo.api.realestate.handler.properties.crud.create.CreatePropertyRequest 6 | import com.example.demo.api.realestate.handler.properties.crud.getbyid.GetPropertyByIdHandler 7 | import com.example.demo.api.realestate.handler.properties.crud.search.SearchPropertiesHandler 8 | import com.example.demo.api.realestate.handler.properties.crud.search.SearchPropertiesRequest 9 | import com.example.demo.api.realestate.handler.properties.crud.search.SearchPropertiesResponse 10 | import com.example.demo.api.realestate.handler.properties.crud.update.UpdatePropertyHandler 11 | import com.example.demo.api.realestate.handler.properties.crud.update.UpdatePropertyRequest 12 | import org.springframework.web.bind.annotation.* 13 | import java.util.* 14 | 15 | @RestController 16 | @CrossOrigin(origins = arrayOf("*")) 17 | class PropertiesCrudController( 18 | private val createHandler: CreatePropertyHandler, 19 | private val updateHandler: UpdatePropertyHandler, 20 | private val getByIdHandler: GetPropertyByIdHandler, 21 | private val searchHandler: SearchPropertiesHandler 22 | ) { 23 | @GetMapping("/properties/{propertyId}") 24 | fun getById(@PathVariable propertyId: UUID): PropertyResponse = 25 | getByIdHandler.handle(propertyId) 26 | 27 | @PostMapping("/properties") 28 | fun create(@RequestBody request: CreatePropertyRequest): PropertyResponse = 29 | createHandler.handle(request) 30 | 31 | @PostMapping("/properties/{propertyId}") 32 | fun update(@PathVariable propertyId: UUID, @RequestBody request: UpdatePropertyRequest): PropertyResponse = 33 | updateHandler.handle(propertyId, request) 34 | 35 | @PostMapping("/properties/search") 36 | fun search(@RequestBody request: SearchPropertiesRequest): SearchPropertiesResponse = 37 | searchHandler.handle(request) 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/realestate/routing/properties/PropertiesLinksController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.realestate.routing.properties 2 | 3 | import com.example.demo.api.realestate.handler.properties.links.create_links.LinkPropertiesHandler 4 | import com.example.demo.api.realestate.handler.properties.links.create_links.LinkPropertiesRequest 5 | import com.example.demo.api.realestate.handler.properties.links.create_links.LinkPropertiesResponse 6 | import com.example.demo.api.realestate.handler.properties.links.duplicates.ListDuplicatePropertiesHandler 7 | import com.example.demo.api.realestate.handler.properties.links.duplicates.ListDuplicatePropertiesResponse 8 | import com.example.demo.api.realestate.handler.properties.links.linked_by.PropertyLinkedByHandler 9 | import com.example.demo.api.realestate.handler.properties.links.linked_by.PropertyLinkedByResponse 10 | import com.example.demo.api.realestate.handler.properties.links.linked_to.PropertyLinksToHandler 11 | import com.example.demo.api.realestate.handler.properties.links.linked_to.PropertyLinksToResponse 12 | import com.example.demo.api.realestate.handler.properties.links.unlink.UnlinkPropertiesHandler 13 | import com.example.demo.api.realestate.handler.properties.links.unlink.UnlinkPropertiesRequest 14 | import com.example.demo.api.realestate.handler.properties.links.unlink.UnlinkPropertiesResponse 15 | import org.springframework.web.bind.annotation.* 16 | import java.util.* 17 | 18 | @RestController 19 | @CrossOrigin(origins = arrayOf("*")) 20 | class PropertiesLinksController( 21 | private val linkHandler: LinkPropertiesHandler, 22 | private val unlinkHandler: UnlinkPropertiesHandler, 23 | private val listDuplicatesHandler: ListDuplicatePropertiesHandler, 24 | private val linksToHandler: PropertyLinksToHandler, 25 | private val linkedByHandler: PropertyLinkedByHandler 26 | ) { 27 | @PostMapping("/properties/link") 28 | fun link(@RequestBody request: LinkPropertiesRequest): LinkPropertiesResponse = 29 | linkHandler.handle(request) 30 | 31 | @PostMapping("/properties/unlink") 32 | fun unlink(@RequestBody request: UnlinkPropertiesRequest): UnlinkPropertiesResponse = 33 | unlinkHandler.handle(request) 34 | 35 | @GetMapping("/properties/{propertyId}/links-to") 36 | fun getLinksTo(@PathVariable propertyId: UUID): PropertyLinksToResponse = 37 | linksToHandler.handle(propertyId) 38 | 39 | @GetMapping("/properties/{propertyId}/linked-by") 40 | fun getLinkedBy(@PathVariable propertyId: UUID): PropertyLinkedByResponse = 41 | linkedByHandler.handle(propertyId) 42 | 43 | @GetMapping("/properties/{propertyId}/duplicates") 44 | fun listDuplicates(@PathVariable propertyId: UUID): ListDuplicatePropertiesResponse = 45 | listDuplicatesHandler.handle(propertyId) 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/auditing/JpaAuthorListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.auditing 2 | 3 | import com.example.demo.api.tweeter.domain.entities.Author 4 | import com.example.demo.api.tweeter.domain.services.EsAuthorService 5 | import com.example.demo.logging.AppLogger 6 | import org.springframework.stereotype.Component 7 | import javax.inject.Inject 8 | import javax.persistence.PrePersist 9 | import javax.persistence.PreUpdate 10 | 11 | @Component 12 | class JpaAuthorListener() { 13 | @PrePersist 14 | fun beforeInsert(o: Author) { 15 | LOG.info("audit: preInsert $o") 16 | } 17 | 18 | @PreUpdate 19 | fun beforeUpdate(o: Author) { 20 | LOG.info("audit: preUpdate $o") 21 | } 22 | 23 | companion object { 24 | private val LOG = AppLogger(this::class) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/auditing/JpaTweetListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.auditing 2 | 3 | import com.example.demo.api.tweeter.domain.entities.Author 4 | import com.example.demo.api.tweeter.domain.entities.Tweet 5 | import com.example.demo.logging.AppLogger 6 | import javax.persistence.PrePersist 7 | import javax.persistence.PreUpdate 8 | 9 | class JpaTweetListener { 10 | @PrePersist 11 | fun beforeInsert(o: Tweet) { 12 | LOG.info("audit: preInsert $o") 13 | } 14 | 15 | @PreUpdate 16 | fun beforeUpdate(o: Tweet) { 17 | LOG.info("audit: preUpdate $o") 18 | } 19 | 20 | companion object { 21 | private val LOG = AppLogger(this::class) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/entities/jpaEntities.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.entities 2 | 3 | import com.example.demo.api.tweeter.domain.auditing.JpaAuthorListener 4 | import com.example.demo.api.tweeter.domain.auditing.JpaTweetListener 5 | import com.example.demo.jpa.JpaTypes 6 | import com.example.demo.logging.AppLogger 7 | import org.hibernate.annotations.Type 8 | import org.hibernate.validator.constraints.Email 9 | import org.hibernate.validator.constraints.NotBlank 10 | import org.springframework.validation.annotation.Validated 11 | import java.time.Instant 12 | import java.util.* 13 | import javax.persistence.* 14 | import javax.validation.constraints.Size 15 | 16 | @Entity 17 | @EntityListeners(JpaAuthorListener::class) 18 | data class Author( 19 | @Id 20 | @Type(type = JpaTypes.UUID) 21 | val id: UUID, 22 | @Version 23 | val version: Int = -1, 24 | @Column(name = "created_at", nullable = false) 25 | private var createdAt: Instant, 26 | @Column(name = "modified_at", nullable = false) 27 | private var modifiedAt: Instant, 28 | 29 | @Column(name = "email", nullable = false) 30 | @get: [NotBlank Email] 31 | val email: String, 32 | @Column(name = "first_name", nullable = false) 33 | @get:[NotBlank Size(min = 3, max = 40)] 34 | val firstName: String, 35 | @Column(name = "last_name", nullable = false) 36 | @get:[NotBlank Size(min = 3, max = 40)] 37 | val lastName: String 38 | ) { 39 | 40 | fun getCreatedAt(): Instant = createdAt 41 | fun getModifiedAt(): Instant = modifiedAt 42 | 43 | @PostLoad 44 | private fun postLoad() { 45 | LOG.info("postLoad $this") 46 | } 47 | 48 | @PreUpdate @Validated 49 | private fun beforeUpdate() { 50 | this.modifiedAt = Instant.now() 51 | LOG.info("beforeUpdate $this") 52 | } 53 | 54 | @PrePersist @Validated 55 | private fun beforeInsert() { 56 | createdAt = Instant.now() 57 | modifiedAt = Instant.now() 58 | LOG.info("beforeInsert $this") 59 | } 60 | 61 | @PostPersist 62 | private fun afterInsert() { 63 | LOG.info("afterInsert $this") 64 | } 65 | 66 | companion object { 67 | private val LOG = AppLogger(this::class) 68 | } 69 | } 70 | 71 | @Entity 72 | @EntityListeners(JpaTweetListener::class) 73 | data class Tweet( 74 | @Id 75 | @Type(type = JpaTypes.UUID) 76 | val id: UUID, 77 | @Version 78 | val version: Int = -1, 79 | @Column(name = "created_at", nullable = false) 80 | private var createdAt: Instant, 81 | @Column(name = "modified_at", nullable = false) 82 | private var modifiedAt: Instant, 83 | 84 | @ManyToOne 85 | @JoinColumn(name = "author.id", nullable = false) 86 | val author: Author, 87 | 88 | @Column(name = "message", nullable = false) 89 | val message: String 90 | ) { 91 | 92 | fun getCreatedAt(): Instant = createdAt 93 | fun getModifiedAt(): Instant = modifiedAt 94 | 95 | @PostLoad 96 | private fun postLoad() { 97 | LOG.info("postLoad $this") 98 | } 99 | 100 | @PreUpdate @Validated 101 | private fun beforeUpdate() { 102 | this.modifiedAt = Instant.now() 103 | LOG.info("beforeUpdate $this") 104 | } 105 | 106 | @PrePersist @Validated 107 | private fun beforeInsert() { 108 | createdAt = Instant.now() 109 | modifiedAt = Instant.now() 110 | LOG.info("beforeInsert $this") 111 | } 112 | 113 | @PostPersist 114 | private fun afterInsert() { 115 | LOG.info("afterInsert $this") 116 | } 117 | 118 | companion object { 119 | private val LOG = AppLogger(this::class) 120 | } 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/repositories/jpaRepositories.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.repositories 2 | 3 | import com.example.demo.api.tweeter.domain.entities.Author 4 | import com.example.demo.api.tweeter.domain.entities.Tweet 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.stereotype.Repository 7 | import java.util.* 8 | 9 | @Repository 10 | //@Transactional(Transactional.TxType.MANDATORY) 11 | interface AuthorRepository : 12 | JpaRepository 13 | //CrudRepository 14 | { 15 | fun getById(id: UUID): Optional 16 | //override fun findAll(): Iterable 17 | } 18 | 19 | 20 | @Repository 21 | //@Transactional(Transactional.TxType.MANDATORY) 22 | interface TweetRepository : JpaRepository { 23 | fun getById(id: UUID): Optional 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/services/esServices.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.services 2 | 3 | import com.example.demo.api.tweeter.domain.entities.Author 4 | import com.example.demo.api.tweeter.domain.entities.Tweet 5 | import com.example.demo.es.EsClientService 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class EsAuthorService( 10 | private val esClientService: EsClientService 11 | ) { 12 | 13 | fun put(author: Author) { 14 | esClientService.put("/tweeter/author/${author.id}", author) 15 | } 16 | } 17 | 18 | @Component 19 | class EsTweetService( 20 | private val esClientService: EsClientService 21 | ) { 22 | 23 | fun put(tweet: Tweet) { 24 | esClientService.put("/tweeter/tweet/${tweet.id}", tweet) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/domain/services/jpaServices.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.domain.services 2 | 3 | import com.example.demo.api.common.EntityNotFoundException 4 | import com.example.demo.api.tweeter.domain.entities.Author 5 | import com.example.demo.api.tweeter.domain.entities.Tweet 6 | import com.example.demo.api.tweeter.domain.repositories.AuthorRepository 7 | import com.example.demo.api.tweeter.domain.repositories.TweetRepository 8 | import com.example.demo.util.optionals.toNullable 9 | import org.springframework.data.domain.Page 10 | import org.springframework.data.domain.PageRequest 11 | import org.springframework.stereotype.Component 12 | import java.util.* 13 | import javax.validation.Valid 14 | 15 | @Component 16 | class JpaTweetService( 17 | private val tweetRepository: TweetRepository 18 | 19 | ) { 20 | fun save(@Valid tweet: Tweet): Tweet { 21 | return tweetRepository.save(tweet) 22 | } 23 | 24 | fun findById(tweetId: UUID): Tweet? { 25 | return tweetRepository 26 | .getById(tweetId) 27 | .toNullable() 28 | } 29 | 30 | fun getById(tweetId: UUID): Tweet { 31 | return findById(tweetId) ?: throw EntityNotFoundException( 32 | "ENTITY NOT FOUND! query: tweet.id=$tweetId" 33 | ) 34 | } 35 | } 36 | 37 | @Component 38 | class JpaAuthorService( 39 | private val authorRepository: AuthorRepository 40 | ) { 41 | fun save(@Valid author: Author): Author { 42 | return authorRepository.save(author) 43 | } 44 | 45 | fun findById(authorId: UUID): Author? { 46 | return authorRepository 47 | .getById(authorId) 48 | .toNullable() 49 | } 50 | 51 | fun findAll(pageRequest: PageRequest): Page { 52 | return authorRepository.findAll(pageRequest) 53 | } 54 | 55 | fun getById(authorId: UUID): Author { 56 | return findById(authorId) ?: throw EntityNotFoundException( 57 | "ENTITY NOT FOUND! query: author.id=$authorId" 58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/routing/AuthorController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.routing 2 | 3 | import com.example.demo.api.common.Pagination 4 | import com.example.demo.api.tweeter.domain.entities.Author 5 | import com.example.demo.api.tweeter.domain.services.EsAuthorService 6 | import com.example.demo.api.tweeter.domain.services.JpaAuthorService 7 | import com.example.demo.logging.AppLogger 8 | import com.example.demo.util.fp.pipe 9 | import io.swagger.annotations.ApiModel 10 | import org.hibernate.validator.constraints.Email 11 | import org.hibernate.validator.constraints.NotBlank 12 | import org.springframework.data.domain.PageRequest 13 | import org.springframework.data.domain.Sort 14 | import org.springframework.http.MediaType 15 | import org.springframework.validation.annotation.Validated 16 | import org.springframework.web.bind.annotation.* 17 | import java.time.Instant 18 | import java.util.* 19 | import javax.validation.constraints.Size 20 | 21 | @RestController 22 | @CrossOrigin(origins = arrayOf("*")) 23 | class AuthorController( 24 | private val jpaAuthorService: JpaAuthorService, 25 | private val esAuthorService: EsAuthorService 26 | ) { 27 | 28 | @PostMapping("/authors", consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE), produces = arrayOf(MediaType.APPLICATION_JSON_VALUE)) 29 | fun create(@RequestBody @Validated request: CreateRequest): Any? { 30 | val author = Author( 31 | id = UUID.randomUUID(), 32 | createdAt = Instant.EPOCH, 33 | modifiedAt = Instant.EPOCH, 34 | email = request.email, 35 | firstName = request.firstName, 36 | lastName = request.lastName 37 | ) pipe { 38 | jpaAuthorService.save(it) 39 | } 40 | 41 | esAuthorService.put(author) 42 | 43 | return author 44 | } 45 | 46 | @PostMapping("/authors/{authorId}", consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE), produces = arrayOf(MediaType.APPLICATION_JSON_VALUE)) 47 | fun update( 48 | @PathVariable authorId: UUID, 49 | @RequestBody @Validated request: UpdateRequest 50 | ): Any? { 51 | LOG.info("update() authorId=$authorId payload=$request") 52 | val sourceAuthor: Author = jpaAuthorService 53 | .getById(authorId) 54 | 55 | val sinkAuthor: Author = 56 | sourceAuthor.copy( 57 | email = request.email, 58 | firstName = request.firstName, 59 | lastName = request.lastName 60 | ) pipe { 61 | val isChanged = it != sourceAuthor 62 | if (isChanged) { 63 | jpaAuthorService.save(it) 64 | } else { 65 | it 66 | } 67 | } 68 | 69 | esAuthorService.put(sinkAuthor) 70 | 71 | return sinkAuthor 72 | } 73 | 74 | @GetMapping("/authors/{authorId}") 75 | fun getOne(@PathVariable authorId: UUID): Any? { 76 | val author: Author = jpaAuthorService.getById(authorId) 77 | 78 | return author 79 | } 80 | 81 | @GetMapping("/authors") 82 | fun findAll( 83 | @RequestParam(defaultValue = "0") 84 | page: Int, 85 | @RequestParam(defaultValue = "20") 86 | pageSize: Int, 87 | @RequestParam(defaultValue = "firstName") 88 | sortedBy: String, 89 | @RequestParam(defaultValue = "DESC") 90 | sortDirection: Sort.Direction 91 | ): Any? { 92 | val pageRequest = PageRequest(page, pageSize, sortDirection, sortedBy) 93 | val pageResult = jpaAuthorService.findAll(pageRequest) 94 | val response = FindAllResponse( 95 | authors = pageResult.content.toList(), 96 | pagination = Pagination.ofPageResult(pageResult) 97 | ) 98 | 99 | return response 100 | } 101 | 102 | 103 | data class FindAllResponse( 104 | val authors: List, 105 | val pagination: Pagination? 106 | ) 107 | 108 | @ApiModel(value = "Author.CreateRequest") 109 | data class CreateRequest( 110 | @get:[NotBlank Email] 111 | val email: String, 112 | @get:[NotBlank Size(min = 1, max = 80)] 113 | val firstName: String, 114 | @get:[NotBlank Size(min = 1, max = 80)] 115 | val lastName: String 116 | ) 117 | 118 | @ApiModel(value = "Author.UpdateRequest") 119 | data class UpdateRequest( 120 | @get:[NotBlank Email] 121 | val email: String, 122 | @get:[NotBlank Size(min = 1, max = 80)] 123 | val firstName: String, 124 | @get:[NotBlank Size(min = 1, max = 80)] 125 | val lastName: String 126 | ) 127 | 128 | companion object { 129 | private val LOG = AppLogger(this::class) 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/api/tweeter/routing/TweetController.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.api.tweeter.routing 2 | 3 | import com.example.demo.api.tweeter.domain.entities.Author 4 | import com.example.demo.api.tweeter.domain.entities.Tweet 5 | import com.example.demo.api.tweeter.domain.services.EsTweetService 6 | import com.example.demo.api.tweeter.domain.services.JpaAuthorService 7 | import com.example.demo.api.tweeter.domain.services.JpaTweetService 8 | import com.example.demo.util.fp.pipe 9 | import io.swagger.annotations.ApiModel 10 | import org.springframework.http.MediaType 11 | import org.springframework.validation.annotation.Validated 12 | import org.springframework.web.bind.annotation.* 13 | import java.time.Instant 14 | import java.util.* 15 | 16 | @RestController 17 | @CrossOrigin(origins = arrayOf("*")) 18 | class TweetController( 19 | private val jpaTweetService: JpaTweetService, 20 | private val jpaAuthorService: JpaAuthorService, 21 | private val esTweetService: EsTweetService 22 | ) { 23 | 24 | @PostMapping("/tweets", consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE), produces = arrayOf(MediaType.APPLICATION_JSON_VALUE)) 25 | fun create(@Validated @RequestBody request: CreateRequest): Any? { 26 | val author: Author = jpaAuthorService.getById(authorId = request.authorId) 27 | 28 | val tweet = Tweet( 29 | id = UUID.randomUUID(), 30 | createdAt = Instant.EPOCH, 31 | modifiedAt = Instant.EPOCH, 32 | author = author, 33 | message = request.message 34 | ) pipe { 35 | jpaTweetService.save(it) 36 | } 37 | 38 | esTweetService.put(tweet) 39 | 40 | return tweet 41 | } 42 | 43 | @GetMapping("/tweets/{tweetId}") 44 | fun getOne(@PathVariable tweetId: UUID): Any? { 45 | val tweet: Tweet = jpaTweetService.getById(tweetId) 46 | 47 | return tweet 48 | } 49 | 50 | @PostMapping("/tweets/{tweetId}") 51 | fun update( 52 | @PathVariable tweetId: UUID, 53 | @Validated @RequestBody request: UpdateRequest 54 | ): Any? { 55 | val sourceTweet: Tweet = jpaTweetService.getById(tweetId = tweetId) 56 | 57 | val sinkTweet: Tweet = sourceTweet.pipe { 58 | if (request.authorId != null) { 59 | val author: Author = jpaAuthorService.getById(authorId = request.authorId) 60 | it.copy(author = author) 61 | } else { 62 | it 63 | } 64 | }.pipe { 65 | if (request.message != null) { 66 | it.copy(message = request.message) 67 | } else { 68 | it 69 | } 70 | } pipe { 71 | val isModified = it != sourceTweet 72 | if (isModified) { 73 | val savedTweet = jpaTweetService.save(it) 74 | esTweetService.put(savedTweet) 75 | savedTweet 76 | } else { 77 | it 78 | } 79 | } 80 | 81 | return sinkTweet 82 | } 83 | 84 | 85 | @ApiModel(value = "Tweet.CreateRequest") 86 | data class CreateRequest( 87 | val authorId: UUID, 88 | val message: String 89 | ) 90 | 91 | @ApiModel(value = "Tweet.UpdateRequest") 92 | data class UpdateRequest( 93 | val authorId: UUID?, 94 | val message: String? 95 | ) 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/configuration/beanValidation.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.configuration 2 | 3 | import org.hibernate.validator.HibernateValidator 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean 7 | import org.springframework.validation.beanvalidation.MethodValidationPostProcessor 8 | 9 | 10 | @Configuration 11 | class BeanValidationConfig { 12 | 13 | @Bean 14 | fun methodValidationPostProcessor(): MethodValidationPostProcessor { 15 | val mvProcessor = MethodValidationPostProcessor() 16 | mvProcessor.setValidator(validator()) 17 | return mvProcessor 18 | } 19 | 20 | //@Bean 21 | //fun validator() = LocalValidatorFactoryBean() 22 | 23 | @Bean 24 | fun validator(): LocalValidatorFactoryBean { 25 | val validator = LocalValidatorFactoryBean() 26 | validator.setProviderClass(HibernateValidator::class.java) 27 | validator.afterPropertiesSet() 28 | return validator 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/configuration/elasticSearch.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.configuration 2 | 3 | import com.example.demo.util.defer.Defer 4 | import org.elasticsearch.client.RestClient 5 | import org.elasticsearch.client.http.HttpHost 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | 10 | 11 | @Configuration 12 | class ElasticsearchConfig( 13 | @Value("\${elasticsearch.host}") 14 | private val host: String, 15 | @Value("\${elasticsearch.port}") 16 | private val port: Int, 17 | @Value("\${elasticsearch.clustername}") 18 | private val clusterName: String 19 | ) { 20 | private val defer = Defer() 21 | 22 | 23 | @Bean 24 | fun client(): RestClient { 25 | 26 | val lowLevelRestClient = RestClient.builder( 27 | HttpHost("localhost", 9200, "http"), 28 | HttpHost("localhost", 9201, "http")).build() 29 | 30 | 31 | defer.addGraceful { lowLevelRestClient.close() } 32 | 33 | return lowLevelRestClient 34 | } 35 | 36 | 37 | } 38 | 39 | /* 40 | @Configuration 41 | class EsHigh( 42 | private val lowLevelRestClient:RestClient 43 | ) { 44 | 45 | @Bean 46 | fun highLevel():RestHighLevelClient { 47 | val highlevelClient = RestHighLevelClient(lowLevelRestClient) 48 | 49 | return highlevelClient 50 | } 51 | } 52 | */ 53 | 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/configuration/jackson.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.configuration 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.databind.SerializationFeature 5 | import com.fasterxml.jackson.databind.util.ISO8601DateFormat 6 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | 10 | @Configuration 11 | class JacksonConfig { 12 | 13 | @Bean 14 | fun om(): ObjectMapper = defaultMapper() 15 | 16 | companion object { 17 | private val DATE_FORMAT_ISO8601 = ISO8601DateFormat() 18 | fun defaultMapper(): ObjectMapper { 19 | return jacksonObjectMapper() 20 | .findAndRegisterModules() 21 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 22 | .setDateFormat(DATE_FORMAT_ISO8601) 23 | // .disable(DeserializationFeature.) 24 | } 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/configuration/queryDsl.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.configuration 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import javax.inject.Provider 7 | import javax.persistence.EntityManager 8 | import javax.persistence.PersistenceContext 9 | 10 | 11 | @Configuration 12 | class QuerydslConfig( 13 | @PersistenceContext 14 | private val em: EntityManager 15 | ) { 16 | 17 | /* 18 | 19 | @Bean 20 | fun getJPAQueryFactory2():JPAQueryFactory { 21 | val provider=object : Provider { 22 | override fun get(): EntityManager { 23 | return em 24 | } 25 | } 26 | return JPAQueryFactory(provider) 27 | } 28 | */ 29 | 30 | 31 | val jpaQueryFactory: JPAQueryFactory 32 | @Bean 33 | get() { 34 | val provider = Provider { em } 35 | return JPAQueryFactory(provider) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/es/EsClientService.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.es 2 | 3 | import com.example.demo.configuration.JacksonConfig 4 | import org.elasticsearch.client.Response 5 | import org.elasticsearch.client.RestClient 6 | import org.elasticsearch.client.http.entity.ContentType 7 | import org.elasticsearch.client.http.nio.entity.NStringEntity 8 | import org.springframework.stereotype.Component 9 | import java.util.* 10 | 11 | @Component 12 | class EsClientService( 13 | private val restClient: RestClient 14 | ) { 15 | 16 | private val objectMapper = JacksonConfig.defaultMapper() 17 | 18 | fun put(endpoint: String, payload: Any): Response { 19 | return restClient.performRequest( 20 | "PUT", 21 | //"/twitter/foo/1", 22 | endpoint, 23 | Collections.emptyMap(), 24 | NStringEntity( 25 | objectMapper.writeValueAsString(payload), 26 | ContentType.APPLICATION_JSON 27 | ) 28 | ) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/jpa/JpaTypes.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.jpa 2 | 3 | object JpaTypes { 4 | const val UUID: String = "pg-uuid" 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/logging/AppLogger.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.logging 2 | 3 | /** 4 | * see: https://stackoverflow.com/questions/34416869/idiomatic-way-of-logging-in-kotlin 5 | */ 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | import kotlin.reflect.KClass 9 | import kotlin.reflect.full.companionObject 10 | 11 | 12 | object AppLogger { 13 | fun get(clazz: Class<*>): Logger { 14 | return LoggerFactory.getLogger(unwrapCompanionClass(clazz).name) 15 | } 16 | 17 | fun get(clazz: KClass<*>): Logger = get(clazz.java) 18 | operator fun invoke(clazz: Class<*>): Logger = get(clazz) 19 | operator fun invoke(clazz: KClass<*>): Logger = get(clazz) 20 | 21 | // unwrap companion class to enclosing class given a Java Class 22 | private fun unwrapCompanionClass(ofClass: Class): Class<*> { 23 | return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) { 24 | ofClass.enclosingClass 25 | } else { 26 | ofClass 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/main.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import org.springframework.boot.SpringApplication 4 | 5 | fun main(args: Array) { 6 | SpringApplication(DemoApplication::class.java) 7 | .run(*args) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/querydsl/queryDslExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.querydsl 2 | 3 | import com.querydsl.core.types.ExpressionUtils 4 | import com.querydsl.core.types.OrderSpecifier 5 | import com.querydsl.core.types.Predicate 6 | import com.querydsl.core.types.dsl.BooleanExpression 7 | import com.querydsl.core.types.dsl.DateTimePath 8 | import com.querydsl.core.types.dsl.NumberPath 9 | import com.querydsl.jpa.impl.JPAQuery 10 | import java.time.Instant 11 | 12 | infix fun BooleanExpression.and(right: Predicate?): BooleanExpression = this.and(right) 13 | 14 | fun BooleanExpression.andAllOf(predicates: List): BooleanExpression { 15 | val t = predicates.toTypedArray() 16 | return this.and(ExpressionUtils.allOf(*t)) 17 | } 18 | 19 | fun BooleanExpression.orAllOf(predicates: List): BooleanExpression { 20 | val t = predicates.toTypedArray() 21 | return this.or(ExpressionUtils.allOf(*t)) 22 | } 23 | 24 | fun BooleanExpression.andAnyOf(predicates: List): BooleanExpression { 25 | val t = predicates.toTypedArray() 26 | return this.and(ExpressionUtils.anyOf(*t)) 27 | } 28 | 29 | fun BooleanExpression.orAnyOf(predicates: List): BooleanExpression { 30 | val t = predicates.toTypedArray() 31 | return this.or(ExpressionUtils.anyOf(*t)) 32 | } 33 | 34 | fun JPAQuery.orderBy(orderSpecifier: List>): JPAQuery { 35 | if (orderSpecifier.isEmpty()) { 36 | return this 37 | } 38 | val t = orderSpecifier.toTypedArray() 39 | return this.orderBy(*t) 40 | } 41 | 42 | fun DateTimePath.eq(value: String): BooleanExpression = this.eq(Instant.parse(value)) 43 | fun DateTimePath.gt(value: String): BooleanExpression = this.gt(Instant.parse(value)) 44 | fun DateTimePath.lt(value: String): BooleanExpression = this.lt(Instant.parse(value)) 45 | fun DateTimePath.goe(value: String): BooleanExpression = this.goe(Instant.parse(value)) 46 | fun DateTimePath.loe(value: String): BooleanExpression = this.loe(Instant.parse(value)) 47 | 48 | fun NumberPath.eq(value: String): BooleanExpression = this.eq(value.toInt()) 49 | fun NumberPath.gt(value: String): BooleanExpression = this.gt(value.toInt()) 50 | fun NumberPath.lt(value: String): BooleanExpression = this.lt(value.toInt()) 51 | fun NumberPath.goe(value: String): BooleanExpression = this.goe(value.toInt()) 52 | fun NumberPath.loe(value: String): BooleanExpression = this.loe(value.toInt()) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/util/defer/kdefer.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.util.defer 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.ConcurrentLinkedDeque 5 | 6 | /* 7 | see: https://gobyexample.com/defer 8 | */ 9 | 10 | 11 | private typealias DeferredAction = () -> Any? 12 | 13 | class Defer : Closeable, AutoCloseable { 14 | 15 | private val stack = ConcurrentLinkedDeque() 16 | 17 | fun add(action: DeferredAction) = push(action) 18 | fun addGraceful(action: DeferredAction) { 19 | push({ 20 | try { 21 | action.invoke() 22 | } catch (ignore: Throwable) { 23 | } 24 | }) 25 | } 26 | 27 | override fun close() { 28 | while (stack.isNotEmpty()) { 29 | pop()?.invoke() 30 | } 31 | } 32 | 33 | private fun push(action: DeferredAction) = stack.push(action) 34 | 35 | private fun pop(): DeferredAction? { 36 | return try { 37 | if (stack.isNotEmpty()) stack.pop() else null 38 | } catch (ignore: Throwable) { 39 | null 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/util/fp/pipeTo.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.util.fp 2 | 3 | // see: https://github.com/MarioAriasC/funKTionale/blob/master/funktionale-pipe/src/main/kotlin/org/funktionale/pipe/pipe.kt 4 | infix inline fun T.pipe(t: (T) -> R): R = t(this) 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/demo/util/optionals/optionals.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.util.optionals 2 | 3 | import java.util.* 4 | 5 | fun Optional.toNullable(): T? = this.orElse(null) -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #elasticsearch.clustername = mkyong-cluster 2 | #elasticsearch.host = localhost 3 | #elasticsearch.port = 9300 4 | 5 | # Home directory of the embedded Elasticsearch instance. Default to the current working directory. 6 | #spring.data.elasticsearch.properties.path.home=target/elastic 7 | #spring.data.elasticsearch.properties.transport.tcp.connect_timeout=120s 8 | 9 | elasticsearch: 10 | useEmbedded: false 11 | clustername: "demo-cluster" 12 | host: "localhost" 13 | port: 9300 14 | 15 | spring: 16 | profiles: 17 | active: dev 18 | http: 19 | converters: 20 | preferred-json-mapper: jackson 21 | jpa: 22 | database: postgresql 23 | properties: 24 | hibernate: 25 | dialect: org.hibernate.dialect.PostgreSQLDialect 26 | # do not allow hibernate to change db schema! just validate it. 27 | ddl-auto: validate 28 | jackson: 29 | date-format: com.fasterxml.jackson.databind.util.ISO8601DateFormat 30 | serialization: 31 | write_dates_as_timestamps: false 32 | data: 33 | elasticsearch: 34 | cluster-nodes=localhost:9300 35 | #spring.data.elasticsearch.cluster-nodes= # Comma-separated list of cluster node addresses. If not specified, starts a client node. 36 | #spring.data.elasticsearch.properties.*= # Additional properties used to configure the client. 37 | #spring.data.elasticsearch.repositories.enabled=true 38 | --- 39 | spring: 40 | profiles: dev 41 | datasource: 42 | url: jdbc:postgresql://localhost:5432/example 43 | username: example 44 | password: example 45 | driver-class-name: org.postgresql.Driver 46 | 47 | jpa: 48 | properties: 49 | hibernate: 50 | show_sql: true 51 | format_sql: true 52 | use_sql_comments: true 53 | generate_statistics: false 54 | 55 | logging: 56 | level: 57 | org: 58 | hibernate: 59 | type: info 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__Initialize_Tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Author ( 2 | id UUID PRIMARY KEY, 3 | version INT NOT NULL, 4 | created_at TIMESTAMP NOT NULL, 5 | modified_at TIMESTAMP NOT NULL, 6 | 7 | first_name TEXT NOT NULL, 8 | last_name TEXT NOT NULL, 9 | email TEXT NOT NULL 10 | ); 11 | 12 | CREATE TABLE Tweet 13 | ( 14 | id UUID PRIMARY KEY, 15 | version INT NOT NULL, 16 | created_at TIMESTAMP NOT NULL, 17 | modified_at TIMESTAMP NOT NULL, 18 | 19 | author_id UUID NOT NULL REFERENCES Author, 20 | message TEXT NOT NULL 21 | ); 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__Tables_RealEstate.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Property ( 2 | id UUID PRIMARY KEY, 3 | version INT NOT NULL, 4 | created_at TIMESTAMP NOT NULL, 5 | modified_at TIMESTAMP NOT NULL, 6 | 7 | name varchar(255) NOT NULL, 8 | type varchar(255) NOT NULL, 9 | 10 | address_country varchar(255) NOT NULL, 11 | address_zip varchar(255) NOT NULL, 12 | address_city varchar(255) NOT NULL, 13 | address_street varchar(255) NOT NULL, 14 | address_number varchar(255) NOT NULL, 15 | address_district varchar(255) NOT NULL, 16 | address_neighborhood varchar(255) NOT NULL 17 | ); 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V3__Table_RealEstate_PropertyLinks.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE PropertyLinks ( 2 | id UUID PRIMARY KEY, 3 | version INT NOT NULL, 4 | created_at TIMESTAMP NOT NULL, 5 | modified_at TIMESTAMP NOT NULL, 6 | 7 | fk_from UUID NOT NULL REFERENCES Property, 8 | fk_to UUID NOT NULL REFERENCES Property 9 | ); 10 | 11 | ALTER TABLE PropertyLinks ADD CONSTRAINT from_and_to_unique UNIQUE (fk_from, fk_to); 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V4__Table_RealEstate_PropertyCluster.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE PropertyCluster ( 2 | id UUID PRIMARY KEY, 3 | version INT NOT NULL, 4 | 5 | created_at TIMESTAMP NOT NULL, 6 | modified_at TIMESTAMP NOT NULL 7 | ); 8 | 9 | ALTER TABLE Property 10 | ADD fk_property_cluster_id UUID NULL DEFAULT NULL REFERENCES PropertyCluster; 11 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V5__Table_RealEstate_Broker.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Broker ( 2 | id UUID PRIMARY KEY, 3 | version INT NOT NULL, 4 | created_at TIMESTAMP NOT NULL, 5 | modified_at TIMESTAMP NOT NULL, 6 | 7 | comment varchar(4096) NOT NULL, 8 | company_name varchar(255) NOT NULL, 9 | first_name varchar(255) NOT NULL, 10 | last_name varchar(255) NOT NULL, 11 | email varchar(255) NOT NULL, 12 | phone_number varchar(255) NOT NULL, 13 | 14 | address_country varchar(255) NOT NULL, 15 | address_state varchar(255) NOT NULL, 16 | address_city varchar(255) NOT NULL, 17 | address_street varchar(255) NOT NULL, 18 | address_number varchar(255) NOT NULL 19 | 20 | ); 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/testdata/cleanup.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM tweet; 2 | DELETE FROM author; 3 | 4 | DELETE FROM propertylinks; 5 | DELETE FROM property; 6 | DELETE FROM propertycluster; 7 | 8 | 9 | --------------------------------------------------------------------------------