diff --git a/2021-02/spring-18/.gitignore b/2021-02/spring-18/.gitignore
new file mode 100644
index 00000000..fbe7a1ed
--- /dev/null
+++ b/2021-02/spring-18/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+*.iml
+
+target/
+
+/node_modules
+/output
diff --git a/2021-02/spring-18/pom.xml b/2021-02/spring-18/pom.xml
new file mode 100644
index 00000000..d4fa3309
--- /dev/null
+++ b/2021-02/spring-18/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-18
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.4.RELEASE
+
+
+
+ 11
+ 11
+
+
+
+
+ io.reactivex.rxjava2
+ rxjava
+
+
+ com.google.guava
+ guava
+ 27.1-jre
+
+
+
diff --git a/2021-02/spring-18/src/main/java/ru/otus/CreateExamples.java b/2021-02/spring-18/src/main/java/ru/otus/CreateExamples.java
new file mode 100644
index 00000000..b8a21736
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/CreateExamples.java
@@ -0,0 +1,35 @@
+package ru.otus;
+
+import io.reactivex.Observable;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class CreateExamples {
+
+ public static void main(String[] args) {
+ Observable obs = justExample();
+ obs.forEach(System.out::println);
+ obs.forEach(System.out::println);
+ }
+
+ public static Observable justExample() {
+ return Observable.just("one", "two", "three");
+ }
+
+ public static Observable createExample() {
+ return Observable.create(emitter -> {
+ if (emitter.isDisposed()) {
+ return;
+ }
+ emitter.onNext("one");
+ emitter.onNext("two");//!
+ emitter.onNext("three");
+ if (!emitter.isDisposed()) {
+ emitter.onComplete();
+ }
+ });
+ }
+
+ public static Observable deferExample() {
+ return Observable.defer(() -> Observable.just("one", "two", "three"));
+ }
+}
diff --git a/2021-02/spring-18/src/main/java/ru/otus/LiveLikeExample.java b/2021-02/spring-18/src/main/java/ru/otus/LiveLikeExample.java
new file mode 100644
index 00000000..3f8e31d4
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/LiveLikeExample.java
@@ -0,0 +1,26 @@
+package ru.otus;
+
+import io.reactivex.Observable;
+
+import java.io.IOException;
+
+public class LiveLikeExample {
+
+ public static void main(String[] args) throws IOException {
+
+ System.in.read();
+ }
+
+ static Observable getName() {
+ return Observable.just("Jake");
+ }
+
+ static Observable getSurname() {
+ return Observable.just("Foo");
+ }
+
+ static Observable save(String fullName) {
+ System.out.println(fullName + " saved!");
+ return Observable.just("OK!");
+ }
+}
diff --git a/2021-02/spring-18/src/main/java/ru/otus/OperatorsExample.java b/2021-02/spring-18/src/main/java/ru/otus/OperatorsExample.java
new file mode 100644
index 00000000..92efffa6
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/OperatorsExample.java
@@ -0,0 +1,79 @@
+package ru.otus;
+
+import com.google.common.collect.ImmutableList;
+import io.reactivex.Observable;
+import io.reactivex.ObservableTransformer;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subjects.PublishSubject;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class OperatorsExample {
+ public static void main(String[] args) throws Exception {
+ simpleExample();
+ System.in.read();
+ }
+
+ public static void simpleExample() throws Exception {
+ List persons = ImmutableList.of(
+ new Person("John", "Dow", "male", LocalDate.of(1992, 3, 12)),
+ new Person("Jane", "Dow", "female", LocalDate.of(2001, 6, 23)),
+ new Person("Howard", "Lovecraft", "male", LocalDate.of(1890, 8, 20)),
+ new Person("Joanne", "Rowling", "female", LocalDate.of(1965, 6, 30)));
+
+ Observable.fromIterable(persons)
+ .filter(
+ person -> person.getBirth().isAfter(LocalDate.of(1990, 1, 1))
+ )
+ .map(p -> p.getFirstName() + " " + p.getLastName())
+ .toList()
+ .subscribe(System.out::println);
+ }
+
+ public static void publisherExample() throws Exception {
+ final Observable ob = magicPublisher();
+ System.out.println("First subscribed");
+ ob.subscribe(System.out::println);
+ Thread.sleep(5000);
+ System.out.println("Second subscribed");
+ ob.subscribe(System.out::println);
+ }
+
+ public static Observable magicPublisher() {
+ Random r = new Random(1);
+ AtomicInteger i = new AtomicInteger();
+ final Observable obs = Observable.generate(emitter ->
+ emitter.onNext("" + i.incrementAndGet()))
+ .concatMap(s -> Observable.just(s).delay(r.nextInt(1000), TimeUnit.MILLISECONDS))
+ .subscribeOn(Schedulers.newThread());
+ PublishSubject subject = PublishSubject.create();
+
+// BehaviorSubject subject = BehaviorSubject.create();
+
+// AsyncSubject subject = AsyncSubject.create();
+// CompletableFuture.runAsync(() -> {
+// try {
+// Thread.sleep(7000);
+// } catch (InterruptedException e) {
+// e.printStackTrace();
+// }
+// subject.onComplete();
+// });
+
+// ReplaySubject subject = ReplaySubject.create();
+ obs.subscribe(subject);
+ return subject;
+ }
+
+ //composeExmaple
+ private static ObservableTransformer filterAndUpperCase() {
+ return upstream -> upstream
+ .filter(s -> s.length() >= 4)
+ .map(String::toUpperCase);
+ }
+}
diff --git a/2021-02/spring-18/src/main/java/ru/otus/Person.java b/2021-02/spring-18/src/main/java/ru/otus/Person.java
new file mode 100644
index 00000000..72d3f520
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/Person.java
@@ -0,0 +1,66 @@
+package ru.otus;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+public class Person {
+ private String firstName;
+ private String lastName;
+ private String gender;
+ private LocalDate birth;
+
+ public Person(String firstName, String lastName, String gender, LocalDate birth) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.gender = gender;
+ this.birth = birth;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getGender() {
+ return gender;
+ }
+
+ public void setGender(String gender) {
+ this.gender = gender;
+ }
+
+ public LocalDate getBirth() {
+ return birth;
+ }
+
+ public void setBirth(LocalDate birth) {
+ this.birth = birth;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Person person = (Person) o;
+ return Objects.equals(firstName, person.firstName) &&
+ Objects.equals(lastName, person.lastName) &&
+ Objects.equals(gender, person.gender) &&
+ Objects.equals(birth, person.birth);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstName, lastName, gender, birth);
+ }
+}
diff --git a/2021-02/spring-18/src/main/java/ru/otus/comparison/AsyncComparison.java b/2021-02/spring-18/src/main/java/ru/otus/comparison/AsyncComparison.java
new file mode 100644
index 00000000..5b0b227a
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/comparison/AsyncComparison.java
@@ -0,0 +1,40 @@
+package ru.otus.comparison;
+
+import io.reactivex.Observable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.io.IOException;
+
+public class AsyncComparison {
+
+ public static void main(String[] args) throws IOException {
+ final long timeStarted = System.currentTimeMillis();
+ final Observable obs = controller();
+ obs.subscribe(System.out::println);
+ System.out.println("Wait time " + (System.currentTimeMillis() - timeStarted));
+ System.in.read();
+ }
+
+ static Observable controller() {
+ return service();
+ }
+
+ static Observable service() {
+ return repository();
+ }
+
+ static Observable repository() {
+ return database();
+ }
+
+ static Observable database() {
+ return Observable.defer(() -> {
+ try {
+ Thread.sleep(4000);
+ } catch (Exception e) {
+ System.out.println("Don't do this");
+ }
+ return Observable.just("Hello world");
+ }).subscribeOn(Schedulers.newThread());
+ }
+}
diff --git a/2021-02/spring-18/src/main/java/ru/otus/comparison/SyncComparison.java b/2021-02/spring-18/src/main/java/ru/otus/comparison/SyncComparison.java
new file mode 100644
index 00000000..c2bf7077
--- /dev/null
+++ b/2021-02/spring-18/src/main/java/ru/otus/comparison/SyncComparison.java
@@ -0,0 +1,31 @@
+package ru.otus.comparison;
+
+public class SyncComparison {
+
+ public static void main(String[] args) {
+ final long timeStarted = System.currentTimeMillis();
+ System.out.println(controller());
+ System.out.println(System.currentTimeMillis() - timeStarted);
+ }
+
+ static String controller() {
+ return service();
+ }
+
+ static String service() {
+ return repository();
+ }
+
+ static String repository() {
+ return database();
+ }
+
+ static String database() {
+ try {
+ Thread.sleep(4000);
+ } catch (Exception e) {
+ System.out.println("Don't do this");
+ }
+ return "Hello world";
+ }
+}
diff --git a/2021-02/spring-19/.gitignore b/2021-02/spring-19/.gitignore
new file mode 100644
index 00000000..4ea52072
--- /dev/null
+++ b/2021-02/spring-19/.gitignore
@@ -0,0 +1,24 @@
+target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
diff --git a/2021-02/spring-19/pom.xml b/2021-02/spring-19/pom.xml
new file mode 100644
index 00000000..06dd04ce
--- /dev/null
+++ b/2021-02/spring-19/pom.xml
@@ -0,0 +1,18 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-19
+ 1.0
+
+ pom
+
+
+ spring-19-web-flux
+ spring-19-reactor
+ spring-19-reactive-spring-data
+
+
diff --git a/2021-02/spring-19/spring-19-reactive-spring-data/pom.xml b/2021-02/spring-19/spring-19-reactive-spring-data/pom.xml
new file mode 100644
index 00000000..5e963c48
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactive-spring-data/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-19-reactive-spring-data
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+ 13
+ 13
+
+
+
+
+ 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/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..5929a2e2
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactive-spring-data/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.ApplicationContext;
+import ru.otus.spring.repostory.AccountRepository;
+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);
+ AccountRepository accountRepository = context.getBean(AccountRepository.class);
+
+ Thread.sleep(20000);
+ }
+}
diff --git a/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/domain/Account.java b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/domain/Account.java
new file mode 100644
index 00000000..2158e0b7
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/domain/Account.java
@@ -0,0 +1,37 @@
+package ru.otus.spring.domain;
+
+public class Account {
+ private String id;
+ private String personId;
+ private Long amount;
+
+ public Account(String id, String personId, Long amount) {
+ this.id = id;
+ this.personId = personId;
+ this.amount = amount;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getPersonId() {
+ return personId;
+ }
+
+ public void setPersonId(String personId) {
+ this.personId = personId;
+ }
+
+ public Long getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Long amount) {
+ this.amount = amount;
+ }
+}
diff --git a/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/domain/Person.java b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..2bdc3894
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/AccountRepository.java b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/AccountRepository.java
new file mode 100644
index 00000000..22bd9275
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/AccountRepository.java
@@ -0,0 +1,8 @@
+package ru.otus.spring.repostory;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import ru.otus.spring.domain.Account;
+import ru.otus.spring.domain.Person;
+
+public interface AccountRepository extends ReactiveMongoRepository {
+}
diff --git a/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java
new file mode 100644
index 00000000..ddd15cef
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/java/ru/otus/spring/repostory/PersonRepository.java
@@ -0,0 +1,15 @@
+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/2021-02/spring-19/spring-19-reactive-spring-data/src/main/resources/application.yml b/2021-02/spring-19/spring-19-reactive-spring-data/src/main/resources/application.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/2021-02/spring-19/spring-19-reactor/pom.xml b/2021-02/spring-19/spring-19-reactor/pom.xml
new file mode 100644
index 00000000..d66f1f55
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactor/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-19-reactor
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+ 13
+ 13
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ io.projectreactor
+ reactor-core
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..cc406c39
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java b/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java
new file mode 100644
index 00000000..e2e67f71
--- /dev/null
+++ b/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/FluxService.java
@@ -0,0 +1,50 @@
+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.Flux;
+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/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/Message.java b/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/Message.java
new file mode 100644
index 00000000..ac58a62c
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/NonFluxService.java b/2021-02/spring-19/spring-19-reactor/src/main/java/ru/otus/spring/reactor/NonFluxService.java
new file mode 100644
index 00000000..1b9be28a
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-web-flux/pom.xml b/2021-02/spring-19/spring-19-web-flux/pom.xml
new file mode 100644
index 00000000..15775369
--- /dev/null
+++ b/2021-02/spring-19/spring-19-web-flux/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-19-web-flux
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+ 13
+ 13
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ io.reactivex.rxjava2
+ rxjava
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..cad1ae76
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/ReactorController.java b/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/ReactorController.java
new file mode 100644
index 00000000..537ead63
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/RxJava2Controller.java b/2021-02/spring-19/spring-19-web-flux/src/main/java/ru/otus/spring/RxJava2Controller.java
new file mode 100644
index 00000000..edf0035f
--- /dev/null
+++ b/2021-02/spring-19/spring-19-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/2021-02/spring-20/.gitignore b/2021-02/spring-20/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2021-02/spring-20/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2021-02/spring-20/pom.xml b/2021-02/spring-20/pom.xml
new file mode 100644
index 00000000..8997be90
--- /dev/null
+++ b/2021-02/spring-20/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20
+ 1.0
+
+ pom
+
+
+ spring-20-exercise
+ spring-20-solution
+
+
diff --git a/2021-02/spring-20/spring-20-exercise/pom.xml b/2021-02/spring-20/spring-20-exercise/pom.xml
new file mode 100644
index 00000000..7fb51891
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20-exercise
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+
+
+ 11
+ 11
+
+
+
+
+
+ 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/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..7b3c42bf
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,69 @@
+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.BodyInserters.fromValue;
+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.*;
+
+@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", accept(APPLICATION_JSON), new PersonHandler(repository)::list)
+ // Обратите внимание на использование pathVariable
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person)))
+ .switchIfEmpty(notFound().build())
+ ).build();
+ }
+
+ // Это пример хэндлера, который даже не бин
+ static class PersonHandler {
+
+ private final PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ // Обратите внимание на пример другого порядка создания response от Flux
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
+
+
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..fc3bded8
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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).delayElements(Duration.ofSeconds(1));
+ }
+
+ @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/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..c6f2df03
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,37 @@
+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);
+ }
+
+ @GetMapping("/person/find")
+ public Flux byName(@RequestParam("name") String name) {
+ return repository.findAllByLastName(name);
+ }
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..2f373b60
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.repository;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@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/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..d1504547
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.rest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@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/2021-02/spring-20/spring-20-solution/pom.xml b/2021-02/spring-20/spring-20-solution/pom.xml
new file mode 100644
index 00000000..60a6d129
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20-solution
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+
+
+ 11
+ 11
+
+
+
+
+
+ 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/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..8bad8f3d
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,82 @@
+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.BodyInserters.fromValue;
+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.*;
+
+@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()
+ // эта функция должна стоять раньше findAll - порядок следования роутов - важен
+ .GET("/func/person", queryParam("name", StringUtils::isNotEmpty),
+ request -> request.queryParam("name")
+ .map(repository::findAllByLastName)
+ .map(persons -> ok().body(persons, Person.class))
+ .orElse(badRequest().build())
+ )
+ // пример другой реализации - начиная с запроса репозитория
+ .GET("/func/person", queryParam("age", StringUtils::isNotEmpty),
+ req -> repository.findAllByLastName(
+ req.queryParam("age").orElseThrow(IllegalArgumentException::new)
+ )
+ .collectList()
+ .flatMap(persons -> ok().body(persons, Person.class)))
+ // Обратите внимание на использование хэндлера
+ .GET("/func/person", accept(APPLICATION_JSON), new PersonHandler(repository)::list)
+ // Обратите внимание на использование pathVariable
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person)))
+ .switchIfEmpty(notFound().build())
+ ).build();
+ }
+
+ // Это пример хэндлера, который даже не бин
+ static class PersonHandler {
+
+ private final PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ // Обратите внимание на пример другого порядка создания response от Flux
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
+
+
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/domain/Person.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..184da872
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,34 @@
+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")
+ .map(String::toUpperCase);
+ }
+
+ @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/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/PersonController.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..2edcaea9
--- /dev/null
+++ b/2021-02/spring-20/spring-20-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/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..2f373b60
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.repository;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@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/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..d1504547
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.rest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@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/2021-02/spring-22/pom.xml b/2021-02/spring-22/pom.xml
new file mode 100644
index 00000000..69a0b0b4
--- /dev/null
+++ b/2021-02/spring-22/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-framework-22-spring-security-start
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.3.RELEASE
+
+
+
+ 11
+ 11
+ 11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-22/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-22/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..5406a277
--- /dev/null
+++ b/2021-02/spring-22/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,12 @@
+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/2021-02/spring-22/src/main/java/ru/otus/spring/rest/PagesController.java b/2021-02/spring-22/src/main/java/ru/otus/spring/rest/PagesController.java
new file mode 100644
index 00000000..1bdf754e
--- /dev/null
+++ b/2021-02/spring-22/src/main/java/ru/otus/spring/rest/PagesController.java
@@ -0,0 +1,32 @@
+package ru.otus.spring.rest;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class PagesController {
+
+ @GetMapping("/")
+ public String indexPage() {
+ return "index";
+ }
+
+ @GetMapping("/public")
+ public String publicPage() {
+ return "public";
+ }
+
+ @GetMapping("/authenticated")
+ public String authenticatedPage() {
+ return "authenticated";
+ }
+
+ @GetMapping("/success")
+ public String successPage(){
+ return "success";
+ }
+}
diff --git a/2021-02/spring-22/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2021-02/spring-22/src/main/java/ru/otus/spring/security/SecurityConfiguration.java
new file mode 100644
index 00000000..cd5b072b
--- /dev/null
+++ b/2021-02/spring-22/src/main/java/ru/otus/spring/security/SecurityConfiguration.java
@@ -0,0 +1,78 @@
+package ru.otus.spring.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+import java.util.Collection;
+
+@EnableWebSecurity
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Override
+ public void configure(WebSecurity web) {
+ web.ignoring()
+ .antMatchers("/")
+ .antMatchers( "/static/**" );
+ }
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ http.csrf().disable()
+ // По умолчанию SecurityContext хранится в сессии. Эта часть вырубает и каждый запросом приходитТ
+// .sessionManagement()
+// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
+// .and()
+ .authorizeRequests()
+ .antMatchers("/public/").anonymous()
+ .and()
+ .authorizeRequests()
+ .antMatchers("/authenticated").authenticated()
+// .and()
+// .authorizeRequests().antMatchers("/public").authenticated()
+ .and()
+ .httpBasic()
+ .and()
+ .anonymous()
+ .principal( "anonymous" )
+ .and()
+ .rememberMe().key( "Some secret" )
+ ;
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+// return new BCryptPasswordEncoder(10);
+ return NoOpPasswordEncoder.getInstance();
+// return new PasswordEncoder() {
+// @Override
+// public String encode(CharSequence charSequence) {
+// return charSequence.toString();
+// }
+//
+// @Override
+// public boolean matches(CharSequence charSequence, String s) {
+// return charSequence.toString().equals(s);
+// }
+// };
+ }
+
+ @Autowired
+ public void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.inMemoryAuthentication()
+ .withUser("admin").password("password").roles("ADMIN")
+ ;
+ }
+}
diff --git a/2021-02/spring-22/src/main/resources/application.yml b/2021-02/spring-22/src/main/resources/application.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/2021-02/spring-22/src/main/resources/templates/authenticated.html b/2021-02/spring-22/src/main/resources/templates/authenticated.html
new file mode 100644
index 00000000..9f8b0d7e
--- /dev/null
+++ b/2021-02/spring-22/src/main/resources/templates/authenticated.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+Только для авторизованных
+
+
diff --git a/2021-02/spring-22/src/main/resources/templates/index.html b/2021-02/spring-22/src/main/resources/templates/index.html
new file mode 100644
index 00000000..f4d11090
--- /dev/null
+++ b/2021-02/spring-22/src/main/resources/templates/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+/public
+
+/authenticated
+
+
diff --git a/2021-02/spring-22/src/main/resources/templates/public.html b/2021-02/spring-22/src/main/resources/templates/public.html
new file mode 100644
index 00000000..77188469
--- /dev/null
+++ b/2021-02/spring-22/src/main/resources/templates/public.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+Доступен всем
+
+
diff --git a/2021-02/spring-22/src/main/resources/templates/success.html b/2021-02/spring-22/src/main/resources/templates/success.html
new file mode 100644
index 00000000..89db5f22
--- /dev/null
+++ b/2021-02/spring-22/src/main/resources/templates/success.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Вы успешно вошли
+
+
+Вы успешно вошли
+
+
diff --git a/2021-02/spring-23/pom.xml b/2021-02/spring-23/pom.xml
new file mode 100644
index 00000000..396cc6f3
--- /dev/null
+++ b/2021-02/spring-23/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-framework-23-auth
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.3.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 11
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+ org.springframework.security
+ spring-security-test
+ ${spring-security.version}
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-23/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-23/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..5406a277
--- /dev/null
+++ b/2021-02/spring-23/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,12 @@
+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/2021-02/spring-23/src/main/java/ru/otus/spring/rest/PagesController.java b/2021-02/spring-23/src/main/java/ru/otus/spring/rest/PagesController.java
new file mode 100644
index 00000000..b7066f8e
--- /dev/null
+++ b/2021-02/spring-23/src/main/java/ru/otus/spring/rest/PagesController.java
@@ -0,0 +1,44 @@
+package ru.otus.spring.rest;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class PagesController {
+
+ @GetMapping("/")
+ public String indexPage() {
+ return "index";
+ }
+
+ @GetMapping("/public")
+ public String publicPage() {
+ SecurityContext securityContext = SecurityContextHolder.getContext();
+ Authentication authentication = securityContext.getAuthentication();
+ System.out.println(authentication.getPrincipal());
+ return "public";
+ }
+
+ @GetMapping("/authenticated")
+ public String authenticatedPage() {
+ SecurityContext securityContext = SecurityContextHolder.getContext();
+// Authentication authentication = securityContext.getAuthentication();
+// UserDetails userDetails = (UserDetails) authentication.getDetails();
+// System.out.println(userDetails.getUsername());
+ return "authenticated";
+ }
+
+ @GetMapping("/success")
+ public String successPage() {
+ return "success";
+ }
+
+ @GetMapping("/error")
+ public String errorPage() {
+ return "error";
+ }
+}
diff --git a/2021-02/spring-23/src/main/java/ru/otus/spring/security/AnonimusUD.java b/2021-02/spring-23/src/main/java/ru/otus/spring/security/AnonimusUD.java
new file mode 100644
index 00000000..b7a31238
--- /dev/null
+++ b/2021-02/spring-23/src/main/java/ru/otus/spring/security/AnonimusUD.java
@@ -0,0 +1,43 @@
+package ru.otus.spring.security;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+
+public class AnonimusUD implements UserDetails {
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return null;
+ }
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+
+ @Override
+ public String getUsername() {
+ return "anonymous";
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/2021-02/spring-23/src/main/java/ru/otus/spring/security/SecurityConfiguration.java b/2021-02/spring-23/src/main/java/ru/otus/spring/security/SecurityConfiguration.java
new file mode 100644
index 00000000..4e5448d3
--- /dev/null
+++ b/2021-02/spring-23/src/main/java/ru/otus/spring/security/SecurityConfiguration.java
@@ -0,0 +1,54 @@
+package ru.otus.spring.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@EnableWebSecurity
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Override
+ public void configure( WebSecurity web ) {
+ web.ignoring().antMatchers( "/" );
+ }
+
+ @Override
+ public void configure( HttpSecurity http ) throws Exception {
+ http.csrf().disable()
+ // По умолчанию SecurityContext хранится в сессии
+ // Это необходимо, чтобы он нигде не хранился
+ // и данные приходили каждый раз с запросом
+// .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS )
+// .and()
+ .authorizeRequests().antMatchers( "/public" ).anonymous()
+ .and()
+ .authorizeRequests().antMatchers( "/authenticated", "/success" ).authenticated()
+
+ .and()
+ // Включает Form-based аутентификацию
+ .formLogin()
+ .passwordParameter( "vk_pass" )
+ .successForwardUrl( "/success" );
+
+// ;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return NoOpPasswordEncoder.getInstance();
+ }
+
+ @Autowired
+ public void configure( AuthenticationManagerBuilder auth ) throws Exception {
+ auth.inMemoryAuthentication()
+ .withUser( "admin" ).password( "password" ).roles( "ADMIN" );
+ }
+}
diff --git a/2021-02/spring-23/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java b/2021-02/spring-23/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java
new file mode 100644
index 00000000..010afbaa
--- /dev/null
+++ b/2021-02/spring-23/src/main/java/ru/otus/spring/security/filter/MyOwnFilter.java
@@ -0,0 +1,17 @@
+package ru.otus.spring.security.filter;
+
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+public class MyOwnFilter extends GenericFilterBean {
+ @Override
+ public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException {
+ servletRequest.getParameterMap().put( "SpecialValue", new String[]{ "My dirty secret" } );
+ filterChain.doFilter( servletRequest, servletResponse );
+ }
+}
diff --git a/2021-02/spring-23/src/main/resources/application.yml b/2021-02/spring-23/src/main/resources/application.yml
new file mode 100644
index 00000000..e0afbd1f
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+logging:
+ level:
+ root: error
\ No newline at end of file
diff --git a/2021-02/spring-23/src/main/resources/templates/authenticated.html b/2021-02/spring-23/src/main/resources/templates/authenticated.html
new file mode 100644
index 00000000..0d2043f0
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/templates/authenticated.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Только для авторизованных
+
+
+Только для авторизованных
+
+
diff --git a/2021-02/spring-23/src/main/resources/templates/error.html b/2021-02/spring-23/src/main/resources/templates/error.html
new file mode 100644
index 00000000..ba4e50a2
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/templates/error.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Упс...
+
+
+Что-то пошло не так. Печалька
+
+
diff --git a/2021-02/spring-23/src/main/resources/templates/index.html b/2021-02/spring-23/src/main/resources/templates/index.html
new file mode 100644
index 00000000..f2d1d1ae
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/templates/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Главная страница
+
+
+/public
+
+/authenticated
+
+
diff --git a/2021-02/spring-23/src/main/resources/templates/public.html b/2021-02/spring-23/src/main/resources/templates/public.html
new file mode 100644
index 00000000..b9f93817
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/templates/public.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Доступен всем
+
+
+Доступен всем
+
+
diff --git a/2021-02/spring-23/src/main/resources/templates/success.html b/2021-02/spring-23/src/main/resources/templates/success.html
new file mode 100644
index 00000000..58414c01
--- /dev/null
+++ b/2021-02/spring-23/src/main/resources/templates/success.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Вы успешно вошли !
+
+
+Вы успешно вошли !
+
+
diff --git a/2021-02/spring-23/src/test/java/ru/otus/spring/rest/PagesControllerTest.java b/2021-02/spring-23/src/test/java/ru/otus/spring/rest/PagesControllerTest.java
new file mode 100644
index 00000000..13a4e4f1
--- /dev/null
+++ b/2021-02/spring-23/src/test/java/ru/otus/spring/rest/PagesControllerTest.java
@@ -0,0 +1,30 @@
+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.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringRunner.class)
+@WebMvcTest(PagesController.class)
+public class PagesControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @WithMockUser(
+ username = "admin",
+ authorities = {"ROLE_ADMIN"}
+ )
+ @Test
+ public void testAuthenticatedOnAdmin() throws Exception {
+ mockMvc.perform(get("/authenticated"))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/2021-03/spring-10-orm/demo-projects/.gitignore b/2021-03/spring-10-orm/demo-projects/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/.gitignore b/2021-03/spring-10-orm/demo-projects/mybatis-demo/.gitignore
new file mode 100644
index 00000000..153c9335
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/.gitignore
@@ -0,0 +1,29 @@
+HELP.md
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+/build/
+
+### VS Code ###
+.vscode/
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/README.md b/2021-03/spring-10-orm/demo-projects/mybatis-demo/README.md
new file mode 100644
index 00000000..44635273
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/README.md
@@ -0,0 +1,2 @@
+# mybatis-demo
+Пример работы с БД через MyBatis
\ No newline at end of file
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/pom.xml b/2021-03/spring-10-orm/demo-projects/mybatis-demo/pom.xml
new file mode 100644
index 00000000..642579ed
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+ ru.otus.example
+ mybatis-demo
+ 0.0.1-SNAPSHOT
+ mybatis-demo
+ MyBatis demo
+
+
+ 11
+ 11
+ 11
+ 2.1.4
+
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ ${mybatis.version}
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java
new file mode 100644
index 00000000..ab5f7513
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/MyBatisDemoApplication.java
@@ -0,0 +1,14 @@
+package ru.otus.example.mybatisdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+
+@SpringBootApplication
+public class MyBatisDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(MyBatisDemoApplication.class, args);
+ }
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java
new file mode 100644
index 00000000..5c8f4728
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Avatar.java
@@ -0,0 +1,13 @@
+package ru.otus.example.mybatisdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Avatar {
+ private long id;
+ private String photoUrl;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java
new file mode 100644
index 00000000..6aa8cff7
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/Course.java
@@ -0,0 +1,13 @@
+package ru.otus.example.mybatisdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Course {
+ private long id;
+ private String name;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java
new file mode 100644
index 00000000..8fa43ebd
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/EMail.java
@@ -0,0 +1,13 @@
+package ru.otus.example.mybatisdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class EMail {
+ private long id;
+ private String email;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java
new file mode 100644
index 00000000..afb324fb
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/models/OtusStudent.java
@@ -0,0 +1,18 @@
+package ru.otus.example.mybatisdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OtusStudent {
+ private long id;
+ private String name;
+ private Avatar avatar;
+ private List emails;
+ private List courses;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java
new file mode 100644
index 00000000..634d1aba
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/AvatarRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.mybatisdemo.repositories;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Result;
+import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Select;
+import ru.otus.example.mybatisdemo.models.Avatar;
+
+@Mapper
+public interface AvatarRepository {
+ @Select("select * from avatars where id = #{id}")
+ @Results(value = {
+ @Result(property = "id", column = "id"),
+ @Result(property = "photoUrl", column = "photo_url")
+ })
+ Avatar getAvatarById(long id);
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java
new file mode 100644
index 00000000..20271981
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/CourseRepository.java
@@ -0,0 +1,17 @@
+package ru.otus.example.mybatisdemo.repositories;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+import ru.otus.example.mybatisdemo.models.Course;
+
+import java.util.List;
+
+@Mapper
+public interface CourseRepository {
+
+ @Select("select * " +
+ "from student_courses sc left join courses c on sc.course_id = c.id " +
+ "where sc.student_id = #{studentId}")
+ List getCoursesByStudentId(long studentId);
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java
new file mode 100644
index 00000000..989b681d
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/EmailRepository.java
@@ -0,0 +1,14 @@
+package ru.otus.example.mybatisdemo.repositories;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+import ru.otus.example.mybatisdemo.models.EMail;
+
+import java.util.List;
+
+@Mapper
+public interface EmailRepository {
+
+ @Select("select * from emails where student_id = #{studentId}")
+ List getEmailsByStudentId(long studentId);
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..82a8260b
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,42 @@
+package ru.otus.example.mybatisdemo.repositories;
+
+import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.mapping.FetchType;
+import ru.otus.example.mybatisdemo.models.Avatar;
+import ru.otus.example.mybatisdemo.models.OtusStudent;
+
+import java.util.List;
+
+@Mapper
+public interface OtusStudentRepository {
+
+ @Select("select * from otus_students")
+ @Results(id = "studentAllMap", value = {
+ @Result(property = "id", column = "id"),
+ @Result(property = "name", column = "name"),
+ @Result(property = "avatar", column = "avatar_id", javaType = Avatar.class,
+ one = @One(select = "ru.otus.example.mybatisdemo.repositories.AvatarRepository.getAvatarById", fetchType = FetchType.EAGER)),
+ @Result(property = "emails", column = "id", javaType = List.class,
+ many = @Many(select = "ru.otus.example.mybatisdemo.repositories.EmailRepository.getEmailsByStudentId", fetchType = FetchType.EAGER)),
+ @Result(property = "courses", column = "id", javaType = List.class,
+ many = @Many(select = "ru.otus.example.mybatisdemo.repositories.CourseRepository.getCoursesByStudentId", fetchType = FetchType.EAGER))
+ })
+ List findAllWithAllInfo();
+
+ @Select("select * from otus_students where id = #{id}")
+ @ResultMap("studentAllMap")
+ OtusStudent findById(long id);
+
+ @Select("select count(*) as students_count from otus_students")
+ long getStudentsCount();
+
+ @Insert("insert into otus_students(name, avatar_id) values (#{name}, #{avatar.id})")
+ void insert(OtusStudent student);
+
+ @Update("update otus_students set name = #{name} where id = #{id}")
+ void updateName(OtusStudent student);
+
+ @Delete("delete from otus_students where id = #{id}")
+ void deleteById(long id);
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java
new file mode 100644
index 00000000..d9a24bce
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/java/ru/otus/example/mybatisdemo/repositories/OtusStudentRepositoryTest.java
@@ -0,0 +1,110 @@
+package ru.otus.example.mybatisdemo.repositories;
+
+import lombok.val;
+import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.mybatisdemo.models.Avatar;
+import ru.otus.example.mybatisdemo.models.OtusStudent;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе MyBatis для работы со студентами ")
+@SpringBootTest
+@Transactional
+public class OtusStudentRepositoryTest {
+
+ private static final String FIELD_ID = "id";
+ private static final String FIELD_PHOTO_URL = "photoUrl";
+ private static final String FIELD_NAME = "name";
+
+ private static final long FIRST_STUDENT_ID = 1L;
+ private static final long FIRST_AVATAR_ID = 1L;
+ private static final String FIRST_STUDENT_NAME = "student_01";
+ private static final String FIRST_AVATAR_URL = "photoUrl_01";
+ private static final String STUDENT_NEW_NAME = "Висусуалий";
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long INSERTED_STUDENT_ID = 11L;
+ private static final int EXPECTED_EMAILS_COUNT = 2;
+ private static final int EXPECTED_COURSES_COUNT = 3;
+
+ @Autowired
+ private OtusStudentRepository studentRepositoryMyBatis;
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ val students = studentRepositoryMyBatis.findAllWithAllInfo();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ }
+
+ @DisplayName("должен загружать число студентов в БД")
+ @Test
+ void shouldReturnCorrectStudentsCount() {
+ long studentsCount = studentRepositoryMyBatis.getStudentsCount();
+ assertThat(studentsCount).isEqualTo(EXPECTED_NUMBER_OF_STUDENTS);
+ }
+
+ @DisplayName(" должен загружать информацию о нужном студенте")
+ @Test
+ void shouldFindExpectedStudentById(){
+ val actualStudent = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID);
+
+ assertThat(actualStudent).isNotNull();
+ assertThat(actualStudent.getName()).isEqualTo(FIRST_STUDENT_NAME);
+ assertThat(actualStudent.getAvatar()).isNotNull()
+ .hasFieldOrPropertyWithValue(FIELD_ID, FIRST_STUDENT_ID)
+ .hasFieldOrPropertyWithValue(FIELD_PHOTO_URL, FIRST_AVATAR_URL);
+ assertThat(actualStudent.getEmails()).isNotNull().hasSize(EXPECTED_EMAILS_COUNT);
+ assertThat(actualStudent.getCourses()).isNotNull().hasSize(EXPECTED_COURSES_COUNT);
+ }
+
+ @DisplayName(" должен сохранить, а потом загрузить информацию о нужном студенте")
+ @Test
+ void shouldSaveAndLoadCorrectStudent() {
+ val expectedStudent = new OtusStudent(0, STUDENT_NEW_NAME,
+ new Avatar(FIRST_AVATAR_ID, FIRST_AVATAR_URL), List.of(), List.of());
+ studentRepositoryMyBatis.insert(expectedStudent);
+ val actualStudent = studentRepositoryMyBatis.findById(INSERTED_STUDENT_ID);
+
+ assertThat(actualStudent)
+ .isNotNull()
+ .usingRecursiveComparison(
+ RecursiveComparisonConfiguration.builder()
+ .withIgnoredFields(FIELD_ID).build())
+ .isEqualTo(expectedStudent);
+ }
+
+
+ @DisplayName(" должен обновлять имя студента в БД")
+ @Test
+ void shouldUpdateStudentName() {
+ val student = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID);
+ student.setName(STUDENT_NEW_NAME);
+ studentRepositoryMyBatis.updateName(student);
+ val actualStudent = studentRepositoryMyBatis.findById(FIRST_STUDENT_ID);
+
+ assertThat(actualStudent).isNotNull().hasFieldOrPropertyWithValue(FIELD_NAME, student.getName());
+ }
+
+ @DisplayName("должен удалять студента из БД по id")
+ @Test
+ void shouldDeleteStudentFromDbById() {
+ val studentsCountBefore = studentRepositoryMyBatis.getStudentsCount();
+ studentRepositoryMyBatis.deleteById(FIRST_STUDENT_ID);
+ val studentsCountAfter = studentRepositoryMyBatis.getStudentsCount();
+
+ assertThat(studentsCountBefore - studentsCountAfter).isEqualTo(1);
+ }
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml
new file mode 100644
index 00000000..dc237b00
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/application.yml
@@ -0,0 +1,8 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ initialization-mode: always
+
+logging:
+ level:
+ ru.otus.example.mybatisdemo.repositories: TRACE
\ No newline at end of file
diff --git a/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/mybatis-demo/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2021-03/spring-10-orm/demo-projects/pom.xml b/2021-03/spring-10-orm/demo-projects/pom.xml
new file mode 100644
index 00000000..711fadf0
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ demo-projects
+ 1.0
+
+ pom
+
+
+ spring-jdbc-demo
+ mybatis-demo
+
+
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore
new file mode 100644
index 00000000..153c9335
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/.gitignore
@@ -0,0 +1,29 @@
+HELP.md
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+/build/
+
+### VS Code ###
+.vscode/
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/README.md b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/README.md
new file mode 100644
index 00000000..462216c3
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/README.md
@@ -0,0 +1,2 @@
+# spring-jdbc-demo
+Пример работы с БД через jdbc
\ No newline at end of file
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml
new file mode 100644
index 00000000..450b4982
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+ ru.otus.example
+ spring-jdbc-demo
+ 0.0.1-SNAPSHOT
+ spring-jdbc-demo
+ Spring jdbc demo
+
+
+ 11
+ 11
+ 11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java
new file mode 100644
index 00000000..28ebfec4
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/SpringJdbcDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springjdbcdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringJdbcDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringJdbcDemoApplication.class, args);
+ }
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java
new file mode 100644
index 00000000..a1963ea4
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Avatar.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springjdbcdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Avatar {
+ private long id;
+ private String photoUrl;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java
new file mode 100644
index 00000000..07b6e2c2
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/Course.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springjdbcdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Course {
+ private long id;
+ private String name;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java
new file mode 100644
index 00000000..16985ad5
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/EMail.java
@@ -0,0 +1,13 @@
+package ru.otus.example.springjdbcdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class EMail {
+ private long id;
+ private String email;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java
new file mode 100644
index 00000000..5ae9167c
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/models/OtusStudent.java
@@ -0,0 +1,18 @@
+package ru.otus.example.springjdbcdemo.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OtusStudent {
+ private long id;
+ private String name;
+ private Avatar avatar;
+ private List emails;
+ private List courses;
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java
new file mode 100644
index 00000000..716ab880
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbc.java
@@ -0,0 +1,9 @@
+package ru.otus.example.springjdbcdemo.repositories;
+
+import ru.otus.example.springjdbcdemo.models.Course;
+
+import java.util.List;
+
+public interface CourseRepositoryJdbc {
+ List findAllUsed();
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbcImpl.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbcImpl.java
new file mode 100644
index 00000000..53915e14
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/CourseRepositoryJdbcImpl.java
@@ -0,0 +1,36 @@
+package ru.otus.example.springjdbcdemo.repositories;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Repository;
+import ru.otus.example.springjdbcdemo.models.Course;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class CourseRepositoryJdbcImpl implements CourseRepositoryJdbc {
+
+ @Autowired
+ private final JdbcOperations op;
+
+ @Override
+ public List findAllUsed() {
+ return op.query("select c.id, c.name " +
+ "from courses c inner join student_courses sc on c.id = sc.course_id " +
+ "group by c.id, c.name " +
+ "order by c.name", new CourseRowMapper());
+ }
+
+ private static class CourseRowMapper implements RowMapper {
+ @Override
+ public Course mapRow(ResultSet rs, int i) throws SQLException {
+ return new Course(rs.getLong(1), rs.getString(2));
+ }
+ }
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java
new file mode 100644
index 00000000..0761373d
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbc.java
@@ -0,0 +1,9 @@
+package ru.otus.example.springjdbcdemo.repositories;
+
+import ru.otus.example.springjdbcdemo.models.OtusStudent;
+
+import java.util.List;
+
+public interface OtusStudentRepositoryJdbc {
+ List findAllWithAllInfo();
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcImpl.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcImpl.java
new file mode 100644
index 00000000..571b4f21
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/OtusStudentRepositoryJdbcImpl.java
@@ -0,0 +1,52 @@
+package ru.otus.example.springjdbcdemo.repositories;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.stereotype.Repository;
+import ru.otus.example.springjdbcdemo.models.Course;
+import ru.otus.example.springjdbcdemo.models.OtusStudent;
+import ru.otus.example.springjdbcdemo.repositories.ext.OtusStudentResultSetExtractor;
+import ru.otus.example.springjdbcdemo.repositories.ext.StudentCourseRelation;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Repository
+@RequiredArgsConstructor
+public class OtusStudentRepositoryJdbcImpl implements OtusStudentRepositoryJdbc {
+
+ private final CourseRepositoryJdbc courseRepository;
+ private final JdbcOperations op;
+
+ @Override
+ public List findAllWithAllInfo() {
+ List courses = courseRepository.findAllUsed();
+ List relations = getAllRelations();
+ Map students =
+ op.query("select os.id, os.name, a.id avatar_id, a.photo_url, e.id email_id, e.email " +
+ "from (otus_students os left join avatars a on " +
+ "os.avatar_id = a.id) left join emails e on os.id = e.student_id",
+ new OtusStudentResultSetExtractor());
+
+ mergeStudentsInfo(students, courses, relations);
+ return new ArrayList<>(Objects.requireNonNull(students).values());
+ }
+
+ private List getAllRelations() {
+ return op.query("select student_id, course_id from student_courses sc order by student_id, course_id",
+ (rs, i) -> new StudentCourseRelation(rs.getLong(1), rs.getLong(2)));
+ }
+
+ private void mergeStudentsInfo(Map students, List courses,
+ List relations) {
+ Map coursesMap = courses.stream().collect(Collectors.toMap(Course::getId, Function.identity()));
+ relations.forEach(r -> {
+ if (students.containsKey(r.getStudentId()) && coursesMap.containsKey(r.getCourseId())) {
+ students.get(r.getStudentId()).getCourses().add(coursesMap.get(r.getCourseId()));
+ }
+ });
+ }
+
+
+}
diff --git a/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java
new file mode 100644
index 00000000..778a46b7
--- /dev/null
+++ b/2021-03/spring-10-orm/demo-projects/spring-jdbc-demo/src/main/java/ru/otus/example/springjdbcdemo/repositories/ext/OtusStudentResultSetExtractor.java
@@ -0,0 +1,37 @@
+package ru.otus.example.springjdbcdemo.repositories.ext;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.ResultSetExtractor;
+import ru.otus.example.springjdbcdemo.models.Avatar;
+import ru.otus.example.springjdbcdemo.models.EMail;
+import ru.otus.example.springjdbcdemo.models.OtusStudent;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OtusStudentResultSetExtractor implements
+ ResultSetExtractor