├── infra └── mongodb │ └── data │ └── .gitignore ├── shared ├── src │ └── main │ │ ├── resources │ │ ├── shared_application.properties │ │ ├── shared_dev_application.properties │ │ ├── shared_prod_application.properties │ │ └── shared_test_application.properties │ │ └── kotlin │ │ └── com │ │ └── example │ │ ├── models.kt │ │ └── config.kt ├── README.md └── build.gradle.kts ├── settings.gradle ├── ui-app ├── src │ ├── main │ │ ├── resources │ │ │ ├── bootstrap.yml │ │ │ ├── application.yml │ │ │ └── templates │ │ │ │ ├── index.html │ │ │ │ ├── quotes.html │ │ │ │ └── guestbook.html │ │ ├── docker │ │ │ └── Dockerfile │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── UiApplication.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── UiApplicationTests.kt ├── README.md └── build.gradle.kts ├── docs ├── reactive-apps.png └── docker.md ├── stream-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── bootstrap.yml │ │ │ ├── banner.txt │ │ │ └── application.yml │ │ ├── docker │ │ │ └── Dockerfile │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── StreamApplication.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── StreamApplicationTests.kt ├── README.md └── build.gradle.kts ├── mongo-data-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── bootstrap.yml │ │ │ ├── data │ │ │ │ ├── guestbook.json │ │ │ │ └── events.json │ │ │ └── application.yml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── domain │ │ │ │ ├── Link.kt │ │ │ │ ├── GuestBookEntry.kt │ │ │ │ ├── Language.kt │ │ │ │ ├── User.kt │ │ │ │ └── Event.kt │ │ │ │ ├── DataApplication.kt │ │ │ │ ├── config │ │ │ │ └── WebConfig.kt │ │ │ │ ├── web │ │ │ │ ├── handler │ │ │ │ │ ├── EventHandler.kt │ │ │ │ │ ├── GuestBookHandler.kt │ │ │ │ │ └── UserHandler.kt │ │ │ │ └── routes │ │ │ │ │ └── ApiRoutes.kt │ │ │ │ ├── DataInitializer.kt │ │ │ │ ├── repository │ │ │ │ ├── EventRepository.kt │ │ │ │ ├── UserRepository.kt │ │ │ │ └── GuestBookRepository.kt │ │ │ │ └── util │ │ │ │ └── Extensions.kt │ │ └── docker │ │ │ └── Dockerfile │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ ├── util │ │ └── ExtensionsTest.kt │ │ └── integration │ │ ├── AbstractIntegrationTests.kt │ │ ├── EventRepositoryTests.kt │ │ ├── EventIntegrationTests.kt │ │ └── UserIntegrationTests.kt ├── README.md └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .editorconfig ├── .gitignore ├── docker-compose.yml ├── gradle.properties ├── LICENSE ├── docker-compose-all.yml ├── gradlew.bat ├── README.md └── gradlew /infra/mongodb/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /shared/src/main/resources/shared_application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shared/src/main/resources/shared_dev_application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shared/src/main/resources/shared_prod_application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shared/src/main/resources/shared_test_application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'shared', 'mongo-data-service', 'stream-service', 'ui-app' 2 | -------------------------------------------------------------------------------- /ui-app/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: ui-app -------------------------------------------------------------------------------- /docs/reactive-apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmlking/reactive-apps/HEAD/docs/reactive-apps.png -------------------------------------------------------------------------------- /stream-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: stream-service -------------------------------------------------------------------------------- /mongo-data-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: mongo-data-service 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmlking/reactive-apps/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/domain/Link.kt: -------------------------------------------------------------------------------- 1 | package com.example.domain 2 | 3 | 4 | data class Link( 5 | val name: String, 6 | val url: String 7 | ) 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-rc-2-all.zip 6 | -------------------------------------------------------------------------------- /shared/README.md: -------------------------------------------------------------------------------- 1 | Shared Lib 2 | ========== 3 | common shared components, utils etc. 4 | 5 | ### Run 6 | ```bash 7 | gradle :shared:clean 8 | ``` 9 | ### Test 10 | ```bash 11 | gradle :shared:test 12 | ``` 13 | ### Build 14 | ```bash 15 | gradle :shared:build 16 | ``` 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [gradlew.bat] 16 | end_of_line = crlf 17 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/DataApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.util.run 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | 6 | @SpringBootApplication 7 | class DataApplication 8 | 9 | fun main(args: Array) { 10 | run(DataApplication::class, *args) 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | 27 | ### Project ### 28 | TODO.md 29 | todo/ -------------------------------------------------------------------------------- /stream-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ( 2 | )\ ) ( ( ) 3 | (()/( ( ) )\))( ' ( ( /( 4 | /(_)) ))\ ( ( ((_)()\ ) ( )( )\()) ( 5 | (_)) /((_) )\ ' )\ _(())\_)() )\ (()\ ((_)\ )\ 6 | / __|(_))( _((_)) ((_)\ \((_)/ /((_) ((_)| |(_)((_) 7 | \__ \| || || ' \()/ _ \ \ \/\/ // _ \| '_|| / / (_-< 8 | |___/ \_,_||_|_|_| \___/ \_/\_/ \___/|_| |_\_\ /__/ -------------------------------------------------------------------------------- /stream-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: ${PORT:8082} 3 | #logging: 4 | # level: 5 | # root: debug 6 | endpoints: 7 | default: 8 | web: 9 | enabled: true 10 | spring: 11 | application: 12 | name: stream-service 13 | jackson: 14 | serialization: 15 | write-date-timestamps-as-nanoseconds: false 16 | 17 | --- 18 | spring: 19 | profiles: docker 20 | 21 | --- 22 | spring: 23 | profiles: cloud 24 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/domain/GuestBookEntry.kt: -------------------------------------------------------------------------------- 1 | package com.example.domain 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.mongodb.core.mapping.Document 5 | 6 | import java.time.LocalDateTime; 7 | 8 | @Document 9 | data class GuestBookEntry( 10 | @Id val id: String?, 11 | val name: String, 12 | val comment: String, 13 | val date: LocalDateTime = LocalDateTime.now() 14 | ) -------------------------------------------------------------------------------- /ui-app/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: ${PORT:8080} 3 | #logging: 4 | # level: 5 | # root: debug 6 | endpoints: 7 | default: 8 | web: 9 | enabled: true 10 | spring: 11 | application: 12 | name: ui-app 13 | app: 14 | mongoApiUrl: ${MONGO_API_URL:http://localhost:8081} 15 | streamApiUrl: ${STREAM_API_URL:http://localhost:8082} 16 | 17 | --- 18 | spring: 19 | profiles: docker 20 | 21 | --- 22 | spring: 23 | profiles: cloud 24 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/com/example/models.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import java.math.BigDecimal 4 | import java.time.Instant 5 | import java.time.LocalDateTime 6 | 7 | data class Quote(val ticker: String, val price: BigDecimal, val instant: Instant = Instant.now()) 8 | 9 | data class GuestBookEntryDTO( 10 | val id: String?, 11 | val name: String, 12 | val comment: String, 13 | val date: LocalDateTime = LocalDateTime.now() 14 | ) { 15 | val stringDate: String 16 | get() = "$date" 17 | } -------------------------------------------------------------------------------- /ui-app/src/test/kotlin/com/example/UiApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.springframework.boot.test.context.SpringBootTest 6 | import org.springframework.test.context.junit.jupiter.SpringExtension 7 | 8 | @ExtendWith(SpringExtension::class) 9 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 10 | class UiApplicationTests { 11 | 12 | @Test 13 | fun contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | MAINTAINER "Sumanth Chinthagunta" 3 | 4 | ARG JAR_NAME 5 | ARG JAVA_OPTS 6 | ARG PORT 7 | VOLUME /tmp 8 | WORKDIR /app 9 | ADD $JAR_NAME app.jar 10 | RUN touch app.jar 11 | EXPOSE $PORT 12 | ENV PORT=$PORT 13 | ENV JAVA_OPTS=$JAVA_OPTS 14 | HEALTHCHECK --interval=90s --timeout=10s --retries=3 CMD curl --fail http://localhost:${PORT}/application/health || exit 1 15 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dserver.port=$PORT -Djava.security.egd=file:/dev/./urandom -jar app.jar" ] 16 | -------------------------------------------------------------------------------- /stream-service/src/test/kotlin/com/example/StreamApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.springframework.boot.test.context.SpringBootTest 6 | import org.springframework.test.context.junit.jupiter.SpringExtension 7 | 8 | @ExtendWith(SpringExtension::class) 9 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 10 | class StreamApplicationTests { 11 | 12 | @Test 13 | fun contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/com/example/config.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.context.annotation.PropertySource 5 | import org.springframework.context.annotation.PropertySources 6 | 7 | @Configuration 8 | @PropertySources( 9 | PropertySource(value = "classpath:shared_application.properties"), 10 | PropertySource(value = "classpath:shared_\${spring.profiles.active}_application.properties", ignoreResourceNotFound = true)) 11 | class SharedConfiguration 12 | -------------------------------------------------------------------------------- /mongo-data-service/src/test/kotlin/com/example/util/ExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.util 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ExtensionsTest { 7 | 8 | @Test 9 | fun toSlug() { 10 | assertEquals("", "".toSlug()) 11 | assertEquals("-", "---".toSlug()) 12 | assertEquals("billetterie-mixit-2017-pre-inscription", "Billetterie MiXiT 2017 : pré-inscription".toSlug()) 13 | assertEquals("mixit-2017-ticketing-pre-registration", "MiXiT 2017 ticketing: pre-registration".toSlug()) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.bundling.Jar 2 | import org.springframework.boot.gradle.tasks.bundling.BootJar 3 | 4 | // Prevents common subproject dependencies from being included in the common jar itself. 5 | // Without this, each subproject that included common would include each shared dependency twice. 6 | //tasks { 7 | // withType { 8 | // enabled = false 9 | // } 10 | // withType { 11 | // enabled = true 12 | // } 13 | //} 14 | val bootJar: BootJar by tasks 15 | bootJar.enabled = false 16 | val jar: Jar by tasks 17 | jar.enabled = true 18 | -------------------------------------------------------------------------------- /ui-app/README.md: -------------------------------------------------------------------------------- 1 | UI App 2 | ====== 3 | UI Client App 4 | 5 | ##### Technology stack 6 | * Spring Boot 7 | * Spring WebFlux 8 | 9 | ##### Features 10 | * Traditional Annotation-based Routes 11 | * WebClient 12 | * Use of webjars 13 | * Thymeleaf 14 | 15 | ### Running 16 | > use `./gradlew` instead of `gradle` if you didn't installed `gradle` 17 | ```bash 18 | gradle ui-app:bootRun 19 | ``` 20 | ### Testing 21 | ```bash 22 | gradle ui-app:test 23 | ``` 24 | ### Building 25 | ```bash 26 | gradle ui-app:build 27 | # build docker image 28 | gradle ui-app:docker 29 | ``` 30 | 31 | ### App 32 | http://localhost:8080/ -------------------------------------------------------------------------------- /ui-app/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:alpine 2 | MAINTAINER "Sumanth Chinthagunta" 3 | 4 | RUN apk add --update curl \ 5 | && rm -rf /var/cache/apk/* 6 | 7 | ARG JAR_NAME 8 | ARG JAVA_OPTS 9 | ARG PORT 10 | #VOLUME /tmp 11 | WORKDIR /app 12 | ADD $JAR_NAME app.jar 13 | RUN touch app.jar 14 | EXPOSE $PORT 15 | ENV PORT=$PORT 16 | ENV JAVA_OPTS=$JAVA_OPTS 17 | HEALTHCHECK --interval=90s --timeout=10s --retries=3 CMD curl --fail http://localhost:${PORT}/application/health || exit 1 18 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dserver.port=$PORT -Djava.security.egd=file:/dev/./urandom -jar app.jar" ] 19 | -------------------------------------------------------------------------------- /stream-service/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:alpine 2 | MAINTAINER "Sumanth Chinthagunta" 3 | 4 | RUN apk add --update curl \ 5 | && rm -rf /var/cache/apk/* 6 | 7 | ARG JAR_NAME 8 | ARG JAVA_OPTS 9 | ARG PORT 10 | #VOLUME /tmp 11 | WORKDIR /app 12 | ADD $JAR_NAME app.jar 13 | RUN touch app.jar 14 | EXPOSE $PORT 15 | ENV PORT=$PORT 16 | ENV JAVA_OPTS=$JAVA_OPTS 17 | HEALTHCHECK --interval=90s --timeout=10s --retries=3 CMD curl --fail http://localhost:${PORT}/application/health || exit 1 18 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dserver.port=$PORT -Djava.security.egd=file:/dev/./urandom -jar app.jar" ] 19 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/domain/Language.kt: -------------------------------------------------------------------------------- 1 | package com.example.domain 2 | 3 | 4 | enum class Language { 5 | FRENCH, 6 | ENGLISH; 7 | 8 | fun toLanguageTag() { 9 | name.toLowerCase().subSequence(0, 2) 10 | } 11 | 12 | companion object { 13 | fun findByTag(name: String): Language { 14 | val language = Language.values().filter { value -> value.name.toLowerCase().substring(0, 2) == name } 15 | if (language.isEmpty()) { 16 | throw IllegalArgumentException() 17 | } 18 | return language.first() 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/config/WebConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.web.reactive.config.CorsRegistry 5 | import org.springframework.web.reactive.config.WebFluxConfigurationSupport 6 | 7 | @Configuration 8 | class WebConfig: WebFluxConfigurationSupport() { 9 | 10 | override fun addCorsMappings(registry: CorsRegistry) { 11 | registry//.addMapping("/**") 12 | .addMapping("/api/**") 13 | .allowedOrigins("*") 14 | .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/web/handler/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.web.handler 2 | 3 | import com.example.domain.Event 4 | import com.example.repository.EventRepository 5 | import com.example.util.json 6 | import org.springframework.stereotype.Component 7 | import org.springframework.web.reactive.function.server.* 8 | import org.springframework.web.reactive.function.server.ServerResponse.ok 9 | 10 | 11 | @Component 12 | class EventHandler(val repository: EventRepository) { 13 | 14 | fun findOne(req: ServerRequest) = ok().json().body(repository.findById(req.pathVariable("id"))) 15 | 16 | fun findAll(req: ServerRequest) = ok().json().body(repository.findAll()) 17 | 18 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mongo: 4 | image: reactive/mongo-data-service:0.1.0-SNAPSHOT 5 | networks: 6 | - reactive-network 7 | stream: 8 | image: reactive/stream-service:0.1.0-SNAPSHOT 9 | networks: 10 | - reactive-network 11 | app: 12 | image: reactive/ui-app:0.1.0-SNAPSHOT 13 | ports: 14 | - 8080:8080 15 | environment: 16 | - app.mongoApiUrl=http://mongo:8080 17 | - app.streamApiUrl=http://stream:8080 18 | networks: 19 | - reactive-network 20 | links: 21 | - mongo 22 | - stream 23 | depends_on: 24 | - mongo 25 | - stream 26 | networks: 27 | reactive-network: 28 | driver: bridge 29 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/domain/User.kt: -------------------------------------------------------------------------------- 1 | package com.example.domain 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.mongodb.core.mapping.Document 5 | 6 | 7 | @Document 8 | data class User( 9 | @Id val login: String, 10 | val firstname: String, 11 | val lastname: String, 12 | val email: String?, 13 | val company: String? = null, 14 | val description: Map = emptyMap(), 15 | val emailHash: String? = null, 16 | val photoUrl: String? = null, 17 | val role: Role = Role.USER, 18 | val links: List = emptyList(), 19 | val legacyId: Long? = null 20 | ) 21 | 22 | enum class Role { 23 | STAFF, 24 | USER 25 | } -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/DataInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.repository.EventRepository1 4 | import com.example.repository.GuestBookRepository 5 | import com.example.repository.UserRepository 6 | import org.springframework.context.event.ContextRefreshedEvent 7 | import org.springframework.context.event.EventListener 8 | import org.springframework.stereotype.Component 9 | 10 | @Component 11 | class DataInitializer(val userRepository: UserRepository, 12 | val guestBookRepository: GuestBookRepository, 13 | val eventRepository: EventRepository1) { 14 | 15 | @EventListener(ContextRefreshedEvent::class) 16 | fun init() { 17 | userRepository.initData() 18 | guestBookRepository.initData() 19 | eventRepository.initData() 20 | } 21 | } -------------------------------------------------------------------------------- /stream-service/README.md: -------------------------------------------------------------------------------- 1 | Stream Service 2 | ============= 3 | Stream API 4 | 5 | ##### Technology stack 6 | * Spring Boot 2.0.0 7 | * Spring WebFlux 8 | * Server-Sent Events (SSE) 9 | 10 | ##### Features 11 | * Server-Sent Events (SSE) 12 | * Functional Style Routes 13 | 14 | ### Run 15 | > use `./gradlew` instead of `gradle` if you didn't installed `gradle` 16 | ```bash 17 | gradle stream-service:bootRun 18 | ``` 19 | ### Test 20 | ```bash 21 | gradle stream-service:test 22 | ``` 23 | ### Build 24 | ```bash 25 | gradle stream-service:build 26 | # continuous build with `-t`. 27 | gradle -t stream-service:build 28 | # build docker image 29 | gradle stream-service:docker 30 | ``` 31 | 32 | ### API 33 | http://localhost:8082/sse/quotes 34 | 35 | 36 | http://localhost:8082/sse/fibonacci 37 | 38 | ### Test testing tool for socket.io 39 | 40 | http://amritb.github.io/socketio-client-tool/ 41 | 42 | Connect URL: http://localhost:8082/websocket/echo [TODO] -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/domain/Event.kt: -------------------------------------------------------------------------------- 1 | package com.example.domain 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.mongodb.core.mapping.Document 5 | import java.time.LocalDate 6 | 7 | @Document 8 | data class Event( 9 | @Id val id: String, 10 | val start: LocalDate, 11 | val end: LocalDate, 12 | val current: Boolean = false, 13 | val sponsors: List = emptyList() 14 | ) { 15 | val year: Int = start.year 16 | } 17 | 18 | @Document 19 | data class EventSponsoring( 20 | val level: SponsorshipLevel, 21 | val sponsorId: String, 22 | val subscriptionDate: LocalDate = LocalDate.now() 23 | ) 24 | 25 | enum class SponsorshipLevel { 26 | GOLD, 27 | SILVER, 28 | BRONZE, 29 | LANYARD, 30 | PARTY, 31 | BREAKFAST, 32 | LUNCH, 33 | HOSTING, 34 | VIDEO, 35 | COMMUNITY, 36 | NONE 37 | } 38 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/resources/data/guestbook.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "5924f47c6200de5220d870cc", 4 | "name": "sumo", 5 | "comment": "hellp world", 6 | "date": [ 7 | 2017, 8 | 5, 9 | 23, 10 | 19, 11 | 48, 12 | 28, 13 | 810000000 14 | ] 15 | }, 16 | { 17 | "id": "5924f4d06200de5220d870cd", 18 | "name": "demo", 19 | "comment": "test 123", 20 | "date": [ 21 | 2017, 22 | 5, 23 | 23, 24 | 19, 25 | 49, 26 | 52, 27 | 322000000 28 | ] 29 | }, 30 | { 31 | "id": "5924f4e36200de5220d870ce", 32 | "name": "demo1", 33 | "comment": "test 345", 34 | "date": [ 35 | 2017, 36 | 5, 37 | 23, 38 | 19, 39 | 50, 40 | 11, 41 | 722000000 42 | ] 43 | }, 44 | { 45 | "id": "5924f4f16200de5220d870cf", 46 | "name": "sumo1", 47 | "comment": "sumo 345", 48 | "date": [ 49 | 2017, 50 | 5, 51 | 23, 52 | 19, 53 | 50, 54 | 25, 55 | 589000000 56 | ] 57 | } 58 | ] -------------------------------------------------------------------------------- /mongo-data-service/src/test/kotlin/com/example/integration/AbstractIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.integration 2 | 3 | import org.junit.jupiter.api.BeforeAll 4 | import org.junit.jupiter.api.TestInstance 5 | import org.junit.jupiter.api.TestInstance.Lifecycle.* 6 | import org.junit.jupiter.api.extension.ExtendWith 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment 9 | import org.springframework.boot.web.server.LocalServerPort 10 | import org.springframework.test.context.junit.jupiter.SpringExtension 11 | import org.springframework.web.reactive.function.client.WebClient 12 | 13 | @ExtendWith(SpringExtension::class) 14 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 15 | @TestInstance(PER_CLASS) 16 | abstract class AbstractIntegrationTests { 17 | 18 | @LocalServerPort 19 | var port: Int? = null 20 | 21 | lateinit var client: WebClient 22 | 23 | @BeforeAll 24 | fun setup() { 25 | client = WebClient.create("http://localhost:$port") 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /stream-service/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.palantir.gradle.docker.DockerExtension 2 | import org.gradle.jvm.tasks.Jar 3 | 4 | apply { 5 | plugin("com.palantir.docker") 6 | } 7 | 8 | val jar: Jar by tasks 9 | docker { 10 | name = "${group}/${jar.baseName}:${jar.version}" 11 | files(jar.outputs) 12 | setDockerfile(file("src/main/docker/Dockerfile")) 13 | buildArgs(mapOf( 14 | "JAR_NAME" to jar.archiveName, 15 | "PORT" to "8080", 16 | "JAVA_OPTS" to "-Xms64m -Xmx128m" 17 | )) 18 | pull(true) 19 | dependsOn(tasks.findByName("build")) 20 | } 21 | 22 | dependencies { 23 | compile(project(":shared")) 24 | 25 | compile("com.fasterxml.jackson.module:jackson-module-kotlin") 26 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") 27 | } 28 | 29 | /** 30 | * Configures the [docker][DockerExtension] project extension. 31 | */ 32 | val Project.docker get() = extensions.getByName("docker") as DockerExtension 33 | 34 | fun Project.docker(configure: DockerExtension.() -> Unit): Unit = extensions.configure("docker", configure) 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=reactive 2 | description=Reactive Showcase Apps 3 | version=0.1.0-SNAPSHOT 4 | 5 | # kotlin 6 | kotlinVersion=1.1.4-3 7 | spekVersion=1.0.89 8 | mockitoVersion=2.1.0-beta.125 9 | mockitoKotlinVersion=0.6.1 10 | 11 | # spring 12 | springBootVersion=2.0.0.RELEASE 13 | springCloudVersion=Finchley.RELEASE 14 | springDependencyManagement=1.0.3.RELEASE 15 | testcontainersVersion=1.4.2 16 | 17 | # thymeleaf 18 | thymeleafVersion=3.0.6.RELEASE 19 | thymeleafTimeVersion=3.0.0.RELEASE 20 | thymeleafSecurityVersion=3.0.2.RELEASE 21 | 22 | # webjars 23 | webjarsLocatorVersion=0.32-1 24 | bootstrapVersion=3.3.7 25 | highchartsVersion=5.0.8 26 | xtermVersion=2.9.2 27 | 28 | # kafka 29 | kafkaVersion=0.11.0.0 30 | avroVersion=1.7.7 31 | reactorKafkaVersion=1.0.0.M3 32 | 33 | # Logback 34 | logbackKafkaAppenderVersion=0.1.0 35 | logstashLogbackEncoderVersion=4.11 36 | influxdbJavaVersion=2.8-SNAPSHOT 37 | 38 | # micrometer 39 | micrometerVersion=latest.release 40 | 41 | # gradle 42 | gradleWrapperVersion=4.2 43 | org.gradle.script.lang.kotlin.accessors.auto=true 44 | 45 | # docker 46 | dockerPluginVersion=0.13.0 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sumanth Chinthagunta 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 | -------------------------------------------------------------------------------- /mongo-data-service/README.md: -------------------------------------------------------------------------------- 1 | Mongo Data Service 2 | ================== 3 | MongoDB Data API 4 | 5 | ##### Technology stack 6 | * Spring Boot 2.0.0 7 | * Spring WebFlux 8 | * Embedded MongoDB 9 | * Reactive MongoDB Driver 10 | 11 | ##### Features 12 | * Functional Style Routes 13 | 14 | ### Run 15 | > use `./gradlew` instead of `gradle` if you didn't installed `gradle` 16 | ```bash 17 | gradle mongo-data-service:bootRun 18 | # run with `dev` profile. loads application-dev.properties 19 | SPRING_PROFILES_ACTIVE=dev gradle mongo-data-service:bootRun 20 | ``` 21 | ### Test 22 | ```bash 23 | gradle mongo-data-service:test 24 | ``` 25 | ### Build 26 | ```bash 27 | gradle mongo-data-service:build 28 | # skip test 29 | gradle mongo-data-service:build -x test 30 | # build docker image 31 | gradle mongo-data-service:docker -x test 32 | ``` 33 | 34 | ### API 35 | http://localhost:8081/api/events 36 | 37 | http://localhost:8081/api/events/mixit12 38 | 39 | http://localhost:8081/api/users 40 | 41 | http://localhost:8081/api/users/pamelafox 42 | 43 | http://localhost:8081/api/staff 44 | 45 | http://localhost:8081/api/staff/sdeleuze 46 | 47 | http://localhost:8081/api/guestbook 48 | 49 | ### EventSource API 50 | http://localhost:8081/sse/guestbook 51 | -------------------------------------------------------------------------------- /mongo-data-service/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.palantir.gradle.docker.DockerExtension 2 | import org.gradle.jvm.tasks.Jar 3 | 4 | apply { 5 | plugin("com.palantir.docker") 6 | } 7 | 8 | val jar: Jar by tasks 9 | docker { 10 | name = "${group}/${jar.baseName}:${jar.version}" 11 | files(jar.outputs) 12 | setDockerfile(file("src/main/docker/Dockerfile")) 13 | buildArgs(mapOf( 14 | "JAR_NAME" to jar.archiveName, 15 | "PORT" to "8080", 16 | "JAVA_OPTS" to "-Xms512m -Xmx1024m" 17 | )) 18 | pull(true) 19 | dependsOn(tasks.findByName("build")) 20 | } 21 | 22 | dependencies { 23 | compile(project(":shared")) 24 | 25 | compile("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") 26 | runtime("de.flapdoodle.embed:de.flapdoodle.embed.mongo") 27 | 28 | compile("com.fasterxml.jackson.module:jackson-module-kotlin") 29 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") 30 | } 31 | 32 | /** 33 | * Configures the [docker][DockerExtension] project extension. 34 | */ 35 | val Project.docker get() = extensions.getByName("docker") as DockerExtension 36 | 37 | fun Project.docker(configure: DockerExtension.() -> Unit): Unit = extensions.configure("docker", configure) 38 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/web/handler/GuestBookHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.web.handler 2 | 3 | import com.example.domain.GuestBookEntry 4 | import com.example.repository.GuestBookRepository 5 | import com.example.util.json 6 | import com.example.util.jsonStream 7 | import com.example.util.textStream 8 | import org.springframework.stereotype.Component 9 | import org.springframework.web.reactive.function.server.ServerRequest 10 | import org.springframework.web.reactive.function.server.ServerResponse.ok 11 | import org.springframework.web.reactive.function.server.body 12 | import org.springframework.web.reactive.function.server.bodyToMono 13 | import java.util.* 14 | 15 | @Component 16 | class GuestBookHandler(val repository: GuestBookRepository) { 17 | 18 | fun findOne(req: ServerRequest) = ok().json().body(repository.findOne(req.pathVariable("id"))) 19 | 20 | fun findAll(req: ServerRequest) = ok().json().body(repository.findAll()) 21 | 22 | fun create(req: ServerRequest) = ok().json().body(repository.save(req.bodyToMono())) 23 | 24 | fun fetchSSE(req: ServerRequest) = ok().textStream() 25 | .body(repository.tailByTimestampGreaterThan((GregorianCalendar.getInstance().time))) 26 | 27 | fun fetch(req: ServerRequest) = ok().jsonStream() 28 | .body(repository.tailByTimestampGreaterThan((GregorianCalendar.getInstance().time))) 29 | } -------------------------------------------------------------------------------- /mongo-data-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: ${PORT:8081} 3 | #logging: 4 | # level: 5 | # root: debug 6 | # org.springframework.web.reactive.function: INFO 7 | # org.springframework.data.mongodb.core.ReactiveMongoTemplate: DEBUG 8 | # org.springframework.data.mongodb.core.query.Query: DEBUG 9 | endpoints: 10 | default: 11 | web: 12 | enabled: true 13 | spring: 14 | application: 15 | name: mongo-data-service 16 | data: 17 | mongodb: 18 | repositories: 19 | enabled: false 20 | 21 | --- 22 | spring: 23 | profiles: docker 24 | data: 25 | mongodb: 26 | host: ${MONGO_HOST:mongodb} 27 | port: ${MONGO_PORT:27017} 28 | username: ${MONGO_USERNAME:admin} 29 | password: ${MONGO_PASSWORD:admin} 30 | authentication-database: ${MONGO_AUTHENTICATION_DATABASE:admin} 31 | grid-fs-database: images 32 | autoconfigure: 33 | exclude: org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration 34 | 35 | --- 36 | spring: 37 | profiles: cloud 38 | data: 39 | mongodb: 40 | host: ${MONGO_HOST:mongodb} 41 | port: ${MONGO_PORT:27017} 42 | username: ${MONGO_USERNAME:admin} 43 | password: ${MONGO_PASSWORD:admin} 44 | authentication-database: ${MONGO_AUTHENTICATION_DATABASE:admin} 45 | grid-fs-database: images 46 | autoconfigure: 47 | exclude: org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration 48 | -------------------------------------------------------------------------------- /ui-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.palantir.gradle.docker.DockerExtension 2 | import org.gradle.jvm.tasks.Jar 3 | 4 | val bootstrapVersion by project 5 | val highchartsVersion by project 6 | val webjarsLocatorVersion by project 7 | 8 | apply { 9 | plugin("com.palantir.docker") 10 | } 11 | 12 | val jar: Jar by tasks 13 | docker { 14 | name = "${group}/${jar.baseName}:${jar.version}" 15 | files(jar.outputs) // jar.outputs , file("src/main/docker/.ssl/truststore.jks") 16 | setDockerfile(file("src/main/docker/Dockerfile")) 17 | buildArgs(mapOf( 18 | "JAR_NAME" to jar.archiveName, 19 | "PORT" to "8080", 20 | "JAVA_OPTS" to "-Xms64m -Xmx128m" 21 | )) 22 | pull(true) 23 | dependsOn(tasks.findByName("build")) 24 | } 25 | 26 | dependencies { 27 | compile(project(":shared")) 28 | 29 | compile("org.springframework.boot:spring-boot-starter-thymeleaf") 30 | // compile("org.thymeleaf.extras:thymeleaf-extras-java8time") 31 | runtime("org.webjars:webjars-locator:$webjarsLocatorVersion") 32 | runtime("org.webjars:bootstrap:$bootstrapVersion") 33 | runtime("org.webjars:highcharts:$highchartsVersion") 34 | 35 | compile("com.fasterxml.jackson.module:jackson-module-kotlin") 36 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") 37 | } 38 | 39 | /** 40 | * Configures the [docker][DockerExtension] project extension. 41 | */ 42 | val Project.docker get() = extensions.getByName("docker") as DockerExtension 43 | 44 | fun Project.docker(configure: DockerExtension.() -> Unit): Unit = extensions.configure("docker", configure) 45 | -------------------------------------------------------------------------------- /mongo-data-service/src/test/kotlin/com/example/integration/EventRepositoryTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.integration 2 | 3 | import com.example.repository.EventRepository 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.data.domain.PageRequest 8 | import org.springframework.data.domain.Sort 9 | import org.springframework.data.domain.Sort.Direction.ASC 10 | import org.springframework.data.domain.Sort.Order 11 | import reactor.test.test 12 | import java.time.Duration 13 | 14 | 15 | class EventRepositoryTests : AbstractIntegrationTests() { 16 | 17 | @Autowired lateinit var eventRepository: EventRepository 18 | 19 | @Test 20 | fun `Count users correctly`() { 21 | val eventCount = eventRepository.count().block(Duration.ofSeconds(2)) 22 | assertEquals(eventCount, 6L) 23 | } 24 | 25 | @Test 26 | fun `Sort users correctly`() { 27 | val events = eventRepository.findAll(Sort.by(Order(ASC, "current"))) 28 | events 29 | .test() 30 | .expectNextCount(6L) 31 | .verifyComplete() 32 | } 33 | 34 | @Test 35 | fun `Filter and page users correctly`() { 36 | val pagedEvents = eventRepository.findByCurrentOrderByStart(true, PageRequest.of(0, 5) ); 37 | pagedEvents 38 | .test() 39 | .consumeNextWith { actual -> 40 | assertEquals(actual.current,true) 41 | println(actual) 42 | } 43 | .verifyComplete() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docker-compose-all.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mongodb: 4 | image: mongo:latest 5 | ports: 6 | - 27017:27017 7 | environment: 8 | - MONGO_DATA_DIR=/data/db 9 | - MONGO_LOG_DIR=/dev/null 10 | - MONGO_INITDB_ROOT_USERNAME=admin 11 | - MONGO_INITDB_ROOT_PASSWORD=admin 12 | - MONGO_INITDB_DATABASE=test 13 | volumes: 14 | - ./infra/mongodb/data:/data/db 15 | networks: 16 | - reactive-network 17 | command: mongod --smallfiles 18 | # command: mongod --smallfiles --logpath=/dev/null # --quiet 19 | 20 | mongo: 21 | image: reactive/mongo-data-service:0.1.0-SNAPSHOT 22 | environment: 23 | - SPRING_PROFILES_ACTIVE=docker 24 | - MONGO_HOST=mongodb 25 | - MONGO_PORT=27017 26 | - MONGO_USERNAME=admin 27 | - MONGO_PASSWORD=admin 28 | - MONGO_AUTHENTICATION_DATABASE=admin 29 | networks: 30 | - reactive-network 31 | links: 32 | - mongodb 33 | depends_on: 34 | - mongodb 35 | 36 | stream: 37 | image: reactive/stream-service:0.1.0-SNAPSHOT 38 | environment: 39 | - SPRING_PROFILES_ACTIVE=docker 40 | networks: 41 | - reactive-network 42 | 43 | app: 44 | image: reactive/ui-app:0.1.0-SNAPSHOT 45 | ports: 46 | - 8080:8080 47 | environment: 48 | - SPRING_PROFILES_ACTIVE=docker 49 | - MONGO_API_URL=http://mongo:8080 50 | - STREAM_API_URL=http://stream:8080 51 | networks: 52 | - reactive-network 53 | links: 54 | - mongo 55 | - stream 56 | depends_on: 57 | - mongo 58 | - stream 59 | 60 | networks: 61 | reactive-network: 62 | driver: bridge 63 | -------------------------------------------------------------------------------- /mongo-data-service/src/test/kotlin/com/example/integration/EventIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.integration 2 | 3 | import com.example.domain.Event 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.http.MediaType.APPLICATION_JSON 7 | import org.springframework.web.reactive.function.client.bodyToFlux 8 | import org.springframework.web.reactive.function.client.bodyToMono 9 | import reactor.test.test 10 | 11 | class EventIntegrationTests : AbstractIntegrationTests() { 12 | 13 | @Test 14 | fun `Find MiXiT 2016 event`() { 15 | client.get().uri("/api/events/mixit16").accept(APPLICATION_JSON) 16 | .retrieve() 17 | .bodyToMono() 18 | .test() 19 | .consumeNextWith { 20 | assertEquals(2016, it.year) 21 | assertFalse(it.current) 22 | } 23 | .verifyComplete() 24 | } 25 | 26 | @Test 27 | fun `Find all events`() { 28 | client.get().uri("/api/events/").accept(APPLICATION_JSON) 29 | .retrieve() 30 | .bodyToFlux() 31 | .test() 32 | .consumeNextWith { assertEquals(2012, it.year) } 33 | .consumeNextWith { assertEquals(2013, it.year) } 34 | .consumeNextWith { assertEquals(2014, it.year) } 35 | .consumeNextWith { assertEquals(2015, it.year) } 36 | .consumeNextWith { assertEquals(2016, it.year) } 37 | .consumeNextWith { assertEquals(2017, it.year) } 38 | .verifyComplete() 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/repository/EventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.repository 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.readValue 5 | import com.example.domain.* 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.core.io.ClassPathResource 8 | import org.springframework.data.domain.Pageable 9 | import org.springframework.data.domain.Sort 10 | import org.springframework.data.mongodb.core.* 11 | import org.springframework.data.mongodb.core.query.Query 12 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository 13 | import org.springframework.data.mongodb.repository.Tailable 14 | import org.springframework.stereotype.Repository 15 | import reactor.core.publisher.Flux 16 | import reactor.core.publisher.Mono 17 | 18 | @Repository 19 | interface EventRepository : ReactiveMongoRepository { 20 | @Tailable fun findBy(): Flux 21 | fun findByCurrentOrderByStart(current : Boolean, pageable: Pageable) : Flux 22 | } 23 | 24 | @Repository 25 | class EventRepository1(val template: ReactiveMongoTemplate, 26 | val objectMapper: ObjectMapper) { 27 | 28 | private val logger = LoggerFactory.getLogger(this.javaClass) 29 | 30 | fun initData() { 31 | if (count().block() == 0L) { 32 | val eventsResource = ClassPathResource("data/events.json") 33 | val events: List = objectMapper.readValue(eventsResource.inputStream) 34 | events.forEach { save(it).block() } 35 | logger.info("Events data initialization complete") 36 | } 37 | } 38 | 39 | fun count() = template.count() 40 | fun save(event: Event) = template.save(event) 41 | } -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/web/routes/ApiRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.example.web.routes 2 | 3 | import com.example.web.handler.* 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.context.annotation.DependsOn 7 | import org.springframework.http.MediaType.* 8 | import org.springframework.web.reactive.function.BodyInserters 9 | import org.springframework.web.reactive.function.server.ServerResponse 10 | import org.springframework.web.reactive.function.server.router 11 | 12 | @Configuration 13 | class ApiRoutes(val eventHandler: EventHandler, 14 | val guestBookHandler: GuestBookHandler, 15 | val userHandler: UserHandler) { 16 | @Bean 17 | fun apiRouter() = router { 18 | (accept(APPLICATION_JSON) and "/api").nest { 19 | 20 | "/events".nest { 21 | GET("/", eventHandler::findAll) 22 | GET("/{id}", eventHandler::findOne) 23 | } 24 | 25 | // users 26 | "/users".nest { 27 | GET("/", userHandler::findAll) 28 | POST("/", userHandler::create) 29 | GET("/{login}", userHandler::findOne) 30 | } 31 | "/staff".nest { 32 | GET("/", userHandler::findAllStaff) 33 | GET("/{login}", userHandler::findOneStaff) 34 | } 35 | 36 | // guest book 37 | "/guestbook".nest { 38 | GET("/", guestBookHandler::findAll) 39 | POST("/", guestBookHandler::create) 40 | GET("/{id}", guestBookHandler::findOne) 41 | } 42 | 43 | } 44 | 45 | GET("/sse/guestbook").nest { 46 | accept(TEXT_EVENT_STREAM, guestBookHandler::fetchSSE) 47 | accept(APPLICATION_STREAM_JSON, guestBookHandler::fetch) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui-app/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Streaming 10 | 11 | 12 | 13 | 14 | 28 |
29 |

