Merge branch 'master' of github.com:OtusTeam/Spring

This commit is contained in:
Yuriy Dvorzhetskiy
2021-05-28 22:39:50 +06:00
411 changed files with 12466 additions and 5 deletions
+7
View File
@@ -0,0 +1,7 @@
.idea/
*.iml
target/
/node_modules
/output
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-18</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,35 @@
package ru.otus;
import io.reactivex.Observable;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class CreateExamples {
public static void main(String[] args) {
Observable<String> obs = justExample();
obs.forEach(System.out::println);
obs.forEach(System.out::println);
}
public static Observable<String> justExample() {
return Observable.just("one", "two", "three");
}
public static Observable<String> 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<String> deferExample() {
return Observable.defer(() -> Observable.just("one", "two", "three"));
}
}
@@ -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<String> getName() {
return Observable.just("Jake");
}
static Observable<String> getSurname() {
return Observable.just("Foo");
}
static Observable<String> save(String fullName) {
System.out.println(fullName + " saved!");
return Observable.just("OK!");
}
}
@@ -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<Person> 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<String> 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<String> magicPublisher() {
Random r = new Random(1);
AtomicInteger i = new AtomicInteger();
final Observable<String> obs = Observable.<String>generate(emitter ->
emitter.onNext("" + i.incrementAndGet()))
.concatMap(s -> Observable.just(s).delay(r.nextInt(1000), TimeUnit.MILLISECONDS))
.subscribeOn(Schedulers.newThread());
PublishSubject<String> subject = PublishSubject.create();
// BehaviorSubject<String> subject = BehaviorSubject.create();
// AsyncSubject<String> subject = AsyncSubject.create();
// CompletableFuture.runAsync(() -> {
// try {
// Thread.sleep(7000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// subject.onComplete();
// });
// ReplaySubject<String> subject = ReplaySubject.create();
obs.subscribe(subject);
return subject;
}
//composeExmaple
private static ObservableTransformer<String, String> filterAndUpperCase() {
return upstream -> upstream
.filter(s -> s.length() >= 4)
.map(String::toUpperCase);
}
}
@@ -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);
}
}
@@ -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<String> obs = controller();
obs.subscribe(System.out::println);
System.out.println("Wait time " + (System.currentTimeMillis() - timeStarted));
System.in.read();
}
static Observable<String> controller() {
return service();
}
static Observable<String> service() {
return repository();
}
static Observable<String> repository() {
return database();
}
static Observable<String> 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());
}
}
@@ -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";
}
}
+24
View File
@@ -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/
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-19</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>spring-19-web-flux</module>
<module>spring-19-reactor</module>
<module>spring-19-reactive-spring-data</module>
</modules>
</project>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-19-reactive-spring-data</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.target>13</maven.compiler.target>
<maven.compiler.source>13</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<Account, String> {
}
@@ -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<Person, String> {
Flux<Person> findByName(String name);
@Query("{ 'name': ?0 }")
Mono<Person> findFirstByName(String name);
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-19-reactor</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.target>13</maven.compiler.target>
<maven.compiler.source>13</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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");
}
}
@@ -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<Message> 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());
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-19-web-flux</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.target>13</maven.compiler.target>
<maven.compiler.source>13</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- работает и без неё, но нужна нам, для RxJava методов -->
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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<String> one() {
return Mono.just("one");
}
@GetMapping("/flux/ten")
public Flux<Integer> list() {
return Flux.range(1, 10);
}
@GetMapping(path = "/flux/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.generate(() -> 0, (state, emitter) -> {
emitter.next(state);
return state + 1;
})
.delayElements(Duration.ofSeconds(1L))
.map(Object::toString);
}
}
@@ -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<String> single() {
return Single.just("one");
}
@GetMapping("/rx/ten")
public Flowable<Integer> list() {
return Flowable.range(1, 10);
}
}
+4
View File
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-20</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>spring-20-exercise</module>
<module>spring-20-solution</module>
</modules>
</project>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-20-exercise</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- Зависимости WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Зависимости Reactive SpringData -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
<!-- Тестирование -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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<ServerResponse> 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<ServerResponse> list(ServerRequest request) {
// Обратите внимание на пример другого порядка создания response от Flux
return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
}
}
}
@@ -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;
}
}
@@ -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<Person, String> {
Flux<Person> findAll();
Mono<Person> findById(String id);
Mono<Person> save(Mono<Person> person);
Flux<Person> findAllByLastName(String lastName);
Flux<Person> findAllByAge(int age);
}
@@ -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<String> one() {
return Mono.just("one");
}
@GetMapping("/flux/ten")
public Flux<Integer> list() {
return Flux.range(1, 10).delayElements(Duration.ofSeconds(1));
}
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.generate(() -> 0, (state, emitter) -> {
emitter.next(state);
return state + 1;
})
.delayElements(Duration.ofSeconds(1L))
.map(i -> "" + i);
}
}
@@ -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<Person> all() {
return repository.findAll();
}
@GetMapping("/person/{id}")
public Mono<Person> byId(@PathVariable("id") String id) {
return repository.findById(id);
}
@PostMapping("/person")
public Mono<Person> save(@RequestBody Mono<Person> dto) {
return repository.save(dto);
}
@GetMapping("/person/find")
public Flux<Person> byName(@RequestParam("name") String name) {
return repository.findAllByLastName(name);
}
}
@@ -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<Person> personMono = repository.save(new Person("Bill", 12));
StepVerifier
.create(personMono)
.assertNext(person -> assertNotNull(person.getId()))
.expectComplete()
.verify();
}
}
@@ -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<ServerResponse> route;
@Test
public void testRoute() {
WebTestClient client = WebTestClient
.bindToRouterFunction(route)
.build();
client.get()
.uri("/func/person")
.exchange()
.expectStatus()
.isOk();
}
}
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-20-solution</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- Зависимости WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Зависимости Reactive SpringData -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
<!-- Тестирование -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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<ServerResponse> 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<ServerResponse> list(ServerRequest request) {
// Обратите внимание на пример другого порядка создания response от Flux
return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
}
}
}
@@ -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;
}
}
@@ -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<Person, String> {
Flux<Person> findAll();
Mono<Person> findById(String id);
Mono<Person> save(Mono<Person> person);
Flux<Person> findAllByLastName(String lastName);
Flux<Person> findAllByAge(int age);
}
@@ -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<String> one() {
return Mono.just("one")
.map(String::toUpperCase);
}
@GetMapping("/flux/ten")
public Flux<Integer> list() {
return Flux.range(1, 10);
}
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.generate(() -> 0, (state, emitter) -> {
emitter.next(state);
return state + 1;
})
.delayElements(Duration.ofSeconds(1L))
.map(i -> "" + i);
}
}
@@ -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<Person> all() {
return repository.findAll();
}
@GetMapping("/person/{id}")
public Mono<Person> byId(@PathVariable("id") String id) {
return repository.findById(id);
}
@GetMapping("/person/byname")
public Flux<Person> byName(@RequestParam("name") String lastName) {
return repository.findAllByLastName(lastName);
}
@GetMapping("/person/byage")
public Flux<Person> byAge(@RequestParam int age) {
return repository.findAllByAge(age);
}
@PostMapping("/person")
public Mono<Person> save(@RequestBody Mono<Person> dto) {
return repository.save(dto);
}
}
@@ -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<Person> personMono = repository.save(new Person("Bill", 12));
StepVerifier
.create(personMono)
.assertNext(person -> assertNotNull(person.getId()))
.expectComplete()
.verify();
}
}
@@ -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<ServerResponse> route;
@Test
public void testRoute() {
WebTestClient client = WebTestClient
.bindToRouterFunction(route)
.build();
client.get()
.uri("/func/person")
.exchange()
.expectStatus()
.isOk();
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-framework-22-spring-security-start</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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";
}
}
@@ -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")
;
}
}
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
</head>
<body>
Только для авторизованных
</body>
</html>
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
</head>
<body>
<a th:href="@{public}">/public</a>
<br>
<a th:href="@{authenticated}">/authenticated</a>
</body>
</html>
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
</head>
<body>
Доступен всем
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Вы успешно вошли</title>
</head>
<body>
Вы успешно вошли
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>spring-framework-23-auth</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- For Spring Security testing -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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";
}
}
@@ -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;
}
}
@@ -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" );
}
}
@@ -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 );
}
}
@@ -0,0 +1,3 @@
logging:
level:
root: error
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Только для авторизованных</title>
</head>
<body>
Только для авторизованных
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Упс...</title>
</head>
<body>
Что-то пошло не так. Печалька
</body>
</html>
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Главная страница</title>
</head>
<body>
<a th:href="@{public}">/public</a>
<br>
<a th:href="@{authenticated}">/authenticated</a>
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Доступен всем</title>
</head>
<body>
Доступен всем
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Вы успешно вошли !</title>
</head>
<body>
Вы успешно вошли !
</body>
</html>
@@ -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());
}
}
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -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/
@@ -0,0 +1,2 @@
# mybatis-demo
Пример работы с БД через MyBatis
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>ru.otus.example</groupId>
<artifactId>mybatis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-demo</name>
<description>MyBatis demo</description>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<mybatis.version>2.1.4</mybatis.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<EMail> emails;
private List<Course> courses;
}
@@ -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);
}
@@ -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<Course> getCoursesByStudentId(long studentId);
}
@@ -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<EMail> getEmailsByStudentId(long studentId);
}
@@ -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<OtusStudent> 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);
}
@@ -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)
);
@@ -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);
}
}
@@ -0,0 +1,8 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
initialization-mode: always
logging:
level:
ru.otus.example.mybatisdemo.repositories: TRACE
@@ -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);
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>demo-projects</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>spring-jdbc-demo</module>
<module>mybatis-demo</module>
</modules>
</project>
@@ -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/
@@ -0,0 +1,2 @@
# spring-jdbc-demo
Пример работы с БД через jdbc
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>ru.otus.example</groupId>
<artifactId>spring-jdbc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-jdbc-demo</name>
<description>Spring jdbc demo</description>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<EMail> emails;
private List<Course> courses;
}
@@ -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<Course> findAllUsed();
}
@@ -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<Course> 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<Course> {
@Override
public Course mapRow(ResultSet rs, int i) throws SQLException {
return new Course(rs.getLong(1), rs.getString(2));
}
}
}
@@ -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<OtusStudent> findAllWithAllInfo();
}
@@ -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<OtusStudent> findAllWithAllInfo() {
List<Course> courses = courseRepository.findAllUsed();
List<StudentCourseRelation> relations = getAllRelations();
Map<Long, OtusStudent> 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<StudentCourseRelation> 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<Long, OtusStudent> students, List<Course> courses,
List<StudentCourseRelation> relations) {
Map<Long, Course> 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()));
}
});
}
}
@@ -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<Map<Long, OtusStudent>> {
@Override
public Map<Long, OtusStudent> extractData(ResultSet rs) throws SQLException,
DataAccessException {
Map<Long, OtusStudent> students = new HashMap<>();
while (rs.next()) {
long id = rs.getLong("id");
OtusStudent student = students.get(id);
if (student == null) {
student = new OtusStudent(id, rs.getString("name"),
new Avatar(rs.getLong("avatar_id"), rs.getString("photo_url")),
new ArrayList<>(), new ArrayList<>());
students.put(student.getId(), student);
}
student.getEmails().add(new EMail(rs.getLong("email_id"),
rs.getString("email")));
}
return students;
}
}
@@ -0,0 +1,11 @@
package ru.otus.example.springjdbcdemo.repositories.ext;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class StudentCourseRelation {
private final long studentId;
private final long courseId;
}
@@ -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)
);
@@ -0,0 +1,35 @@
package ru.otus.example.springjdbcdemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jdbc для работы со студентами ")
@JdbcTest
@Import({OtusStudentRepositoryJdbcImpl.class, CourseRepositoryJdbcImpl.class})
class OtusStudentRepositoryJdbcImplTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
@Autowired
private OtusStudentRepositoryJdbcImpl repositoryJdbc;
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
val students = repositoryJdbc.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);
students.forEach(System.out::println);
}
}

Some files were not shown because too many files have changed in this diff Show More