diff --git a/2019-08/spring-17/.gitignore b/2019-08/spring-17/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2019-08/spring-17/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2019-08/spring-17/pom.xml b/2019-08/spring-17/pom.xml
new file mode 100644
index 00000000..2ea124e8
--- /dev/null
+++ b/2019-08/spring-17/pom.xml
@@ -0,0 +1,18 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-17
+ 1.0
+
+ pom
+
+
+ spring-17-web-flux
+ spring-17-reactor
+ spring-17-reactive-spring-data
+
+
diff --git a/2019-08/spring-17/spring-17-reactive-spring-data/pom.xml b/2019-08/spring-17/spring-17-reactive-spring-data/pom.xml
new file mode 100644
index 00000000..bd8c05d5
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactive-spring-data/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-17-reactive-spring-data
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.6.RELEASE
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ org.mongodb
+ mongodb-driver-reactivestreams
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/Main.java b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..6ad1b997
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,37 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
+import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
+import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
+import org.springframework.context.ApplicationContext;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repostory.PersonRepository;
+
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) throws InterruptedException {
+ ApplicationContext context = SpringApplication.run(Main.class);
+
+ PersonRepository repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(
+ Flux.fromArray(new String[]{"Pushkin", "Lermontov"})
+ .map(Person::new)
+ ).subscribe();
+
+ Disposable flux = repository.findAll()
+ .subscribe(p -> System.out.println(p.getName()));
+
+ Thread.sleep(20000);
+
+ flux.dispose();
+ }
+}
diff --git a/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/domain/Person.java b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..2bdc3894
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,27 @@
+package ru.otus.spring.domain;
+
+public class Person {
+
+ private String id;
+ private String name;
+
+ public Person(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java
new file mode 100644
index 00000000..0485aa38
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java
@@ -0,0 +1,17 @@
+package ru.otus.spring.repostory;
+
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+
+public interface PersonRepository extends ReactiveMongoRepository {
+
+ Flux findByName(String name);
+
+ @Query("{ 'name': ?0 }")
+ Mono findFirstByName(String name);
+}
diff --git a/2019-08/spring-17/spring-17-reactive-spring-data/src/main/resources/application.yml b/2019-08/spring-17/spring-17-reactive-spring-data/src/main/resources/application.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/2019-08/spring-17/spring-17-reactor/pom.xml b/2019-08/spring-17/spring-17-reactor/pom.xml
new file mode 100644
index 00000000..b5a98b8d
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactor/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-17-reactor
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.3.RELEASE
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ io.projectreactor
+ reactor-core
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/Main.java b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..cc406c39
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,20 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import ru.otus.spring.reactor.FluxService;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ ConfigurableApplicationContext context = SpringApplication.run(Main.class);
+
+ FluxService service = context.getBean(FluxService.class);
+
+ service.printHello("Ivan");
+ }
+}
+
+
diff --git a/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java
new file mode 100644
index 00000000..3d2ca3cf
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java
@@ -0,0 +1,49 @@
+package ru.otus.spring.reactor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.Disposable;
+import reactor.core.publisher.DirectProcessor;
+import reactor.core.publisher.Mono;
+
+@Service
+public class FluxService {
+
+ private final Logger logger = LoggerFactory.getLogger(FluxService.class);
+
+ private final NonFluxService nonFluxService;
+ private final DirectProcessor processor;
+ private final Disposable flow;
+
+ @Autowired
+ public FluxService(NonFluxService nonFluxService) {
+ this.nonFluxService = nonFluxService;
+ // Создаём процессор - это reactor-овская реализация reactive-stream интерфейса
+ // Direct processor, кстати - это простой последовательный вызов методов)
+ processor = DirectProcessor.create();
+ // Здесь мы настриваем flow
+ flow = Mono.from(processor)
+ .map(nonFluxService::nonFluxSayHello)
+ .subscribe(this::printMessage);
+ }
+
+ /**
+ * Этот метод будет инициировать асинзронную обрабтку сообщения
+ *
+ * @param name это имя будет приходить из не-reactor окружения
+ */
+ public void printHello(String name) {
+ processor.onNext(new Message(name));
+ }
+
+ /**
+ * А это терминальный шаг для сообщения
+ *
+ * @param message а это финальный шаг для сообщения, отсюда можно вернуть рзультат в не-реактив окружение
+ */
+ private void printMessage(Message message) {
+ logger.info("Message received: {}", message.getValue());
+ }
+}
diff --git a/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/Message.java b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/Message.java
new file mode 100644
index 00000000..ac58a62c
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/Message.java
@@ -0,0 +1,14 @@
+package ru.otus.spring.reactor;
+
+public class Message {
+
+ private final String value;
+
+ public Message(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/NonFluxService.java b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/NonFluxService.java
new file mode 100644
index 00000000..1b9be28a
--- /dev/null
+++ b/2019-08/spring-17/spring-17-reactor/src/main/java/ru/otus/spring/reactor/NonFluxService.java
@@ -0,0 +1,24 @@
+package ru.otus.spring.reactor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NonFluxService {
+
+ private final Logger logger = LoggerFactory.getLogger(NonFluxService.class);
+
+ public Message nonFluxSayHello(Message message) {
+ logger.info("Message received in non-flux service: {}", message.getValue());
+
+ final String name = message.getValue();
+ final String withHello = "Hello, " + name + "!";
+ try {
+ Thread.sleep(1000);
+ return new Message(withHello);
+ } catch (InterruptedException ex) {
+ return new Message(withHello);
+ }
+ }
+}
diff --git a/2019-08/spring-17/spring-17-web-flux/pom.xml b/2019-08/spring-17/spring-17-web-flux/pom.xml
new file mode 100644
index 00000000..22e77b85
--- /dev/null
+++ b/2019-08/spring-17/spring-17-web-flux/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-17-web-flux
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.3.RELEASE
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ io.reactivex.rxjava2
+ rxjava
+ 2.2.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/Main.java b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..cad1ae76
--- /dev/null
+++ b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,14 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Main.class);
+ }
+}
+
+
diff --git a/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/ReactorController.java b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/ReactorController.java
new file mode 100644
index 00000000..537ead63
--- /dev/null
+++ b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/ReactorController.java
@@ -0,0 +1,33 @@
+package ru.otus.spring;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@RestController
+public class ReactorController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping("/flux/ten")
+ public Flux list() {
+ return Flux.range(1, 10);
+ }
+
+ @GetMapping(path = "/flux/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(Object::toString);
+ }
+}
diff --git a/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/RxJava2Controller.java b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/RxJava2Controller.java
new file mode 100644
index 00000000..edf0035f
--- /dev/null
+++ b/2019-08/spring-17/spring-17-web-flux/src/main/java/ru/otus/spring/RxJava2Controller.java
@@ -0,0 +1,20 @@
+package ru.otus.spring;
+
+import io.reactivex.Flowable;
+import io.reactivex.Single;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class RxJava2Controller {
+
+ @GetMapping("/rx/one")
+ public Single single() {
+ return Single.just("one");
+ }
+
+ @GetMapping("/rx/ten")
+ public Flowable list() {
+ return Flowable.range(1, 10);
+ }
+}
diff --git a/2019-08/spring-18/.gitignore b/2019-08/spring-18/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2019-08/spring-18/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2019-08/spring-18/pom.xml b/2019-08/spring-18/pom.xml
new file mode 100644
index 00000000..11195a0a
--- /dev/null
+++ b/2019-08/spring-18/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-18
+ 1.0
+
+ pom
+
+
+ spring-18-exercise
+ spring-18-solution
+
+
diff --git a/2019-08/spring-18/spring-18-exercise/pom.xml b/2019-08/spring-18/spring-18-exercise/pom.xml
new file mode 100644
index 00000000..1f23381e
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-18-exercise
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.2.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/Main.java b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..c9b686ae
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,67 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import java.util.Arrays;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.web.reactive.function.BodyInserters.fromObject;
+import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.ok;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ ApplicationContext context = SpringApplication.run(Main.class);
+ PersonRepository repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).subscribe(p -> System.out.println(p.getLastName()));
+
+ }
+
+ @Bean
+ public RouterFunction composedRoutes(PersonRepository repository) {
+
+ PersonHandler handler = new PersonHandler(repository);
+
+ RouterFunction route = route()
+ .GET("/func/person", accept(APPLICATION_JSON), handler::list)
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person)))
+ )
+ .build();
+
+ return route;
+ }
+
+ static class PersonHandler {
+
+ private PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
+
+
diff --git a/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,55 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+@Document
+public class Person {
+
+ @Id
+ private String id;
+
+ @JsonProperty("name")
+ @Field("name")
+ private String lastName;
+
+ private int age;
+
+ public Person() {
+ }
+
+ public Person(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Person(String lastName, int age) {
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.repository;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository
+ extends ReactiveMongoRepository {
+
+ Flux findAll();
+
+ Mono findById(String id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..3d723f35
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,33 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@RestController
+public class AnnotatedController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping("/flux/ten")
+ public Flux list() {
+ return Flux.range(1, 10);
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(i -> "" + i);
+ }
+}
diff --git a/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/PersonController.java b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..d1a8c364
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,32 @@
+package ru.otus.spring.rest;
+
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+@RestController
+public class PersonController {
+
+ private final PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/person")
+ public Flux all() {
+ return repository.findAll();
+ }
+
+ @GetMapping("/person/{id}")
+ public Mono byId(@PathVariable("id") String id) {
+ return repository.findById(id);
+ }
+
+ @PostMapping("/person")
+ public Mono save(@RequestBody Mono dto) {
+ return repository.save(dto);
+ }
+}
diff --git a/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..0ecf3ca8
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,31 @@
+package ru.otus.spring.repository;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(SpringRunner.class)
+@DataMongoTest
+public class PersonRepositoryTest {
+
+ @Autowired
+ private PersonRepository repository;
+
+ @Test
+ public void shouldSetIdOnSave() {
+ Mono personMono = repository.save(new Person("Bill", 12));
+
+ StepVerifier
+ .create(personMono)
+ .assertNext(person -> assertNotNull(person.getId()))
+ .expectComplete()
+ .verify();
+ }
+}
diff --git a/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..b6eb9806
--- /dev/null
+++ b/2019-08/spring-18/spring-18-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,32 @@
+package ru.otus.spring.rest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class PersonControllerTest {
+
+ @Autowired
+ private RouterFunction route;
+
+ @Test
+ public void testRoute() {
+ WebTestClient client = WebTestClient
+ .bindToRouterFunction(route)
+ .build();
+
+ client.get()
+ .uri("/func/person")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+
+
+}
diff --git a/2019-08/spring-18/spring-18-solution/pom.xml b/2019-08/spring-18/spring-18-solution/pom.xml
new file mode 100644
index 00000000..967dc1f2
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-18-solution
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.2.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/Main.java b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..934532de
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,58 @@
+package ru.otus.spring;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import java.util.Arrays;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.web.reactive.function.BodyInserters.fromObject;
+import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
+import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.notFound;
+import static org.springframework.web.reactive.function.server.ServerResponse.ok;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ ApplicationContext context = SpringApplication.run(Main.class);
+ PersonRepository repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).subscribe(p -> System.out.println(p.getLastName()));
+
+ }
+
+ @Bean
+ public RouterFunction composedRoutes(PersonRepository repository) {
+ return route()
+ .GET("/func/person", queryParam("name", StringUtils::isNotEmpty),
+ request -> request.queryParam("name")
+ .map(repository::findAllByLastName)
+ .map(persons -> ok().body(persons, Person.class))
+ .orElse(notFound().build())
+ )
+ .GET("/func/person", accept(APPLICATION_JSON),
+ request -> ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class))
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person)))
+ ).build();
+ }
+}
+
+
diff --git a/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/domain/Person.java b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,55 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+@Document
+public class Person {
+
+ @Id
+ private String id;
+
+ @JsonProperty("name")
+ @Field("name")
+ private String lastName;
+
+ private int age;
+
+ public Person() {
+ }
+
+ public Person(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Person(String lastName, int age) {
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.repository;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository
+ extends ReactiveMongoRepository {
+
+ Flux findAll();
+
+ Mono findById(String id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..3d723f35
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,33 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@RestController
+public class AnnotatedController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping("/flux/ten")
+ public Flux list() {
+ return Flux.range(1, 10);
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(i -> "" + i);
+ }
+}
diff --git a/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/PersonController.java b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..2edcaea9
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,42 @@
+package ru.otus.spring.rest;
+
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+@RestController
+public class PersonController {
+
+ private PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/person")
+ public Flux all() {
+ return repository.findAll();
+ }
+
+ @GetMapping("/person/{id}")
+ public Mono byId(@PathVariable("id") String id) {
+ return repository.findById(id);
+ }
+
+ @GetMapping("/person/byname")
+ public Flux byName(@RequestParam("name") String lastName) {
+ return repository.findAllByLastName(lastName);
+ }
+
+ @GetMapping("/person/byage")
+ public Flux byAge(@RequestParam int age) {
+ return repository.findAllByAge(age);
+ }
+
+ @PostMapping("/person")
+ public Mono save(@RequestBody Mono dto) {
+ return repository.save(dto);
+ }
+}
diff --git a/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..69eef1f6
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,43 @@
+package ru.otus.spring.repository;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(SpringRunner.class)
+@DataMongoTest
+public class PersonRepositoryTest {
+
+ @Autowired
+ private PersonRepository repository;
+
+ @Test
+ public void shouldSetIdOnSave() {
+ Mono personMono = repository.save(new Person("Bill", 12));
+
+ StepVerifier
+ .create(personMono)
+ .assertNext(person -> assertNotNull(person.getId()))
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ public void shouldFindByAge() {
+ repository.save(new Person("Pushkin", 18)).subscribe();
+
+ StepVerifier.create(
+ repository.findAllByAge(18)
+ )
+ .expectNextCount(1)
+ .expectComplete()
+ .verify();
+ }
+}
diff --git a/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..b6eb9806
--- /dev/null
+++ b/2019-08/spring-18/spring-18-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,32 @@
+package ru.otus.spring.rest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class PersonControllerTest {
+
+ @Autowired
+ private RouterFunction route;
+
+ @Test
+ public void testRoute() {
+ WebTestClient client = WebTestClient
+ .bindToRouterFunction(route)
+ .build();
+
+ client.get()
+ .uri("/func/person")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+
+
+}