Staff

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
LoginNameEmailCompany
sumoJane Doedumo@demo.comXYZ
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.repository 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.readValue 5 | import com.example.domain.Role 6 | import com.example.domain.User 7 | import org.springframework.core.io.ClassPathResource 8 | import org.springframework.data.mongodb.core.* 9 | import org.springframework.data.mongodb.core.query.Query 10 | import org.springframework.stereotype.Repository 11 | import reactor.core.publisher.Mono 12 | import org.slf4j.LoggerFactory 13 | import org.springframework.data.mongodb.core.query.Criteria.* 14 | 15 | 16 | @Repository 17 | class UserRepository(val template: ReactiveMongoTemplate, 18 | val objectMapper: ObjectMapper) { 19 | 20 | private val logger = LoggerFactory.getLogger(this.javaClass) 21 | 22 | fun initData() { 23 | if (count().block() == 0L) { 24 | val usersResource = ClassPathResource("data/users.json") 25 | val users: List = objectMapper.readValue(usersResource.inputStream) 26 | users.forEach { save(it).block() } 27 | logger.info("Users data initialization complete") 28 | } 29 | } 30 | 31 | fun count() = template.count() 32 | 33 | fun findByYear(year: Int) = 34 | template.find(Query(where("year").`is`(year))) 35 | 36 | 37 | fun findByRole(role: Role) = 38 | template.find(Query(where("role").`is`(role))) 39 | 40 | 41 | fun findByRoleAndEvent(role: Role, event: String) = 42 | template.find(Query(where("role").`is`(role).and("events").`in`(event))) 43 | 44 | 45 | fun findOneByRole(login: String, role: Role) = 46 | template.findOne(Query(where("role").`in`(role).and("_id").`is`(login))) 47 | 48 | 49 | fun findAll() = template.findAll() 50 | 51 | fun findOne(login: String) = template.findById(login) 52 | 53 | fun findMany(logins: List) = template.find(Query(where("_id").`in`(logins))) 54 | 55 | fun findByLegacyId(id: Long) = 56 | template.findOne(Query(where("legacyId").`is`(id))) 57 | 58 | fun deleteAll() = template.remove(Query()) 59 | 60 | fun deleteOne(login: String) = template.remove(Query(where("_id").`is`(login))) 61 | 62 | fun save(user: User) = template.save(user) 63 | 64 | fun save(user: Mono) = template.save(user) 65 | 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/repository/GuestBookRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.repository 2 | 3 | import com.example.domain.GuestBookEntry 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.readValue 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.core.io.ClassPathResource 8 | import org.springframework.data.domain.Sort 9 | import org.springframework.data.mongodb.core.* 10 | import org.springframework.data.mongodb.core.CollectionOptions 11 | import org.springframework.data.mongodb.core.query.Criteria.* 12 | import org.springframework.data.mongodb.core.query.Query 13 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository 14 | import org.springframework.data.mongodb.repository.Tailable 15 | import org.springframework.stereotype.Repository 16 | import reactor.core.publisher.Flux 17 | import reactor.core.publisher.Mono 18 | import java.util.* 19 | 20 | @Repository 21 | class GuestBookRepository(val template: ReactiveMongoTemplate, 22 | val objectMapper: ObjectMapper) { 23 | 24 | private val logger = LoggerFactory.getLogger(this.javaClass) 25 | 26 | fun initData() { 27 | if (!template.collectionExists(GuestBookEntry::class.java).block()!!) { 28 | // Create collection with capped = true 29 | template.createCollection(GuestBookEntry::class.java, CollectionOptions.empty().maxDocuments(100L).size(1_048_576L).capped()).block() 30 | 31 | // Initialize Data 32 | val guestBookResource = ClassPathResource("data/guestbook.json") 33 | val guestBookEntries: List = objectMapper.readValue(guestBookResource.inputStream) 34 | guestBookEntries.forEach { save(it).block() } 35 | logger.info("GuestBookEntry data initialization complete") 36 | } 37 | } 38 | 39 | fun count() = template.count() 40 | 41 | fun findAll() = template.find(Query().with(Sort.by("year"))) 42 | fun tailByTimestampGreaterThan(timestamp: Date) = template.tail(Query(where("date").gte(timestamp))) 43 | 44 | fun findOne(id: String) = template.findById(id) 45 | 46 | fun deleteAll() = template.remove(Query()) 47 | fun deleteOne(id: String) = template.remove(Query(where("_id").`is`(id))) 48 | 49 | fun save(entry: GuestBookEntry) = template.save(entry) 50 | fun save(entry: Mono) = template.save(entry) 51 | } 52 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | Docker 2 | ====== 3 | Docker Cheat Sheet. 4 | 5 | ### Install 6 | Install `Docker for Mac` app [Installation](https://docs.docker.com/docker-for-mac/install/) 7 | 8 | ### Docker Commands 9 | 10 | ```bash 11 | # To see list of images 12 | docker images 13 | docker images -a 14 | # To delete an image 15 | docker rmi eb46b3df6e36 16 | docker rmi eb46b3df6e36 -f 17 | # To run an image 18 | docker run -p 8081:8081 -i -t reactive/mongo-data-service:0.1.0-SNAPSHOT 19 | docker run -p 8082:8082 --name stream-service -it reactive/stream-service:0.1.0-SNAPSHOT 20 | docker start -p 8082:8082 -it reactive/stream-service:0.1.0-SNAPSHOT 21 | docker run -p 8080:8080 -e "app.mongoApiUrl=http://localhost:8081" -e "app.streamApiUrl=http://localhost:8082" -i -t reactive/ui-app:0.1.0-SNAPSHOT 22 | # Using Spring Profiles 23 | $ docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 8082:8082 -i -t reactive/stream-service:0.1.0-SNAPSHOT 24 | # Debugging the application in a Docker container 25 | $ docker run -e "JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n" -p 8082:8082 -p 5005:5005 -i -t reactive/stream-service:0.1.0-SNAPSHOT 26 | # To see list of running containers 27 | docker ps 28 | # To stop a running container 29 | docker stop 81c723d22865 30 | # SSH to the running container (CONTAINER ID from `docker ps` command) 31 | docker exec -i sh 32 | # To check logs 33 | docker logs stream-service 34 | # inspect a docker image 35 | docker inspect confluentinc/ksql-cli 36 | ``` 37 | 38 | ##### Docker Compose 39 | ```bash 40 | # start containers in the background 41 | docker-compose up -d 42 | # start containers in the foreground 43 | docker-compose up 44 | # show runnning containers 45 | docker-compose ps 46 | # scaling containers and load balancing 47 | docker-compose scale stream=3 48 | # 1. stop the running containers using 49 | docker-compose stop 50 | # 2. remove the stopped containers using 51 | docker-compose rm -f 52 | # connect(ssh) to a service and run a command 53 | docker-compose exec -it mongo mongod 54 | # see logs of a service 55 | docker-compose logs -f mongo 56 | # start specific docker-compose file 57 | docker-compose -f docker-compose-all.yml up 58 | # just start only a single service 59 | docker-compose -f docker-compose-all.yml up stream 60 | # restart single service 61 | docker-compose -f docker-compose-all.yml restart stream 62 | ``` 63 | 64 | ### Maintenance 65 | ```bash 66 | docker container prune 67 | docker image prune 68 | docker network prune 69 | docker volume prune 70 | # will delete ALL unused data (i.e. In order: containers stopped, volumes without containers and images with no containers). 71 | docker system prune 72 | ``` 73 | -------------------------------------------------------------------------------- /mongo-data-service/src/test/kotlin/com/example/integration/UserIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.integration 2 | 3 | import com.example.domain.Role 4 | import com.example.domain.User 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.http.MediaType.APPLICATION_JSON 8 | import org.springframework.web.reactive.function.client.bodyToFlux 9 | import org.springframework.web.reactive.function.client.bodyToMono 10 | import reactor.test.test 11 | 12 | class UserIntegrationTests : AbstractIntegrationTests() { 13 | 14 | @Test 15 | fun `Create a new user`() { 16 | client.post().uri("/api/users/").accept(APPLICATION_JSON).contentType(APPLICATION_JSON) 17 | .syncBody(User("brian", "Brian", "Clozel", "bc@gm.com")) 18 | .retrieve() 19 | .bodyToMono() 20 | .test() 21 | .consumeNextWith { assertEquals("brian", it.login) } 22 | .verifyComplete() 23 | } 24 | 25 | @Test 26 | fun `Find Dan North`() { 27 | client.get().uri("/api/users/tastapod").accept(APPLICATION_JSON) 28 | .retrieve() 29 | .bodyToMono() 30 | .test() 31 | .consumeNextWith { 32 | assertEquals("North", it.lastname) 33 | assertEquals("Dan", it.firstname) 34 | assertTrue(it.role == Role.USER) 35 | } 36 | .verifyComplete() 37 | } 38 | 39 | @Test 40 | fun `Find Guillaume Ehret staff member`() { 41 | client.get().uri("/api/staff/guillaumeehret").accept(APPLICATION_JSON) 42 | .retrieve() 43 | .bodyToFlux() 44 | .test() 45 | .consumeNextWith { 46 | assertEquals("Ehret", it.lastname) 47 | assertEquals("Guillaume", it.firstname) 48 | assertTrue(it.role == Role.STAFF) 49 | } 50 | .verifyComplete() 51 | } 52 | 53 | @Test 54 | fun `Find all staff members`() { 55 | client.get().uri("/api/staff/").accept(APPLICATION_JSON) 56 | .retrieve() 57 | .bodyToFlux() 58 | .test() 59 | .expectNextCount(15) 60 | .verifyComplete() 61 | } 62 | 63 | @Test 64 | fun `Find Zenika Lyon`() { 65 | client.get().uri("/api/users/Zenika Lyon").accept(APPLICATION_JSON) 66 | .retrieve() 67 | .bodyToFlux() 68 | .test() 69 | .consumeNextWith { 70 | assertEquals("Jacob", it.lastname) 71 | assertEquals("Hervé", it.firstname) 72 | assertTrue(it.role == Role.USER) 73 | } 74 | .verifyComplete() 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/web/handler/UserHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.web.handler 2 | 3 | import com.example.domain.* 4 | import com.example.repository.UserRepository 5 | import com.example.util.* 6 | import org.springframework.stereotype.Component 7 | import org.springframework.web.reactive.function.server.* 8 | import org.springframework.web.reactive.function.server.ServerResponse.* 9 | import reactor.core.publisher.toMono 10 | import java.net.URI.* 11 | import java.net.URLDecoder 12 | 13 | 14 | @Component 15 | class UserHandler(val repository: UserRepository) { 16 | 17 | fun findOneView(req: ServerRequest) = 18 | try { 19 | val idLegacy = req.pathVariable("login").toLong() 20 | repository.findByLegacyId(idLegacy).flatMap { 21 | ok().render("user", mapOf(Pair("user", it.toDto(req.language())))) 22 | } 23 | } catch (e:NumberFormatException) { 24 | repository.findOne(URLDecoder.decode(req.pathVariable("login"), "UTF-8")).flatMap { 25 | ok().render("user", mapOf(Pair("user", it.toDto(req.language())))) 26 | } 27 | } 28 | 29 | fun findAll(req: ServerRequest) = ok().json().body(repository.findAll()) 30 | fun findOne(req: ServerRequest) = ok().json().body(repository.findOne(req.pathVariable("login"))) 31 | 32 | fun findAllStaff(req: ServerRequest) = ok().json().body(repository.findByRole(Role.STAFF)) 33 | fun findOneStaff(req: ServerRequest) = ok().json().body(repository.findOneByRole(req.pathVariable("login"), Role.STAFF)) 34 | 35 | fun create(req: ServerRequest) = repository.save(req.bodyToMono()).flatMap { 36 | created(create("/api/user/${it.login}")).json().body(it.toMono()) 37 | } 38 | 39 | } 40 | 41 | class UserDto( 42 | val login: String, 43 | val firstname: String, 44 | val lastname: String, 45 | var email: String, 46 | var company: String? = null, 47 | var description: String, 48 | var emailHash: String? = null, 49 | var photoUrl: String? = null, 50 | val role: Role, 51 | var links: List, 52 | val logoType: String?, 53 | val logoWebpUrl: String? = null 54 | ) 55 | 56 | fun User.toDto(language: Language) = 57 | UserDto(login, firstname, lastname, email ?: "", company, description[language] ?: "", 58 | emailHash, photoUrl, role, links, logoType(photoUrl), logoWebpUrl(photoUrl)) 59 | 60 | private fun logoWebpUrl(url: String?) = 61 | when { 62 | url == null -> null 63 | url.endsWith("png") -> url.replace("png", "webp") 64 | url.endsWith("jpg") -> url.replace("jpg", "webp") 65 | else -> null 66 | } 67 | 68 | private fun logoType(url: String?) = 69 | when { 70 | url == null -> null 71 | url.endsWith("svg") -> "image/svg+xml" 72 | url.endsWith("png") -> "image/png" 73 | url.endsWith("jpg") -> "image/jpeg" 74 | else -> null 75 | } 76 | -------------------------------------------------------------------------------- /ui-app/src/main/resources/templates/quotes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Streaming 10 | 11 | 12 | 13 | 14 | 15 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reactive Apps 2 | ============= 3 | A simple demo application showcases end-to-end `Functional Reactive Programming (FRP)` with Spring 5. 4 | 5 | ![Reactive](./docs/reactive-apps.png "Reactive App") 6 | 7 | ##### Technology stack 8 | * Spring Framework 5 9 | * Spring Boot 2.0.0 10 | * Spring WebFlux 11 | * Embedded MongoDB 12 | * Reactive MongoDB Driver 13 | * Gradle 4 14 | 15 | ##### Highlights 16 | * Use of Server-Sent Events (SSE) rendered in HTML by Thymeleaf from a reactive data stream. 17 | * Use of Server-Sent Events (SSE) rendered in JSON by Spring WebFlux from a reactive data stream. 18 | * Use of Spring Data MongoDB's reactive (Reactive Streams) driver support. 19 | * Use of Spring Data MongoDB's support for infinite reactive data streams based on MongoDB tailable cursor (see [here](https://docs.mongodb.com/manual/core/tailable-cursors/)). 20 | * Use of Thymeleaf's fully-HTML5-compatible syntax. 21 | * Use of `webjars` for client-side dependency managements. 22 | * Reactive Netty as a server 23 | * Multi-project builds with Gradle Kotlin Script. 24 | * Kotlin as a language 25 | * Cross-Origin Resource Sharing (CORS) 26 | * Docker deployment 27 | 28 | 29 | ### Prerequisites 30 | 1. Gradle 4 (Install via [sdkman](http://sdkman.io/)) 31 | 2. Docker for Mac [Setup Instructions](./docs/Docker.md) 32 | 33 | ### Build 34 | ```bash 35 | # build all 3 executable jars 36 | gradle build 37 | # continuous build with `-t`. 38 | # this shoud be started before any run tasks i.e., `gradle ui-app:bootRun`, for spring's devtools to work. 39 | gradle -t build 40 | # build all 3 apps 41 | gradle build -x test -x shared:build 42 | # build all 3 docker images 43 | gradle docker -x test -x shared:build 44 | ``` 45 | 46 | ### Test 47 | ```bash 48 | gradle test 49 | ``` 50 | 51 | ### Run 52 | ##### Manual 53 | Start all 3 apps: [mongo-data-service](./mongo-data-service/), [stream-service](./stream-service/), [ui-app](./ui-app/) 54 | > If you want to debug the app, add --debug-jvm parameter to Gradle command line 55 | 56 | ##### Docker 57 | You can also build Docker images and run all via `Docker Compose` 58 | ```bash 59 | # start containers in the background 60 | docker-compose up -d 61 | # start containers in the foreground 62 | docker-compose up 63 | # show runnning containers 64 | docker-compose ps 65 | # scaling containers and load balancing 66 | docker-compose scale stream=2 67 | # 1. stop the running containers using 68 | docker-compose stop 69 | # 2. remove the stopped containers using 70 | docker-compose rm -f 71 | # start specific docker-compose file 72 | docker-compose -f docker-compose-all.yml up 73 | # see logs of a service 74 | docker-compose -f docker-compose-all.yml logs mongodb 75 | # connect(ssh) to a service and run a command 76 | docker-compose -f docker-compose-all.yml exec mongodb mongo -u "admin" -p "admin" --authenticationDatabase "admin" 77 | # restart single service 78 | docker-compose -f docker-compose-all.yml restart mongodb 79 | # start single service 80 | docker-compose -f docker-compose-all.yml up mongodb 81 | # check health for a service 82 | docker inspect --format "{{json .State.Health.Status }}" reactiveapps_app_1 83 | docker ps 84 | ``` 85 | >Access UI App at http://localhost:8080 86 | 87 | 88 | ### Gradle Commands 89 | ```bash 90 | # upgrade project gradle version 91 | gradle wrapper --gradle-version 4.2-rc-2 --distribution-type all 92 | # gradle daemon status 93 | gradle --status 94 | gradle --stop 95 | ``` 96 | 97 | ### Credits 98 | * [MiXiT](https://github.com/mixitconf/mixit) 99 | * [Stéphane Nicoll](https://github.com/snicoll-demos/demo-webflux-streaming) 100 | * [Daniel Fernández](https://github.com/danielfernandez/reactive-matchday) 101 | * [Stathis Souris](https://ssouris.github.io/2017/06/02/petclinic-spring-5-kotlin-reactive-mongodb.html) 102 | * [Sébastien Deleuze](https://github.com/sdeleuze/spring-kotlin-functional) 103 | 104 | ### TODO 105 | * [spring-kotlin-functional](https://github.com/sdeleuze/spring-kotlin-functional) 106 | * [service-blocks](https://github.com/kbastani/service-block-samples) 107 | -------------------------------------------------------------------------------- /ui-app/src/main/kotlin/com/example/UiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.hibernate.validator.constraints.NotBlank 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.boot.SpringApplication 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.http.MediaType.* 8 | import org.springframework.stereotype.Controller 9 | import org.springframework.ui.Model 10 | import org.springframework.validation.BindingResult 11 | import org.springframework.web.bind.annotation.GetMapping 12 | import org.springframework.web.bind.annotation.PostMapping 13 | import org.springframework.web.bind.annotation.ResponseBody 14 | import org.springframework.web.reactive.function.client.WebClient 15 | import org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable 16 | import reactor.core.publisher.Flux 17 | import reactor.core.publisher.Mono 18 | import javax.validation.Valid 19 | 20 | 21 | @SpringBootApplication 22 | class UiApplication 23 | 24 | @Controller 25 | class MainController(@Value("\${app.mongoApiUrl}") val mongoApiUrl: String, 26 | @Value("\${app.streamApiUrl}") val streamApiUrl: String) { 27 | 28 | @GetMapping("/") 29 | fun home(model: Model): String { 30 | val userList = WebClient.create(mongoApiUrl) 31 | .get() 32 | .uri("api/staff") 33 | .accept(APPLICATION_JSON) 34 | .retrieve() 35 | .bodyToFlux(User::class.java) 36 | .log() 37 | 38 | // model.addAttribute("users", userList.collectList().block()); 39 | model.addAttribute("users", ReactiveDataDriverContextVariable(userList, 1)) 40 | return "index"; 41 | } 42 | 43 | @GetMapping("/quotes") 44 | fun quotes(): String { 45 | return "quotes" 46 | } 47 | 48 | @GetMapping(path = arrayOf("/quotes/feed"), produces = arrayOf(TEXT_EVENT_STREAM_VALUE)) 49 | @ResponseBody 50 | fun fetchQuotesStream(): Flux { 51 | return WebClient.create(streamApiUrl) 52 | .get() 53 | .uri("/sse/quotes") 54 | .accept(APPLICATION_STREAM_JSON) 55 | .retrieve() 56 | .bodyToFlux(Quote::class.java) 57 | .share() 58 | .log() 59 | } 60 | 61 | @GetMapping("/guestbook") 62 | fun guestBook(model: Model): String { 63 | val entryList = WebClient.create(mongoApiUrl) 64 | .get() 65 | .uri("api/guestbook") 66 | .accept(APPLICATION_JSON) 67 | .retrieve() 68 | .bodyToFlux(GuestBookEntryDTO::class.java) 69 | .log() 70 | 71 | model.addAttribute("entries", ReactiveDataDriverContextVariable(entryList, 1)) // buffers size = 1 72 | return "guestbook" 73 | } 74 | 75 | @PostMapping("/guestbook") 76 | @ResponseBody 77 | fun postGuestBook(@Valid guestBookEntryVO: GuestBookEntryVO, result: BindingResult): Mono { 78 | if(result.hasErrors()) { 79 | println(result.allErrors) 80 | throw Error("allErrors") 81 | } 82 | return WebClient.create(mongoApiUrl) 83 | .post() 84 | .uri("api/guestbook") 85 | .accept(APPLICATION_JSON) 86 | .syncBody(guestBookEntryVO) 87 | .exchange() 88 | .flatMap{response -> response.bodyToMono(GuestBookEntryDTO::class.java)} 89 | .log() 90 | } 91 | 92 | @GetMapping(path = arrayOf("/guestbook/feed"), produces = arrayOf(TEXT_EVENT_STREAM_VALUE)) 93 | @ResponseBody 94 | fun guestBookStream(): Flux { 95 | return WebClient.create(mongoApiUrl) 96 | .get() 97 | .uri("/sse/guestbook") 98 | .accept(APPLICATION_STREAM_JSON) 99 | .retrieve() 100 | .bodyToFlux(GuestBookEntryDTO::class.java) 101 | .share() 102 | .log() 103 | } 104 | 105 | @GetMapping(path = arrayOf("/guestbook/feed_html"), produces = arrayOf(TEXT_EVENT_STREAM_VALUE)) 106 | fun guestBookHtmlStream(model: Model): String { 107 | val guestBookStream = WebClient.create(mongoApiUrl) 108 | .get() 109 | .uri("/sse/guestbook") 110 | .accept(APPLICATION_STREAM_JSON) 111 | .retrieve() 112 | .bodyToFlux(GuestBookEntryDTO::class.java) 113 | .share() 114 | .log() 115 | 116 | model.addAttribute("entries", ReactiveDataDriverContextVariable(guestBookStream, 1)) // buffers size = 1 117 | // Will use the same "guestbook" template, but only a fragment: the `entries` block. 118 | return "guestbook :: #entries" 119 | } 120 | 121 | } 122 | 123 | 124 | data class User( 125 | val login: String, 126 | val firstname: String, 127 | val lastname: String, 128 | val email: String?, 129 | val company: String? = null, 130 | val emailHash: String? = null, 131 | val photoUrl: String? = null 132 | ) 133 | data class GuestBookEntryVO( 134 | val name: String, 135 | @NotBlank(message = "comment can't be empty!") 136 | val comment: String 137 | ) 138 | 139 | fun main(args: Array) { 140 | SpringApplication.run(UiApplication::class.java, *args) 141 | } 142 | 143 | 144 | //@Bean 145 | //open fun templateEngine(): SpringTemplateEngine { 146 | // val templateEngine = SpringTemplateEngine() 147 | // templateEngine.setTemplateResolver(templateResolver()) 148 | // templateEngine.addDialect(SpringSecurityDialect()) 149 | // templateEngine.addDialect(Java8TimeDialect()) 150 | // return templateEngine 151 | //} -------------------------------------------------------------------------------- /mongo-data-service/src/main/kotlin/com/example/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.util 2 | 3 | import com.example.domain.Language 4 | import org.springframework.boot.SpringApplication 5 | import org.springframework.http.MediaType.* 6 | import org.springframework.web.reactive.function.server.ServerRequest 7 | import org.springframework.web.reactive.function.server.ServerResponse 8 | import org.springframework.web.reactive.function.server.ServerResponse.* 9 | import java.net.URI 10 | import java.text.Normalizer 11 | import java.time.LocalDate 12 | import java.time.LocalDateTime 13 | import java.time.ZoneOffset 14 | import java.time.ZonedDateTime 15 | import java.time.format.DateTimeFormatter 16 | import java.time.format.DateTimeFormatterBuilder 17 | import java.time.temporal.ChronoField 18 | import java.util.* 19 | import java.util.stream.Collectors 20 | import java.util.stream.IntStream 21 | import kotlin.reflect.KClass 22 | 23 | // ---------------------- 24 | // Spring Boot extensions 25 | // ---------------------- 26 | 27 | fun run(type: KClass<*>, vararg args: String) = SpringApplication.run(type.java, *args) 28 | 29 | // ------------------------- 30 | // Spring WebFlux extensions 31 | // ------------------------- 32 | 33 | fun ServerRequest.language() = 34 | Language.findByTag(this.headers().asHttpHeaders().acceptLanguageAsLocales.first().language) 35 | 36 | fun ServerRequest.locale() = 37 | this.headers().asHttpHeaders().acceptLanguageAsLocales.first() ?: Locale.ENGLISH 38 | 39 | fun ServerResponse.BodyBuilder.json() = contentType(APPLICATION_JSON_UTF8) 40 | 41 | fun ServerResponse.BodyBuilder.xml() = contentType(APPLICATION_XML) 42 | 43 | fun ServerResponse.BodyBuilder.html() = contentType(TEXT_HTML) 44 | 45 | fun ServerResponse.BodyBuilder.textStream() = contentType(TEXT_EVENT_STREAM) 46 | fun ServerResponse.BodyBuilder.jsonStream() = contentType(APPLICATION_STREAM_JSON) 47 | 48 | fun permanentRedirect(uri: String) = permanentRedirect(URI(uri)).build() 49 | 50 | fun seeOther(uri: String) = seeOther(URI(uri)).build() 51 | 52 | // -------------------- 53 | // Date/Time extensions 54 | // -------------------- 55 | 56 | fun LocalDateTime.formatDate(language: Language): String = 57 | if (language == Language.ENGLISH) this.format(englishDateFormatter) else this.format(frenchDateFormatter) 58 | 59 | fun LocalDateTime.formatTalkDate(language: Language): String = 60 | if (language == Language.ENGLISH) this.format(englishTalkDateFormatter) else this.format(frenchTalkDateFormatter).capitalize() 61 | 62 | fun LocalDateTime.formatTalkTime(language: Language): String = 63 | if (language == Language.ENGLISH) this.format(englishTalkTimeFormatter) else this.format(frenchTalkTimeFormatter) 64 | 65 | fun LocalDateTime.toRFC3339(): String = ZonedDateTime.of(this, ZoneOffset.UTC) .format(rfc3339Formatter) 66 | 67 | 68 | private val daysLookup: Map = 69 | IntStream.rangeClosed(1, 31).boxed().collect(Collectors.toMap(Int::toLong, ::getOrdinal)) 70 | 71 | private val frenchDateFormatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.FRENCH) 72 | 73 | private val englishDateFormatter = DateTimeFormatterBuilder() 74 | .appendPattern("MMMM") 75 | .appendLiteral(" ") 76 | .appendText(ChronoField.DAY_OF_MONTH, daysLookup) 77 | .appendLiteral(" ") 78 | .appendPattern("yyyy") 79 | .toFormatter(Locale.ENGLISH) 80 | 81 | private val frenchTalkDateFormatter = DateTimeFormatter.ofPattern("EEEE d MMMM", Locale.FRENCH) 82 | 83 | private val frenchTalkTimeFormatter = DateTimeFormatter.ofPattern("HH'h'mm", Locale.FRENCH) 84 | 85 | private val englishTalkDateFormatter = DateTimeFormatterBuilder() 86 | .appendPattern("EEEE") 87 | .appendLiteral(" ") 88 | .appendPattern("MMMM") 89 | .appendLiteral(" ") 90 | .appendText(ChronoField.DAY_OF_MONTH, daysLookup) 91 | .toFormatter(Locale.ENGLISH) 92 | 93 | private val englishTalkTimeFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH) 94 | 95 | private val rfc3339Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX") 96 | 97 | 98 | 99 | private fun getOrdinal(n: Int) = 100 | when { 101 | n in 11..13 -> "${n}th" 102 | n % 10 == 1 -> "${n}st" 103 | n % 10 == 2 -> "${n}nd" 104 | n % 10 == 3 -> "${n}rd" 105 | else -> "${n}th" 106 | } 107 | 108 | // ---------------- 109 | // Other extensions 110 | // ---------------- 111 | 112 | fun String.stripAccents() = Normalizer 113 | .normalize(this, Normalizer.Form.NFD) 114 | .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "") 115 | 116 | fun String.toSlug() = toLowerCase() 117 | .stripAccents() 118 | .replace("\n", " ") 119 | .replace("[^a-z\\d\\s]".toRegex(), " ") 120 | .split(" ") 121 | .joinToString("-") 122 | .replace("-+".toRegex(), "-") // Avoid multiple consecutive "--" 123 | 124 | fun Iterable.shuffle(): Iterable = 125 | toMutableList().apply { Collections.shuffle(this) } 126 | 127 | fun localePrefix(locale: Locale) = if (locale.language == "en") "/en" else "" 128 | 129 | // ---------------- 130 | // Date Extension methods 131 | // ---------------- 132 | 133 | fun LocalDate.toStr(format:String = "dd/MM/yyyy") = DateTimeFormatter.ofPattern(format).format(this) 134 | fun String.toLocalDate(format:String = "dd/MM/yyyy") = LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) -------------------------------------------------------------------------------- /stream-service/src/main/kotlin/com/example/StreamApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.http.MediaType.APPLICATION_STREAM_JSON 8 | import org.springframework.http.MediaType.TEXT_EVENT_STREAM 9 | import org.springframework.stereotype.Component 10 | import org.springframework.stereotype.Service 11 | import org.springframework.web.reactive.function.server.ServerRequest 12 | import org.springframework.web.reactive.function.server.ServerResponse.ok 13 | import org.springframework.web.reactive.function.server.router 14 | import reactor.core.publisher.Flux 15 | import reactor.core.publisher.SynchronousSink 16 | import reactor.core.publisher.toFlux 17 | import java.math.BigDecimal 18 | import java.math.MathContext 19 | import java.time.Duration 20 | import java.time.Duration.ofMillis 21 | import java.time.Instant 22 | import java.util.* 23 | import kotlin.coroutines.experimental.buildIterator 24 | import kotlin.coroutines.experimental.buildSequence 25 | 26 | @SpringBootApplication 27 | class StreamApplication 28 | 29 | // TODO: Kotlin Functional bean declaration DSL 30 | // https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685 31 | //fun beans() = beans { 32 | // bean { 33 | // QuoteRoutes(it.ref()) 34 | // } 35 | //} 36 | 37 | @Configuration 38 | class QuoteRoutes(val streamHandler: StreamHandler) { 39 | 40 | @Bean 41 | fun quoteRouter() = router { 42 | GET("/sse/quotes").nest { 43 | accept(TEXT_EVENT_STREAM, streamHandler::fetchQuotesSSE) 44 | accept(APPLICATION_STREAM_JSON, streamHandler::fetchQuotes) 45 | } 46 | GET("/sse/fibonacci").nest { 47 | accept(TEXT_EVENT_STREAM, streamHandler::fetchFibonacciSSE) 48 | accept(APPLICATION_STREAM_JSON, streamHandler::fetchFibonacci) 49 | } 50 | } 51 | 52 | } 53 | 54 | @Component 55 | class StreamHandler(val quoteGenerator: QuoteGenerator) { 56 | final val quoteStream = quoteGenerator.fetchQuoteStream(ofMillis(200)).share() 57 | final val fibonacciStream= quoteGenerator.fetchFibonacciStream(ofMillis(1000)).share(); 58 | 59 | fun fetchQuotesSSE(req: ServerRequest) = ok() 60 | .contentType(TEXT_EVENT_STREAM) 61 | .body(quoteStream, Quote::class.java) 62 | 63 | fun fetchQuotes(req: ServerRequest) = ok() 64 | .contentType(APPLICATION_STREAM_JSON) 65 | .body(quoteStream, Quote::class.java) 66 | 67 | fun fetchFibonacciSSE(req: ServerRequest) = ok() 68 | .contentType(TEXT_EVENT_STREAM) 69 | .body(fibonacciStream, String::class.java) 70 | 71 | fun fetchFibonacci(req: ServerRequest) = ok() 72 | .contentType(APPLICATION_STREAM_JSON) 73 | .body(fibonacciStream, String::class.java) 74 | } 75 | 76 | @Service 77 | class QuoteGenerator { 78 | 79 | val mathContext = MathContext(2) 80 | 81 | val random = Random() 82 | 83 | val prices = listOf( 84 | Quote("CTXS", BigDecimal(82.26, mathContext)), 85 | Quote("DELL", BigDecimal(63.74, mathContext)), 86 | Quote("GOOG", BigDecimal(847.24, mathContext)), 87 | Quote("MSFT", BigDecimal(65.11, mathContext)), 88 | Quote("ORCL", BigDecimal(45.71, mathContext)), 89 | Quote("RHT", BigDecimal(84.29, mathContext)), 90 | Quote("VMW", BigDecimal(92.21, mathContext)) 91 | ) 92 | 93 | 94 | fun fetchQuoteStream(period: Duration) = Flux.generate({ 0 }, 95 | { index, sink: SynchronousSink -> 96 | sink.next(updateQuote(prices[index])) 97 | (index + 1) % prices.size 98 | }).zipWith(Flux.interval(period)) 99 | .map { it.t1.copy(instant = Instant.now()) } 100 | .log("ss-QuoteGenerator") 101 | 102 | 103 | private fun updateQuote(quote: Quote) = quote.copy( 104 | price = quote.price.add(quote.price.multiply( 105 | BigDecimal(0.05 * random.nextDouble()), mathContext)) 106 | ) 107 | 108 | 109 | val fibonacci= buildIterator { 110 | 111 | var a = 0L 112 | var b = 1L 113 | 114 | while (true) { 115 | yield(b) 116 | 117 | val next = a + b 118 | a = b; b = next 119 | } 120 | } 121 | 122 | fun fetchFibonacciStream(interval: Duration) = fibonacci.toFlux() 123 | .delayElements(interval) 124 | .map{it.toString()} 125 | .log("ss-fibonacci") 126 | 127 | } 128 | 129 | // NOTE: declared it in shared module `commons` 130 | //data class Quote(val ticker: String, val price: BigDecimal, val instant: Instant = Instant.now()) 131 | 132 | fun main(args: Array) { 133 | SpringApplication.run(StreamApplication::class.java, *args) 134 | // SpringApplication(Application::class.java).apply { 135 | // addInitializers(beans()) 136 | // run(*args) 137 | // } 138 | } 139 | 140 | 141 | // Programmatic bootstrap - Start without spring boot 142 | //fun main(args: Array) { 143 | // val quoteGenerator = QuoteGenerator(); 144 | // val quoteHandler = QuoteHandler(quoteGenerator); 145 | // val routes = QuoteRoutes(quoteHandler).quoteRouter() 146 | // 147 | // val handler = ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(routes)) 148 | // HttpServer.create(8082).newHandler(handler).block() 149 | // 150 | // Thread.currentThread().join() 151 | //} 152 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ui-app/src/main/resources/templates/guestbook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Streaming 10 | 11 | 12 | 13 | 14 | 28 |
29 |
30 |
31 |

Comment

32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 | 42 |
43 |
44 |
45 |

GuestBook

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
IdDateNameComment
IdSome DateSumomy comment
64 |
65 |
66 |
67 | 68 | 69 | 101 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /mongo-data-service/src/main/resources/data/events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "current": false, 4 | "end": [ 5 | 2012, 6 | 4, 7 | 26 8 | ], 9 | "id": "mixit12", 10 | "sponsors": [ 11 | { 12 | "level": "NONE", 13 | "sponsorId": "Zenika Lyon", 14 | "subscriptionDate": [ 15 | 2017, 16 | 2, 17 | 28 18 | ] 19 | }, 20 | { 21 | "level": "NONE", 22 | "sponsorId": "Sqli", 23 | "subscriptionDate": [ 24 | 2017, 25 | 2, 26 | 28 27 | ] 28 | }, 29 | { 30 | "level": "NONE", 31 | "sponsorId": "VISEO", 32 | "subscriptionDate": [ 33 | 2017, 34 | 2, 35 | 28 36 | ] 37 | }, 38 | { 39 | "level": "NONE", 40 | "sponsorId": "Yseop", 41 | "subscriptionDate": [ 42 | 2017, 43 | 2, 44 | 28 45 | ] 46 | }, 47 | { 48 | "level": "NONE", 49 | "sponsorId": "terre.dagile", 50 | "subscriptionDate": [ 51 | 2017, 52 | 2, 53 | 28 54 | ] 55 | }, 56 | { 57 | "level": "NONE", 58 | "sponsorId": "woonoz", 59 | "subscriptionDate": [ 60 | 2017, 61 | 2, 62 | 28 63 | ] 64 | }, 65 | { 66 | "level": "NONE", 67 | "sponsorId": "thales", 68 | "subscriptionDate": [ 69 | 2017, 70 | 2, 71 | 28 72 | ] 73 | }, 74 | { 75 | "level": "NONE", 76 | "sponsorId": "google", 77 | "subscriptionDate": [ 78 | 2017, 79 | 2, 80 | 28 81 | ] 82 | } 83 | ], 84 | "start": [ 85 | 2012, 86 | 4, 87 | 26 88 | ], 89 | "year": 2012 90 | }, 91 | { 92 | "current": false, 93 | "end": [ 94 | 2013, 95 | 4, 96 | 26 97 | ], 98 | "id": "mixit13", 99 | "sponsors": [ 100 | { 101 | "level": "NONE", 102 | "sponsorId": "Zenika Lyon", 103 | "subscriptionDate": [ 104 | 2017, 105 | 2, 106 | 28 107 | ] 108 | }, 109 | { 110 | "level": "NONE", 111 | "sponsorId": "Sqli", 112 | "subscriptionDate": [ 113 | 2017, 114 | 2, 115 | 28 116 | ] 117 | }, 118 | { 119 | "level": "NONE", 120 | "sponsorId": "VISEO", 121 | "subscriptionDate": [ 122 | 2017, 123 | 2, 124 | 28 125 | ] 126 | }, 127 | { 128 | "level": "NONE", 129 | "sponsorId": "terre.dagile", 130 | "subscriptionDate": [ 131 | 2017, 132 | 2, 133 | 28 134 | ] 135 | }, 136 | { 137 | "level": "NONE", 138 | "sponsorId": "woonoz", 139 | "subscriptionDate": [ 140 | 2017, 141 | 2, 142 | 28 143 | ] 144 | }, 145 | { 146 | "level": "NONE", 147 | "sponsorId": "thales", 148 | "subscriptionDate": [ 149 | 2017, 150 | 2, 151 | 28 152 | ] 153 | }, 154 | { 155 | "level": "NONE", 156 | "sponsorId": "google", 157 | "subscriptionDate": [ 158 | 2017, 159 | 2, 160 | 28 161 | ] 162 | }, 163 | { 164 | "level": "NONE", 165 | "sponsorId": "sonarsource", 166 | "subscriptionDate": [ 167 | 2017, 168 | 2, 169 | 28 170 | ] 171 | }, 172 | { 173 | "level": "NONE", 174 | "sponsorId": "elasticsearch", 175 | "subscriptionDate": [ 176 | 2017, 177 | 2, 178 | 28 179 | ] 180 | }, 181 | { 182 | "level": "NONE", 183 | "sponsorId": "GitHub", 184 | "subscriptionDate": [ 185 | 2017, 186 | 2, 187 | 28 188 | ] 189 | } 190 | ], 191 | "start": [ 192 | 2013, 193 | 4, 194 | 25 195 | ], 196 | "year": 2013 197 | }, 198 | { 199 | "current": false, 200 | "end": [ 201 | 2014, 202 | 4, 203 | 30 204 | ], 205 | "id": "mixit14", 206 | "sponsors": [ 207 | { 208 | "level": "NONE", 209 | "sponsorId": "Zenika Lyon", 210 | "subscriptionDate": [ 211 | 2017, 212 | 2, 213 | 28 214 | ] 215 | }, 216 | { 217 | "level": "NONE", 218 | "sponsorId": "VISEO", 219 | "subscriptionDate": [ 220 | 2017, 221 | 2, 222 | 28 223 | ] 224 | }, 225 | { 226 | "level": "NONE", 227 | "sponsorId": "terre.dagile", 228 | "subscriptionDate": [ 229 | 2017, 230 | 2, 231 | 28 232 | ] 233 | }, 234 | { 235 | "level": "NONE", 236 | "sponsorId": "woonoz", 237 | "subscriptionDate": [ 238 | 2017, 239 | 2, 240 | 28 241 | ] 242 | }, 243 | { 244 | "level": "NONE", 245 | "sponsorId": "google", 246 | "subscriptionDate": [ 247 | 2017, 248 | 2, 249 | 28 250 | ] 251 | }, 252 | { 253 | "level": "NONE", 254 | "sponsorId": "GitHub", 255 | "subscriptionDate": [ 256 | 2017, 257 | 2, 258 | 28 259 | ] 260 | }, 261 | { 262 | "level": "NONE", 263 | "sponsorId": "cgi", 264 | "subscriptionDate": [ 265 | 2017, 266 | 2, 267 | 28 268 | ] 269 | }, 270 | { 271 | "level": "NONE", 272 | "sponsorId": "steria", 273 | "subscriptionDate": [ 274 | 2017, 275 | 2, 276 | 28 277 | ] 278 | }, 279 | { 280 | "level": "NONE", 281 | "sponsorId": "SerliFr", 282 | "subscriptionDate": [ 283 | 2017, 284 | 2, 285 | 28 286 | ] 287 | }, 288 | { 289 | "level": "NONE", 290 | "sponsorId": "Open_ESN", 291 | "subscriptionDate": [ 292 | 2017, 293 | 2, 294 | 28 295 | ] 296 | }, 297 | { 298 | "level": "NONE", 299 | "sponsorId": "econocom-osiatis", 300 | "subscriptionDate": [ 301 | 2017, 302 | 2, 303 | 28 304 | ] 305 | }, 306 | { 307 | "level": "NONE", 308 | "sponsorId": "Mozilla", 309 | "subscriptionDate": [ 310 | 2017, 311 | 2, 312 | 28 313 | ] 314 | }, 315 | { 316 | "level": "NONE", 317 | "sponsorId": "VersionOne", 318 | "subscriptionDate": [ 319 | 2017, 320 | 2, 321 | 28 322 | ] 323 | }, 324 | { 325 | "level": "NONE", 326 | "sponsorId": "Redhat", 327 | "subscriptionDate": [ 328 | 2017, 329 | 2, 330 | 28 331 | ] 332 | }, 333 | { 334 | "level": "NONE", 335 | "sponsorId": "Atlassian", 336 | "subscriptionDate": [ 337 | 2017, 338 | 2, 339 | 28 340 | ] 341 | }, 342 | { 343 | "level": "NONE", 344 | "sponsorId": "Mailjet", 345 | "subscriptionDate": [ 346 | 2017, 347 | 2, 348 | 28 349 | ] 350 | } 351 | ], 352 | "start": [ 353 | 2014, 354 | 4, 355 | 29 356 | ], 357 | "year": 2014 358 | }, 359 | { 360 | "current": false, 361 | "end": [ 362 | 2015, 363 | 4, 364 | 17 365 | ], 366 | "id": "mixit15", 367 | "sponsors": [ 368 | { 369 | "level": "NONE", 370 | "sponsorId": "VISEO", 371 | "subscriptionDate": [ 372 | 2017, 373 | 2, 374 | 28 375 | ] 376 | }, 377 | { 378 | "level": "NONE", 379 | "sponsorId": "woonoz", 380 | "subscriptionDate": [ 381 | 2017, 382 | 2, 383 | 28 384 | ] 385 | }, 386 | { 387 | "level": "NONE", 388 | "sponsorId": "google", 389 | "subscriptionDate": [ 390 | 2017, 391 | 2, 392 | 28 393 | ] 394 | }, 395 | { 396 | "level": "NONE", 397 | "sponsorId": "cgi", 398 | "subscriptionDate": [ 399 | 2017, 400 | 2, 401 | 28 402 | ] 403 | }, 404 | { 405 | "level": "NONE", 406 | "sponsorId": "Sopra Steria", 407 | "subscriptionDate": [ 408 | 2017, 409 | 2, 410 | 28 411 | ] 412 | }, 413 | { 414 | "level": "NONE", 415 | "sponsorId": "neosoftlyon", 416 | "subscriptionDate": [ 417 | 2017, 418 | 2, 419 | 28 420 | ] 421 | }, 422 | { 423 | "level": "NONE", 424 | "sponsorId": "FIDUCIAL", 425 | "subscriptionDate": [ 426 | 2017, 427 | 2, 428 | 28 429 | ] 430 | }, 431 | { 432 | "level": "NONE", 433 | "sponsorId": "SII_rhonealpes", 434 | "subscriptionDate": [ 435 | 2017, 436 | 2, 437 | 28 438 | ] 439 | }, 440 | { 441 | "level": "NONE", 442 | "sponsorId": "XebiaLabs", 443 | "subscriptionDate": [ 444 | 2017, 445 | 2, 446 | 28 447 | ] 448 | }, 449 | { 450 | "level": "NONE", 451 | "sponsorId": "alfene", 452 | "subscriptionDate": [ 453 | 2017, 454 | 2, 455 | 28 456 | ] 457 | }, 458 | { 459 | "level": "NONE", 460 | "sponsorId": "Enalean", 461 | "subscriptionDate": [ 462 | 2017, 463 | 2, 464 | 28 465 | ] 466 | }, 467 | { 468 | "level": "NONE", 469 | "sponsorId": "ERDF-Linky", 470 | "subscriptionDate": [ 471 | 2017, 472 | 2, 473 | 28 474 | ] 475 | }, 476 | { 477 | "level": "NONE", 478 | "sponsorId": "stormshield", 479 | "subscriptionDate": [ 480 | 2017, 481 | 2, 482 | 28 483 | ] 484 | }, 485 | { 486 | "level": "NONE", 487 | "sponsorId": "WorldlineFrance", 488 | "subscriptionDate": [ 489 | 2017, 490 | 2, 491 | 28 492 | ] 493 | }, 494 | { 495 | "level": "NONE", 496 | "sponsorId": "Elao", 497 | "subscriptionDate": [ 498 | 2017, 499 | 2, 500 | 28 501 | ] 502 | }, 503 | { 504 | "level": "NONE", 505 | "sponsorId": "sourcingisr", 506 | "subscriptionDate": [ 507 | 2017, 508 | 2, 509 | 28 510 | ] 511 | } 512 | ], 513 | "start": [ 514 | 2015, 515 | 4, 516 | 16 517 | ], 518 | "year": 2015 519 | }, 520 | { 521 | "current": false, 522 | "end": [ 523 | 2016, 524 | 4, 525 | 22 526 | ], 527 | "id": "mixit16", 528 | "sponsors": [ 529 | { 530 | "level": "NONE", 531 | "sponsorId": "Zenika Lyon", 532 | "subscriptionDate": [ 533 | 2017, 534 | 2, 535 | 28 536 | ] 537 | }, 538 | { 539 | "level": "NONE", 540 | "sponsorId": "Sopra Steria", 541 | "subscriptionDate": [ 542 | 2017, 543 | 2, 544 | 28 545 | ] 546 | }, 547 | { 548 | "level": "NONE", 549 | "sponsorId": "annick.challancin@esker.fr", 550 | "subscriptionDate": [ 551 | 2017, 552 | 2, 553 | 28 554 | ] 555 | }, 556 | { 557 | "level": "NONE", 558 | "sponsorId": "VISEO", 559 | "subscriptionDate": [ 560 | 2017, 561 | 2, 562 | 28 563 | ] 564 | }, 565 | { 566 | "level": "NONE", 567 | "sponsorId": "WorldlineFrance", 568 | "subscriptionDate": [ 569 | 2017, 570 | 2, 571 | 28 572 | ] 573 | }, 574 | { 575 | "level": "NONE", 576 | "sponsorId": "Sword", 577 | "subscriptionDate": [ 578 | 2017, 579 | 2, 580 | 28 581 | ] 582 | }, 583 | { 584 | "level": "NONE", 585 | "sponsorId": "stormshield", 586 | "subscriptionDate": [ 587 | 2017, 588 | 2, 589 | 28 590 | ] 591 | }, 592 | { 593 | "level": "NONE", 594 | "sponsorId": "Redhat", 595 | "subscriptionDate": [ 596 | 2017, 597 | 2, 598 | 28 599 | ] 600 | }, 601 | { 602 | "level": "NONE", 603 | "sponsorId": "ERDF-Linky", 604 | "subscriptionDate": [ 605 | 2017, 606 | 2, 607 | 28 608 | ] 609 | }, 610 | { 611 | "level": "NONE", 612 | "sponsorId": "cgi", 613 | "subscriptionDate": [ 614 | 2017, 615 | 2, 616 | 28 617 | ] 618 | }, 619 | { 620 | "level": "NONE", 621 | "sponsorId": "sonarsource", 622 | "subscriptionDate": [ 623 | 2017, 624 | 2, 625 | 28 626 | ] 627 | }, 628 | { 629 | "level": "NONE", 630 | "sponsorId": "econocom-osiatis", 631 | "subscriptionDate": [ 632 | 2017, 633 | 2, 634 | 28 635 | ] 636 | }, 637 | { 638 | "level": "NONE", 639 | "sponsorId": "sourcingisr", 640 | "subscriptionDate": [ 641 | 2017, 642 | 2, 643 | 28 644 | ] 645 | }, 646 | { 647 | "level": "NONE", 648 | "sponsorId": "SII_rhonealpes", 649 | "subscriptionDate": [ 650 | 2017, 651 | 2, 652 | 28 653 | ] 654 | }, 655 | { 656 | "level": "NONE", 657 | "sponsorId": "1338530083", 658 | "subscriptionDate": [ 659 | 2017, 660 | 2, 661 | 28 662 | ] 663 | }, 664 | { 665 | "level": "NONE", 666 | "sponsorId": "stackoverflow", 667 | "subscriptionDate": [ 668 | 2017, 669 | 2, 670 | 28 671 | ] 672 | }, 673 | { 674 | "level": "NONE", 675 | "sponsorId": "alfene", 676 | "subscriptionDate": [ 677 | 2017, 678 | 2, 679 | 28 680 | ] 681 | }, 682 | { 683 | "level": "NONE", 684 | "sponsorId": "amaris", 685 | "subscriptionDate": [ 686 | 2017, 687 | 2, 688 | 28 689 | ] 690 | }, 691 | { 692 | "level": "NONE", 693 | "sponsorId": "boot-start", 694 | "subscriptionDate": [ 695 | 2017, 696 | 2, 697 | 28 698 | ] 699 | }, 700 | { 701 | "level": "NONE", 702 | "sponsorId": "hired", 703 | "subscriptionDate": [ 704 | 2017, 705 | 2, 706 | 28 707 | ] 708 | }, 709 | { 710 | "level": "NONE", 711 | "sponsorId": "onlylyon", 712 | "subscriptionDate": [ 713 | 2017, 714 | 2, 715 | 28 716 | ] 717 | }, 718 | { 719 | "level": "NONE", 720 | "sponsorId": "pivotal", 721 | "subscriptionDate": [ 722 | 2017, 723 | 2, 724 | 28 725 | ] 726 | }, 727 | { 728 | "level": "NONE", 729 | "sponsorId": "infoq", 730 | "subscriptionDate": [ 731 | 2017, 732 | 2, 733 | 28 734 | ] 735 | }, 736 | { 737 | "level": "NONE", 738 | "sponsorId": "lyontechhub", 739 | "subscriptionDate": [ 740 | 2017, 741 | 2, 742 | 28 743 | ] 744 | } 745 | ], 746 | "start": [ 747 | 2016, 748 | 4, 749 | 21 750 | ], 751 | "year": 2016 752 | }, 753 | { 754 | "current": true, 755 | "end": [ 756 | 2017, 757 | 4, 758 | 21 759 | ], 760 | "id": "mixit17", 761 | "sponsors": [ 762 | { 763 | "level": "GOLD", 764 | "sponsorId": "Zenika Lyon", 765 | "subscriptionDate": [ 766 | 2016, 767 | 11, 768 | 4 769 | ] 770 | }, 771 | { 772 | "level": "GOLD", 773 | "sponsorId": "Sword", 774 | "subscriptionDate": [ 775 | 2016, 776 | 12, 777 | 7 778 | ] 779 | }, 780 | { 781 | "level": "GOLD", 782 | "sponsorId": "Ippon", 783 | "subscriptionDate": [ 784 | 2016, 785 | 12, 786 | 14 787 | ] 788 | }, 789 | { 790 | "level": "GOLD", 791 | "sponsorId": "Sopra Steria", 792 | "subscriptionDate": [ 793 | 2016, 794 | 12, 795 | 23 796 | ] 797 | }, 798 | { 799 | "level": "GOLD", 800 | "sponsorId": "annick.challancin@esker.fr", 801 | "subscriptionDate": [ 802 | 2017, 803 | 1, 804 | 10 805 | ] 806 | }, 807 | { 808 | "level": "GOLD", 809 | "sponsorId": "LDLC", 810 | "subscriptionDate": [ 811 | 2017, 812 | 1, 813 | 20 814 | ] 815 | }, 816 | { 817 | "level": "GOLD", 818 | "sponsorId": "VISEO", 819 | "subscriptionDate": [ 820 | 2017, 821 | 2, 822 | 20 823 | ] 824 | }, 825 | { 826 | "level": "GOLD", 827 | "sponsorId": "GitHub", 828 | "subscriptionDate": [ 829 | 2017, 830 | 2, 831 | 28 832 | ] 833 | }, 834 | { 835 | "level": "LANYARD", 836 | "sponsorId": "WorldlineFrance", 837 | "subscriptionDate": [ 838 | 2016, 839 | 10, 840 | 19 841 | ] 842 | }, 843 | { 844 | "level": "PARTY", 845 | "sponsorId": "onlylyon", 846 | "subscriptionDate": [ 847 | 2017, 848 | 1, 849 | 1 850 | ] 851 | }, 852 | { 853 | "level": "PARTY", 854 | "sponsorId": "Hopwork", 855 | "subscriptionDate": [ 856 | 2016, 857 | 11, 858 | 2 859 | ] 860 | }, 861 | { 862 | "level": "SILVER", 863 | "sponsorId": "SerliFr", 864 | "subscriptionDate": [ 865 | 2016, 866 | 12, 867 | 13 868 | ] 869 | }, 870 | { 871 | "level": "SILVER", 872 | "sponsorId": "SII_rhonealpes", 873 | "subscriptionDate": [ 874 | 2016, 875 | 12, 876 | 20 877 | ] 878 | }, 879 | { 880 | "level": "SILVER", 881 | "sponsorId": "woonoz", 882 | "subscriptionDate": [ 883 | 2017, 884 | 1, 885 | 20 886 | ] 887 | }, 888 | { 889 | "level": "SILVER", 890 | "sponsorId": "Algolia", 891 | "subscriptionDate": [ 892 | 2017, 893 | 1, 894 | 23 895 | ] 896 | }, 897 | { 898 | "level": "SILVER", 899 | "sponsorId": "Enedis", 900 | "subscriptionDate": [ 901 | 2017, 902 | 1, 903 | 24 904 | ] 905 | }, 906 | { 907 | "level": "SILVER", 908 | "sponsorId": "Axway", 909 | "subscriptionDate": [ 910 | 2017, 911 | 2, 912 | 24 913 | ] 914 | }, 915 | { 916 | "level": "SILVER", 917 | "sponsorId": "sourcingisr", 918 | "subscriptionDate": [ 919 | 2017, 920 | 1, 921 | 25 922 | ] 923 | }, 924 | { 925 | "level": "SILVER", 926 | "sponsorId": "NinjaSquad", 927 | "subscriptionDate": [ 928 | 2017, 929 | 3, 930 | 27 931 | ] 932 | }, 933 | { 934 | "level": "SILVER", 935 | "sponsorId": "USEway", 936 | "subscriptionDate": [ 937 | 2017, 938 | 4, 939 | 4 940 | ] 941 | }, 942 | { 943 | "level": "HOSTING", 944 | "sponsorId": "pivotal", 945 | "subscriptionDate": [ 946 | 2017, 947 | 1, 948 | 20 949 | ] 950 | }, 951 | { 952 | "level": "VIDEO", 953 | "sponsorId": "infoq", 954 | "subscriptionDate": [ 955 | 2017, 956 | 4, 957 | 14 958 | ] 959 | } 960 | ], 961 | "start": [ 962 | 2017, 963 | 4, 964 | 20 965 | ], 966 | "year": 2017 967 | } 968 | ] 969 | --------------------------------------------------------------------------------