Compare commits
40 Commits
add-new-bo
...
reservatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
727481a520 | ||
|
|
5f4809cfd4 | ||
|
|
c8b8c26d80 | ||
|
|
9e5ba16a1c | ||
|
|
3f07eac529 | ||
|
|
a234301f1d | ||
|
|
14441bfbd6 | ||
|
|
648b877b86 | ||
|
|
fbeac054cd | ||
|
|
6435c351f9 | ||
|
|
c9de6efc28 | ||
|
|
fa0c0fe4f4 | ||
|
|
672c6f6557 | ||
|
|
d9b68daa94 | ||
|
|
1c59f79cde | ||
|
|
d2f52f849d | ||
|
|
6f998cf030 | ||
|
|
f613a67517 | ||
|
|
8bea3936b7 | ||
|
|
bebd23d22b | ||
|
|
4b28fe3460 | ||
|
|
b3378b8c43 | ||
|
|
246071b743 | ||
|
|
4b2f22307a | ||
|
|
b06d8219a4 | ||
|
|
05b4c9e270 | ||
|
|
fe0dd60540 | ||
|
|
4b1fa6048d | ||
|
|
3c4527fff4 | ||
|
|
d325e9196e | ||
|
|
3f57a0c9c4 | ||
|
|
4b9a0b1987 | ||
|
|
442bf870c3 | ||
|
|
83f947b70b | ||
|
|
8a66e987ad | ||
|
|
dc3730f1c9 | ||
|
|
9cd190deb7 | ||
|
|
f450176b20 | ||
|
|
2eb6483aba | ||
|
|
022046139c |
34
.github/workflows/master.yaml
vendored
34
.github/workflows/master.yaml
vendored
@@ -35,4 +35,36 @@ jobs:
|
||||
- name: SonarCloud Scan
|
||||
run: mvn -B clean verify -Psonar,component-test -Dsonar.login=${{ secrets.SONAR_TOKEN }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
artifact:
|
||||
name: Publish - GitHub Packages
|
||||
runs-on: ubuntu-18.04
|
||||
needs: [test, sonar]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11.0.4
|
||||
- name: Publish artifact on GitHub Packages
|
||||
run: mvn -B clean deploy -DskipTests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker:
|
||||
name: Publish - Docker Hub
|
||||
runs-on: ubuntu-18.04
|
||||
needs: [test, sonar]
|
||||
env:
|
||||
REPO: ${{ secrets.DOCKER_REPO }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Login to Docker Hub
|
||||
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
|
||||
- name: Build Docker image
|
||||
run: docker build -t $REPO:latest -t $REPO:${GITHUB_SHA::8} .
|
||||
- name: Publish Docker image
|
||||
run: docker push $REPO
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
sendgrid.env
|
||||
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM maven:3.6.3-jdk-11-slim AS build
|
||||
RUN mkdir -p /workspace
|
||||
WORKDIR /workspace
|
||||
COPY pom.xml /workspace
|
||||
COPY src /workspace/src
|
||||
RUN mvn -B -f pom.xml clean package -DskipTests
|
||||
|
||||
FROM openjdk:11-jdk-slim
|
||||
COPY --from=build /workspace/target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
28
README.md
28
README.md
@@ -5,7 +5,33 @@
|
||||
|
||||
This is a small application that provides basic REST endpoints for managing library (add new book, reserve, borrow it, etc.).
|
||||
|
||||
The technology behind it:
|
||||
* Java 11
|
||||
* Postgres
|
||||
* Spring Boot
|
||||
|
||||
## Installing / Getting started
|
||||
|
||||
Description in future
|
||||
#### Using `docker-compose`
|
||||
|
||||
In the terminal run the following command:
|
||||
```console
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
#### Using Maven
|
||||
|
||||
First make sure that you adjust the configuration file - `src/main/resources/application.yml` with connection details to your database.
|
||||
|
||||
Then, in the terminal run the following command:
|
||||
```console
|
||||
$ mvn clean package
|
||||
$ mvn spring-boot:run
|
||||
```
|
||||
|
||||
#### Inside IntelliJ (with H2 or Postgres database)
|
||||
|
||||
First configure how you run the `LibraryHexagonalApplication.java` by adding `--spring.profiles.active=h2` (for H2 database) or `--spring.profiles.active=postgres` (for Postgres database) as a **Program argument**.
|
||||
|
||||
Then just run the `LibraryHexagonalApplication.java` class so it will use H2 database (you don't need to have postgres database up and running).
|
||||
|
||||
|
||||
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: "postgres:9.6-alpine"
|
||||
container_name: postgres
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
- POSTGRES_SERVER=postgres
|
||||
- POSTGRES_DB=library
|
||||
- POSTGRES_USER=library
|
||||
- POSTGRES_PASSWORD=library
|
||||
|
||||
library:
|
||||
build: .
|
||||
container_name: library-app
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- POSTGRES_SERVER=postgres
|
||||
- POSTGRES_DB=library
|
||||
- POSTGRES_USER=library
|
||||
- POSTGRES_PASSWORD=library
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
41
pom.xml
41
pom.xml
@@ -20,7 +20,9 @@
|
||||
<spring-cloud.version>Hoxton.M3</spring-cloud.version>
|
||||
<surefire.and.failsafe.report.dir>target/test-report</surefire.and.failsafe.report.dir>
|
||||
<code.coverage.exclusions>
|
||||
**/io/wkrzywiec/hexagonal/library/domain/book/dto/**,
|
||||
**/io/wkrzywiec/hexagonal/library/borrowing/model/**,
|
||||
**/io/wkrzywiec/hexagonal/library/email/model/**,
|
||||
**/io/wkrzywiec/hexagonal/library/inventory/model/**,
|
||||
**/io/wkrzywiec/hexagonal/library/infrastructure/repository/*Entity.java
|
||||
</code.coverage.exclusions>
|
||||
</properties>
|
||||
@@ -50,6 +52,16 @@
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-jaxb-annotations</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr</artifactId>
|
||||
<version>0.10.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
@@ -57,6 +69,21 @@
|
||||
<scope>runtime</scope>
|
||||
<version>1.4.200</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.2.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>3.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sendgrid</groupId>
|
||||
<artifactId>sendgrid-java</artifactId>
|
||||
<version>4.4.8</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
@@ -106,6 +133,10 @@
|
||||
</executions>
|
||||
<configuration>
|
||||
<reportsDirectory>${surefire.and.failsafe.report.dir}</reportsDirectory>
|
||||
<includes>
|
||||
<include>**/*ITCase.java</include>
|
||||
<include>**/*ComponentTest.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -232,4 +263,12 @@
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>github</id>
|
||||
<name>Library REST application (hexagonal)</name>
|
||||
<url>https://maven.pkg.github.com/wkrzywiec/library-hexagonal</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.boot.web.server.LocalServerPort;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class MakeReservationComponentTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private BookRepository bookRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String baseURL;
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
this.baseURL = "http://localhost:" + port;
|
||||
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reserve available book")
|
||||
@Sql({"/book-and-user.sql", "/available-book.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenBookIsAvailable_thenMakeReservation_thenBookIsReserved() {
|
||||
//given
|
||||
Long homoDeusBookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
Long activeUserId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM user WHERE email = ?",
|
||||
Long.class,
|
||||
"john.doe@test.com");
|
||||
|
||||
BookReservationCommand reservationCommand =
|
||||
BookReservationCommand.builder()
|
||||
.bookId(homoDeusBookId )
|
||||
.userId(activeUserId)
|
||||
.build();
|
||||
|
||||
//when
|
||||
given()
|
||||
.contentType("application/json")
|
||||
.body(reservationCommand)
|
||||
.when()
|
||||
.post( baseURL + "/reservations")
|
||||
.prettyPeek()
|
||||
.then();
|
||||
|
||||
Long reservationId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM reserved WHERE book_id = ?",
|
||||
Long.class,
|
||||
homoDeusBookId);
|
||||
|
||||
assertTrue(reservationId > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package io.wkrzywiec.hexagonal.library.application;
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.ValidatableResponse;
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.ExternalBookIdDTO;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.repository.BookEntity;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -13,15 +11,16 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class AddNewBookTest {
|
||||
public class AddNewBookComponentTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
@@ -55,29 +54,37 @@ public class AddNewBookTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Add new book to a database")
|
||||
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved(){
|
||||
@DisplayName("Add new book to a database & make it available")
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved() {
|
||||
//given
|
||||
BookDetailsDTO homoDeusBookDetails = TestData.homoDeusBookDetailsDTO();
|
||||
ExternalBookIdDTO googleBookId =
|
||||
ExternalBookIdDTO.builder()
|
||||
.value(homoDeusBookDetails.getBookExternalId())
|
||||
AddNewBookCommand addNewBookCommand =
|
||||
AddNewBookCommand.builder()
|
||||
.googleBookId(TestData.homoDeusBookGoogleId())
|
||||
.build();
|
||||
|
||||
//when
|
||||
ValidatableResponse response =
|
||||
given()
|
||||
.contentType("application/json")
|
||||
.body(googleBookId)
|
||||
.when()
|
||||
.post( baseURL + "/books")
|
||||
.prettyPeek()
|
||||
.then();
|
||||
given()
|
||||
.contentType("application/json")
|
||||
.body(addNewBookCommand)
|
||||
.when()
|
||||
.post( baseURL + "/books")
|
||||
.prettyPeek()
|
||||
.then();
|
||||
|
||||
//then
|
||||
String homoDeusSql = "select * from book where book_external_id = '" + homoDeusBookDetails.getBookExternalId() + "'";
|
||||
BookEntity savedBook = (BookEntity) jdbc.queryForObject(homoDeusSql, new BeanPropertyRowMapper(BookEntity.class));
|
||||
assertEquals(homoDeusBookDetails.getTitle(), savedBook.getTitle());
|
||||
assertEquals(homoDeusBookDetails.getTitle(), savedBook.getTitle());
|
||||
Long savedBookId = jdbc.queryForObject(
|
||||
"SELECT id FROM book WHERE book_external_id = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookGoogleId());
|
||||
|
||||
assertTrue(savedBookId > 0);
|
||||
|
||||
Long availableBookId = jdbc.queryForObject(
|
||||
"SELECT id FROM available WHERE book_id = ?",
|
||||
Long.class,
|
||||
savedBookId);
|
||||
|
||||
assertTrue(availableBookId > 0);
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,4 @@ spring:
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password: password
|
||||
jpa.database-platform: org.hibernate.dialect.H2Dialect
|
||||
jpa.database-platform: org.hibernate.dialect.H2Dialect
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.BorrowingFacade;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.infrastructure.BorrowingDatabaseAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.infrastructure.SpringBorrowingEventPublisherAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
public class BorrowingDomainConfig {
|
||||
|
||||
@Bean
|
||||
public BorrowingDatabase borrowingDatabase(JdbcTemplate jdbcTemplate) {
|
||||
return new BorrowingDatabaseAdapter(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BorrowingEventPublisher borrowingEventPublisher(ApplicationEventPublisher applicationEventPublisher){
|
||||
return new SpringBorrowingEventPublisherAdapter(applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MakeBookAvailable makeBookAvailable(BorrowingDatabase database, BorrowingEventPublisher borrowingEventPublisher) {
|
||||
return new BorrowingFacade(database, borrowingEventPublisher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.EmailFacade;
|
||||
import io.wkrzywiec.hexagonal.library.email.infrastructure.EmailDatabaseAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.email.infrastructure.SendGridEmailSender;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
public class EmailDomainConfig {
|
||||
|
||||
@Bean
|
||||
public EmailSender emailSender() {
|
||||
return new SendGridEmailSender();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EmailDatabase libraryDatabase(JdbcTemplate jdbcTemplate){
|
||||
return new EmailDatabaseAdapter(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SendReservationConfirmation sendReservationConfirmation(EmailSender emailSender, EmailDatabase database){
|
||||
return new EmailFacade(emailSender, database);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.GoogleBooksAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.InventoryDatabaseAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.SpringInventoryEventPublisherAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
class InventoryDomainConfig {
|
||||
|
||||
@Bean
|
||||
SpringInventoryEventPublisherAdapter springInventoryEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
return new SpringInventoryEventPublisherAdapter(applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AddNewBook addNewBook(BookRepository repository, RestTemplate restTemplate, ApplicationEventPublisher applicationEventPublisher){
|
||||
return new InventoryFacade(
|
||||
new InventoryDatabaseAdapter(repository),
|
||||
new GoogleBooksAdapter(restTemplate),
|
||||
springInventoryEventPublisher(applicationEventPublisher));
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,15 @@ package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@SpringBootApplication
|
||||
@Import({
|
||||
LibraryHexagonalConfig.class,
|
||||
InventoryDomainConfig.class,
|
||||
BorrowingDomainConfig.class,
|
||||
EmailDomainConfig.class
|
||||
})
|
||||
public class LibraryHexagonalApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
public class LibraryHexagonalConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplate restTemplate(){
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.ActiveUserNotFoundException;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.AvailableBookNotFoundExeption;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
|
||||
|
||||
public class BorrowingFacade implements MakeBookAvailable, ReserveBook {
|
||||
|
||||
private final BorrowingDatabase database;
|
||||
private final BorrowingEventPublisher eventPublisher;
|
||||
|
||||
public BorrowingFacade(BorrowingDatabase database, BorrowingEventPublisher eventPublisher) {
|
||||
this.database = database;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MakeBookAvailableCommand bookAvailableCommand) {
|
||||
database.setBookAvailable(bookAvailableCommand.getBookId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long handle(BookReservationCommand bookReservation) {
|
||||
AvailableBook availableBook =
|
||||
database.getAvailableBook(bookReservation.getBookId())
|
||||
.orElseThrow(() -> new AvailableBookNotFoundExeption(bookReservation.getBookId()));
|
||||
|
||||
ActiveUser activeUser =
|
||||
database.getActiveUser(bookReservation.getUserId())
|
||||
.orElseThrow(() -> new ActiveUserNotFoundException(bookReservation.getUserId()));
|
||||
|
||||
ReservedBook reservedBook = activeUser.reserve(availableBook);
|
||||
ReservationDetails reservationDetails = database.save(reservedBook);
|
||||
eventPublisher.publish(new BookReservedEvent(reservationDetails));
|
||||
return reservationDetails.getReservationId().getIdAsLong();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.application;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class NewBookWasAddedEventHandler {
|
||||
|
||||
private final MakeBookAvailable makeBookAvailable;
|
||||
|
||||
@EventListener
|
||||
public void handle(NewBookWasAddedEvent event){
|
||||
makeBookAvailable.handle(new MakeBookAvailableCommand(event.getBookIdAsLong()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.application;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/reservations")
|
||||
@RequiredArgsConstructor
|
||||
public class ReservationController {
|
||||
|
||||
private final ReserveBook reserveBook;
|
||||
|
||||
@PostMapping("")
|
||||
public ResponseEntity<String> makeReservation(@RequestBody BookReservationCommand reservationCommand){
|
||||
Long reservationId = reserveBook.handle(reservationCommand);
|
||||
return new ResponseEntity<>("Reservation has been made with an id " + reservationId, HttpStatus.CREATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationId;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void setBookAvailable(Long bookId) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO available (book_id) VALUES (?)",
|
||||
bookId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AvailableBook> getAvailableBook(Long bookId) {
|
||||
try {
|
||||
return Optional.ofNullable(
|
||||
jdbcTemplate.queryForObject(
|
||||
"SELECT book_id FROM available WHERE book_id = ?",
|
||||
AvailableBook.class,
|
||||
bookId));
|
||||
} catch (DataAccessException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ActiveUser> getActiveUser(Long userId) {
|
||||
try {
|
||||
jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM public.user as u WHERE u.id = ?",
|
||||
Long.class,
|
||||
userId);
|
||||
} catch (DataAccessException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<ReservedBook> reservedBooksByUser = getReservedBooksByUser(userId);
|
||||
return Optional.of(new ActiveUser(userId, reservedBooksByUser));
|
||||
}
|
||||
|
||||
private List<ReservedBook> getReservedBooksByUser(Long userId) {
|
||||
try {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT book_id FROM reserved WHERE reserved.user_id = ?",
|
||||
ReservedBook.class,
|
||||
userId
|
||||
);
|
||||
} catch (DataAccessException exception){
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReservationDetails save(ReservedBook reservedBook) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO reserved (book_id, user_id) VALUES (?, ?)",
|
||||
reservedBook.getIdAsLong(),
|
||||
reservedBook.getAssignedUserIdAsLong());
|
||||
|
||||
ReservationId reservationId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM reserved WHERE book_id = ?",
|
||||
ReservationId.class,
|
||||
reservedBook.getIdAsLong());
|
||||
return new ReservationDetails(reservationId, reservedBook);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SpringBorrowingEventPublisherAdapter implements BorrowingEventPublisher {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Override
|
||||
public void publish(BookReservedEvent event) {
|
||||
eventPublisher.publishEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.TooManyBooksAssignedToUserException;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class ActiveUser {
|
||||
|
||||
private final Long id;
|
||||
private final List<ReservedBook> reservedBooks;
|
||||
|
||||
public ActiveUser(Long id, List<ReservedBook> reservedBooks) {
|
||||
this.id = id;
|
||||
this.reservedBooks = reservedBooks;
|
||||
}
|
||||
|
||||
public ReservedBook reserve(AvailableBook availableBook){
|
||||
if (reservedBooks.size() < 3){
|
||||
ReservedBook reservedBook = new ReservedBook(availableBook.getIdAsLong(), id);
|
||||
reservedBooks.add(reservedBook);
|
||||
return reservedBook;
|
||||
} else {
|
||||
throw new TooManyBooksAssignedToUserException(id);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getIdAsLong(){
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<ReservedBook> getReservedBookList(){
|
||||
return reservedBooks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class AvailableBook implements Book {
|
||||
|
||||
private final Long id;
|
||||
|
||||
public AvailableBook(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getIdAsLong() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
interface Book {
|
||||
Long getIdAsLong();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Builder
|
||||
public class BookReservationCommand {
|
||||
private Long bookId;
|
||||
private Long userId;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class BookReservedEvent {
|
||||
|
||||
private final ReservationId reservationId;
|
||||
private final Long userId;
|
||||
private final ReservedBook reservedBook;
|
||||
private final Instant timestamp;
|
||||
|
||||
public BookReservedEvent(ReservationDetails reservationDetails) {
|
||||
this.reservationId = reservationDetails.getReservationId();
|
||||
this.userId = reservationDetails.getReservedBook().getAssignedUserIdAsLong();
|
||||
this.reservedBook = reservationDetails.getReservedBook();
|
||||
timestamp = Instant.now();
|
||||
}
|
||||
|
||||
public Long getReservationIdAsLong() {
|
||||
return reservationId.getIdAsLong();
|
||||
}
|
||||
|
||||
public Long getUserIdAsLong() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public Long getBookIdAsLong() {
|
||||
return reservedBook.getIdAsLong();
|
||||
}
|
||||
|
||||
public String getEventTimeStampAsString() {
|
||||
return timestamp.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Builder
|
||||
public class MakeBookAvailableCommand {
|
||||
private Long bookId;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
public class ReservationDetails {
|
||||
|
||||
private final ReservationId reservationId;
|
||||
private final ReservedBook reservedBook;
|
||||
|
||||
public ReservationDetails(ReservationId reservationId, ReservedBook reservedBook) {
|
||||
this.reservationId = reservationId;
|
||||
this.reservedBook = reservedBook;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class ReservationId {
|
||||
private final Long id;
|
||||
|
||||
public ReservationId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getIdAsLong(){
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class ReservedBook implements Book {
|
||||
|
||||
private final Long bookId;
|
||||
private final Long userId;
|
||||
|
||||
public ReservedBook(Long bookId, Long userId) {
|
||||
this.bookId = bookId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getIdAsLong() {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
public Long getAssignedUserIdAsLong(){
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
|
||||
|
||||
public class ActiveUserNotFoundException extends RuntimeException {
|
||||
public ActiveUserNotFoundException(Long bookId){
|
||||
super("There is no active user with an ID: " + bookId,
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
|
||||
|
||||
public class AvailableBookNotFoundExeption extends RuntimeException {
|
||||
public AvailableBookNotFoundExeption(Long bookId){
|
||||
super("There is no available book with an ID: " + bookId,
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
|
||||
|
||||
public class TooManyBooksAssignedToUserException extends RuntimeException {
|
||||
public TooManyBooksAssignedToUserException(Long userId){
|
||||
super("You can't assign another book to user account: " + userId + ". Reason: Too many books already assigned.",
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
|
||||
|
||||
public interface MakeBookAvailable {
|
||||
void handle(MakeBookAvailableCommand bookAvailableCommand);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
|
||||
public interface ReserveBook {
|
||||
Long handle(BookReservationCommand bookReservation);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BorrowingDatabase {
|
||||
void setBookAvailable(Long bookId);
|
||||
Optional<AvailableBook> getAvailableBook(Long bookId);
|
||||
Optional<ActiveUser> getActiveUser(Long userId);
|
||||
ReservationDetails save(ReservedBook reservedBook);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
|
||||
|
||||
public interface BorrowingEventPublisher {
|
||||
public void publish(BookReservedEvent event);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.ExternalBookIdDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.AddNewBook;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.GetBookDetails;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.outgoing.BookDatabase;
|
||||
|
||||
public class BookFacade implements AddNewBook {
|
||||
|
||||
private BookDatabase database;
|
||||
private GetBookDetails getBookDetails;
|
||||
|
||||
public BookFacade(BookDatabase database, GetBookDetails getBookDetails) {
|
||||
this.database = database;
|
||||
this.getBookDetails = getBookDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ExternalBookIdDTO externalBookIdDTO){
|
||||
BookDetailsDTO bookDetails = getBookDetails.handle(externalBookIdDTO.getValue());
|
||||
database.save(bookDetails);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public class BookDetailsDTO {
|
||||
|
||||
private String bookExternalId;
|
||||
private String isbn10;
|
||||
private String isbn13;
|
||||
private String title;
|
||||
private List<String> authors;
|
||||
private String publisher;
|
||||
private String publishedDate;
|
||||
private String description;
|
||||
private int pages;
|
||||
@EqualsAndHashCode.Exclude
|
||||
private String imageLink;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.ExternalBookIdDTO;
|
||||
|
||||
public interface AddNewBook {
|
||||
void handle(ExternalBookIdDTO externalBookIdDTO);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
|
||||
public interface GetBookDetails {
|
||||
BookDetailsDTO handle(String bookId);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
|
||||
public interface BookDatabase {
|
||||
|
||||
void save(BookDetailsDTO newBookDTO);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.wkrzywiec.hexagonal.library.email;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.model.EmailAddress;
|
||||
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
|
||||
|
||||
class EmailCreator {
|
||||
|
||||
static ReservationConfirmEmail reservationEmail(Long reservationId, String bookTitle, String emailTo){
|
||||
|
||||
EmailAddress from = new EmailAddress("tom@library.com");
|
||||
EmailAddress to = new EmailAddress(emailTo);
|
||||
|
||||
String subject = String.format("Library - book reservation confirmation (id - %d)", reservationId);
|
||||
String content = String.format("Dear reader,%n you have reserved a %s book which will be waiting for you in our library for next 2 days. Your reservation id is %d. %n Have a nice day, %n Library",
|
||||
bookTitle, reservationId);
|
||||
return new ReservationConfirmEmail(from, to, subject, content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.wkrzywiec.hexagonal.library.email;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
|
||||
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class EmailFacade implements SendReservationConfirmation {
|
||||
|
||||
private final EmailSender emailSender;
|
||||
private final EmailDatabase database;
|
||||
|
||||
@Override
|
||||
public void handle(SendReservationConfirmationCommand sendReservationConfirmation) {
|
||||
String bookTitle = database
|
||||
.getTitleByBookId(sendReservationConfirmation.getBookId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Can't get book title from database. Reason: there is no book with an id: " + sendReservationConfirmation.getBookId()));
|
||||
String userEmailAddress = database
|
||||
.getUserEmailAddress(sendReservationConfirmation.getUserId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Can't get email address from database. Reason: there is no user with an id: " + sendReservationConfirmation.getUserId()));
|
||||
|
||||
ReservationConfirmEmail reservationConfirmEmail = EmailCreator.reservationEmail(
|
||||
sendReservationConfirmation.getReservationId(),
|
||||
bookTitle,
|
||||
userEmailAddress
|
||||
);
|
||||
emailSender.sendReservationConfirmationEmail(reservationConfirmEmail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.application;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BookReservedEventHandler {
|
||||
|
||||
private final SendReservationConfirmation sendReservationConfirmation;
|
||||
|
||||
@EventListener
|
||||
public void handle(BookReservedEvent event) {
|
||||
sendReservationConfirmation.handle(
|
||||
new SendReservationConfirmationCommand(
|
||||
event.getReservationIdAsLong(),
|
||||
event.getUserIdAsLong(),
|
||||
event.getBookIdAsLong()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class EmailDatabaseAdapter implements EmailDatabase {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public Optional<String> getTitleByBookId(Long bookId) {
|
||||
try {
|
||||
return Optional.ofNullable(jdbcTemplate.queryForObject(
|
||||
"SELECT title FROM book WHERE id = ?",
|
||||
String.class,
|
||||
bookId));
|
||||
} catch (DataAccessException ex){
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getUserEmailAddress(Long userId) {
|
||||
try {
|
||||
return Optional.ofNullable(jdbcTemplate.queryForObject(
|
||||
"SELECT email FROM user WHERE id = ?",
|
||||
String.class,
|
||||
userId));
|
||||
} catch (DataAccessException ex){
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.infrastructure;
|
||||
|
||||
import com.sendgrid.Method;
|
||||
import com.sendgrid.Request;
|
||||
import com.sendgrid.SendGrid;
|
||||
import com.sendgrid.helpers.mail.Mail;
|
||||
import com.sendgrid.helpers.mail.objects.Content;
|
||||
import com.sendgrid.helpers.mail.objects.Email;
|
||||
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SendGridEmailSender implements EmailSender {
|
||||
|
||||
@Override
|
||||
public void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail) {
|
||||
Email from = new Email(reservationConfirmEmail.getFromEmailAddressAsString());
|
||||
Email to = new Email(reservationConfirmEmail.getToEmailAddressAsString());
|
||||
Content content = new Content("text/plain", reservationConfirmEmail.getContentAsString());
|
||||
Mail mail = new Mail(
|
||||
from,
|
||||
reservationConfirmEmail.getSubjectAsString(),
|
||||
to,
|
||||
content);
|
||||
|
||||
SendGrid sg = new SendGrid(System.getenv("SENDGRID_API_KEY"));
|
||||
Request request = new Request();
|
||||
try {
|
||||
request.setMethod(Method.POST);
|
||||
request.setEndpoint("mail/send");
|
||||
request.setBody(mail.build());
|
||||
sg.api(request);
|
||||
} catch (IOException ex) {
|
||||
System.out.print(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.model;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EmailAddress {
|
||||
|
||||
private final String value;
|
||||
|
||||
public EmailAddress(String value) {
|
||||
Pattern pattern = Pattern.compile("^(.+)@(.+)$");
|
||||
Matcher matcher = pattern.matcher(value);
|
||||
if(matcher.matches()){
|
||||
this.value = value;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Provided value is not an email address");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String getAsString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.model;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ReservationConfirmEmail {
|
||||
|
||||
private final EmailAddress from;
|
||||
private final EmailAddress to;
|
||||
private final String subject;
|
||||
private final String content;
|
||||
|
||||
public String getFromEmailAddressAsString(){
|
||||
return this.from.getAsString();
|
||||
}
|
||||
|
||||
public String getToEmailAddressAsString(){
|
||||
return this.to.getAsString();
|
||||
}
|
||||
|
||||
public String getSubjectAsString(){
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
public String getContentAsString(){
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class SendReservationConfirmationCommand {
|
||||
|
||||
private final Long reservationId;
|
||||
private final Long userId;
|
||||
private final Long bookId;
|
||||
|
||||
public SendReservationConfirmationCommand(Long reservationId, Long userId, Long bookId) {
|
||||
this.reservationId = reservationId;
|
||||
this.userId = userId;
|
||||
this.bookId = bookId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
|
||||
|
||||
public interface SendReservationConfirmation {
|
||||
void handle(SendReservationConfirmationCommand reservationConfirmationCommand);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.ports.outgoing;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface EmailDatabase {
|
||||
Optional<String> getTitleByBookId(Long bookId);
|
||||
Optional<String> getUserEmailAddress(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.ports.outgoing;
|
||||
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
|
||||
|
||||
public interface EmailSender {
|
||||
void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.BookFacade;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.AddNewBook;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.GetBookDetails;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.outgoing.BookDatabase;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.adapter.BookDatabaseAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.adapter.GoogleBooksAdapter;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.repository.PostgresBookRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class BookDomainConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplate restTemplate(){
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
BookDatabase bookRepository(PostgresBookRepository repository){
|
||||
return new BookDatabaseAdapter(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
GetBookDetails getBookDetails(RestTemplate restTemplate){
|
||||
return new GoogleBooksAdapter(restTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AddNewBook addNewBook(BookDatabase database, GetBookDetails getBookDetails){
|
||||
return new BookFacade(database, getBookDetails);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure.adapter;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.outgoing.BookDatabase;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.repository.AuthorEntiy;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.repository.BookEntity;
|
||||
import io.wkrzywiec.hexagonal.library.infrastructure.repository.PostgresBookRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class BookDatabaseAdapter implements BookDatabase {
|
||||
|
||||
private final PostgresBookRepository repository;
|
||||
|
||||
@Override
|
||||
public void save(BookDetailsDTO bookDetailsDTO) {
|
||||
BookEntity bookEntity = BookEntity.builder()
|
||||
.bookExternalId(bookDetailsDTO.getBookExternalId())
|
||||
.isbn10(bookDetailsDTO.getIsbn10())
|
||||
.isbn13(bookDetailsDTO.getIsbn13())
|
||||
.title(bookDetailsDTO.getTitle())
|
||||
.authors(mapToAuthorList(bookDetailsDTO.getAuthors()))
|
||||
.publisher(bookDetailsDTO.getPublisher())
|
||||
.publishedDate(bookDetailsDTO.getPublishedDate())
|
||||
.description(bookDetailsDTO.getDescription())
|
||||
.pages(bookDetailsDTO.getPages())
|
||||
.imageLink(bookDetailsDTO.getImageLink())
|
||||
.build();
|
||||
repository.save(bookEntity);
|
||||
}
|
||||
|
||||
private Set<AuthorEntiy> mapToAuthorList(List<String> authorNameList){
|
||||
return authorNameList.stream()
|
||||
.map(authorName -> AuthorEntiy.builder().name(authorName).build())
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure.repository;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface PostgresBookRepository extends CrudRepository<BookEntity, Long> {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
|
||||
|
||||
|
||||
public class InventoryFacade implements AddNewBook{
|
||||
|
||||
private InventoryDatabase database;
|
||||
private GetBookDetails getBookDetails;
|
||||
private InventoryEventPublisher eventPublisher;
|
||||
|
||||
public InventoryFacade(InventoryDatabase database, GetBookDetails getBookDetails, InventoryEventPublisher eventPublisher) {
|
||||
this.database = database;
|
||||
this.getBookDetails = getBookDetails;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(AddNewBookCommand addNewBookCommand){
|
||||
Book book = getBookDetails.handle(addNewBookCommand.getGoogleBookId());
|
||||
Book savedBook = database.save(book);
|
||||
eventPublisher.publishNewBookWasAddedEvent(new NewBookWasAddedEvent(savedBook.getIdAsLong()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.application;
|
||||
package io.wkrzywiec.hexagonal.library.inventory.application;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.ExternalBookIdDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.AddNewBook;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -18,8 +18,8 @@ public class BookCommandController {
|
||||
private final AddNewBook addNewBook;
|
||||
|
||||
@PostMapping("")
|
||||
public ResponseEntity<String> addNewBook(@RequestBody ExternalBookIdDTO externalBookIdDTO){
|
||||
addNewBook.handle(externalBookIdDTO);
|
||||
public ResponseEntity<String> addNewBook(@RequestBody AddNewBookCommand addNewBookCommand){
|
||||
addNewBook.handle(addNewBookCommand);
|
||||
return new ResponseEntity<>("New book was added to library", HttpStatus.CREATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface BookRepository extends CrudRepository<Book, Long> {
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure.adapter;
|
||||
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.GetBookDetails;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Author;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.BookIdentification;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn10;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn13;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -12,7 +16,6 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -26,10 +29,9 @@ public class GoogleBooksAdapter implements GetBookDetails {
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Override
|
||||
public BookDetailsDTO handle(String googleBookId) {
|
||||
public Book handle(String googleBookId) {
|
||||
|
||||
HttpHeaders requestHeader = new HttpHeaders();
|
||||
// requestHeader.add("Accept", MediaType.ALL_VALUE);
|
||||
HttpEntity<Object> requestEntity = new HttpEntity<>(requestHeader);
|
||||
|
||||
ResponseEntity<String> responseEntity =
|
||||
@@ -45,18 +47,19 @@ public class GoogleBooksAdapter implements GetBookDetails {
|
||||
|
||||
JsonObject volumeInfo = response.getAsJsonObject("volumeInfo");
|
||||
|
||||
return BookDetailsDTO.builder()
|
||||
.bookExternalId(googleBookId)
|
||||
.isbn10(extractIsbn(volumeInfo, "ISBN_10"))
|
||||
.isbn13(extractIsbn(volumeInfo, "ISBN_13"))
|
||||
.title(volumeInfo.get("title").getAsString())
|
||||
.authors(extractAuthors(volumeInfo))
|
||||
.publisher(volumeInfo.get("publisher").getAsString())
|
||||
.publishedDate(volumeInfo.get("publishedDate").getAsString())
|
||||
.description(volumeInfo.get("description").getAsString())
|
||||
.pages(volumeInfo.get("pageCount").getAsInt())
|
||||
.imageLink(extractImage(volumeInfo))
|
||||
.build();
|
||||
Isbn10 isbn10 = new Isbn10(extractIsbn(volumeInfo, "ISBN_10"));
|
||||
Isbn13 isbn13 = new Isbn13(extractIsbn(volumeInfo, "ISBN_13"));
|
||||
|
||||
return new Book(
|
||||
new BookIdentification(googleBookId, isbn10, isbn13),
|
||||
volumeInfo.get("title").getAsString(),
|
||||
extractAuthors(volumeInfo),
|
||||
volumeInfo.get("publisher").getAsString(),
|
||||
volumeInfo.get("publishedDate").getAsString(),
|
||||
volumeInfo.get("description").getAsString(),
|
||||
volumeInfo.get("pageCount").getAsInt(),
|
||||
extractImage(volumeInfo)
|
||||
);
|
||||
}
|
||||
|
||||
private String extractIsbn(JsonObject volumeInfo, String isbnType) {
|
||||
@@ -73,7 +76,7 @@ public class GoogleBooksAdapter implements GetBookDetails {
|
||||
.orElseThrow(() -> new RuntimeException("Inside volumeInfo there is no " + isbnType));
|
||||
}
|
||||
|
||||
private List<String> extractAuthors(JsonObject volumeInfo) {
|
||||
private Set<Author> extractAuthors(JsonObject volumeInfo) {
|
||||
return StreamSupport.stream(
|
||||
ofNullable(volumeInfo)
|
||||
.map(volume -> volume.getAsJsonArray("authors"))
|
||||
@@ -81,7 +84,8 @@ public class GoogleBooksAdapter implements GetBookDetails {
|
||||
.spliterator(),
|
||||
false)
|
||||
.map(JsonElement::getAsString)
|
||||
.collect(Collectors.toList());
|
||||
.map(Author::new)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private String extractImage(JsonObject volumeInfo) {
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class InventoryDatabaseAdapter implements InventoryDatabase {
|
||||
|
||||
private final BookRepository repository;
|
||||
|
||||
@Override
|
||||
public Book save(Book book) {
|
||||
return repository.save(book);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SpringInventoryEventPublisherAdapter implements InventoryEventPublisher {
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Override
|
||||
public void publishNewBookWasAddedEvent(NewBookWasAddedEvent event) {
|
||||
applicationEventPublisher.publishEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain.book.dto;
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class ExternalBookIdDTO {
|
||||
|
||||
private String value;
|
||||
public class AddNewBookCommand {
|
||||
private String googleBookId;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure.repository;
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
@@ -9,10 +10,11 @@ import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Builder
|
||||
@Entity
|
||||
@Table(name="author")
|
||||
public class AuthorEntiy {
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public class Author {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy= GenerationType.IDENTITY)
|
||||
@@ -21,4 +23,11 @@ public class AuthorEntiy {
|
||||
|
||||
@Column(name="name", unique=true)
|
||||
private String name;
|
||||
|
||||
public Author(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private Author() {
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
package io.wkrzywiec.hexagonal.library.infrastructure.repository;
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.GeneratedValue;
|
||||
@@ -17,31 +14,20 @@ import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
import javax.persistence.ManyToMany;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@Builder
|
||||
@Entity
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Setter
|
||||
@Getter
|
||||
@Table(name="book")
|
||||
public class BookEntity {
|
||||
@EqualsAndHashCode
|
||||
public class Book {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name="book_external_id")
|
||||
private String bookExternalId;
|
||||
|
||||
@Column(name="isbn_10")
|
||||
private String isbn10;
|
||||
|
||||
@Column(name="isbn_13")
|
||||
private String isbn13;
|
||||
@Embedded
|
||||
private BookIdentification identification;
|
||||
|
||||
@Column(name="title")
|
||||
private String title;
|
||||
@@ -52,7 +38,7 @@ public class BookEntity {
|
||||
name="book_author",
|
||||
joinColumns=@JoinColumn(name="book_id"),
|
||||
inverseJoinColumns=@JoinColumn(name="author_id"))
|
||||
private Set<AuthorEntiy> authors;
|
||||
private Set<Author> authors;
|
||||
|
||||
@Column(name="publisher")
|
||||
private String publisher;
|
||||
@@ -63,9 +49,28 @@ public class BookEntity {
|
||||
@Column(name="description", columnDefinition="TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name="pages")
|
||||
@Column(name="page_count")
|
||||
private int pages;
|
||||
|
||||
@Column(name="imageLink", columnDefinition="TEXT")
|
||||
@EqualsAndHashCode.Exclude
|
||||
private String imageLink;
|
||||
|
||||
public Book(BookIdentification identification, String title, Set<Author> authors, String publisher, String publishedDate, String description, int pages, String imageLink) {
|
||||
this.identification = identification;
|
||||
this.title = title;
|
||||
this.authors = authors;
|
||||
this.publisher = publisher;
|
||||
this.publishedDate = publishedDate;
|
||||
this.description = description;
|
||||
this.pages = pages;
|
||||
this.imageLink = imageLink;
|
||||
}
|
||||
|
||||
public Long getIdAsLong(){
|
||||
return id;
|
||||
}
|
||||
|
||||
private Book() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embeddable;
|
||||
import javax.persistence.Embedded;
|
||||
|
||||
@Embeddable
|
||||
@EqualsAndHashCode
|
||||
public class BookIdentification {
|
||||
|
||||
@Column(name="book_external_id")
|
||||
private String bookExternalId;
|
||||
|
||||
@Embedded
|
||||
private Isbn10 isbn10;
|
||||
|
||||
@Embedded
|
||||
private Isbn13 isbn13;
|
||||
|
||||
public BookIdentification(String bookExternalId, Isbn10 isbn10, Isbn13 isbn13) {
|
||||
this.bookExternalId = bookExternalId;
|
||||
this.isbn10 = isbn10;
|
||||
this.isbn13 = isbn13;
|
||||
}
|
||||
|
||||
private BookIdentification() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embeddable;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@Embeddable
|
||||
public class Isbn10 {
|
||||
@Column(name="isbn_10")
|
||||
private String value;
|
||||
|
||||
public Isbn10(String value) {
|
||||
if (value.matches("\\d{10}")){
|
||||
this.value = value;
|
||||
} else {
|
||||
throw new IllegalArgumentException("ISBN-10 should have 10 digits only.");
|
||||
}
|
||||
}
|
||||
|
||||
private Isbn10() {
|
||||
}
|
||||
|
||||
public String getAsString(){
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embeddable;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@Embeddable
|
||||
public class Isbn13 {
|
||||
|
||||
@Column(name="isbn_13")
|
||||
private String value;
|
||||
|
||||
public Isbn13(String value) {
|
||||
if (value.matches("\\d{13}")){
|
||||
this.value = value;
|
||||
} else {
|
||||
throw new IllegalArgumentException("ISBN-13 should have 10 digits only.");
|
||||
}
|
||||
}
|
||||
|
||||
private Isbn13() {
|
||||
}
|
||||
|
||||
public String getAsString(){
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class NewBookWasAddedEvent {
|
||||
|
||||
private final Long bookId;
|
||||
private final Instant timestamp;
|
||||
|
||||
public NewBookWasAddedEvent(Long bookId) {
|
||||
this.bookId = bookId;
|
||||
timestamp = Instant.now();
|
||||
}
|
||||
|
||||
public Long getBookIdAsLong() {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
public String getEventTimeStampAsString() {
|
||||
return timestamp.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.ports.incoming;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
|
||||
|
||||
public interface AddNewBook {
|
||||
void handle(AddNewBookCommand addNewBookCommand);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
|
||||
public interface GetBookDetails {
|
||||
Book handle(String bookId);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
|
||||
public interface InventoryDatabase {
|
||||
Book save(Book book);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory.ports.outgoing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
|
||||
|
||||
public interface InventoryEventPublisher {
|
||||
void publishNewBookWasAddedEvent(NewBookWasAddedEvent event);
|
||||
}
|
||||
9
src/main/resources/application-h2.yml
Normal file
9
src/main/resources/application-h2.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
spring:
|
||||
application:
|
||||
name: library
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password: password
|
||||
jpa.database-platform: org.hibernate.dialect.H2Dialect
|
||||
9
src/main/resources/application-postgres.yml
Normal file
9
src/main/resources/application-postgres.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
spring:
|
||||
application:
|
||||
name: library
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/library
|
||||
driverClassName: org.postgresql.Driver
|
||||
username: library
|
||||
password: library
|
||||
jpa.database-platform: org.hibernate.dialect.PostgreSQL9Dialect
|
||||
@@ -2,8 +2,8 @@ spring:
|
||||
application:
|
||||
name: library
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password: password
|
||||
jpa.database-platform: org.hibernate.dialect.H2Dialect
|
||||
url: jdbc:postgresql://${POSTGRES_SERVER}:5432/${POSTGRES_DB}
|
||||
driverClassName: org.postgresql.Driver
|
||||
username: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
jpa.database-platform: org.hibernate.dialect.PostgreSQL9Dialect
|
||||
23
src/main/resources/db/changelog/01_books_and_authors.sql
Normal file
23
src/main/resources/db/changelog/01_books_and_authors.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS public.book (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
book_external_id CHARACTER VARYING(255) NOT NULL UNIQUE,
|
||||
isbn_10 CHARACTER VARYING(10) DEFAULT NULL,
|
||||
isbn_13 CHARACTER VARYING(13) DEFAULT NULL,
|
||||
title CHARACTER VARYING(255) NOT NULL,
|
||||
publisher CHARACTER VARYING(255) DEFAULT NULL,
|
||||
published_date CHARACTER VARYING(255) DEFAULT NULL,
|
||||
description TEXT DEFAULT NULL,
|
||||
page_count INTEGER DEFAULT NULL,
|
||||
image_link CHARACTER VARYING(1000) DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.author (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name CHARACTER VARYING(255) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.book_author (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
book_id BIGINT NOT NULL REFERENCES public.book,
|
||||
author_id BIGINT NOT NULL REFERENCES public.author
|
||||
);
|
||||
6
src/main/resources/db/changelog/02_users.sql
Normal file
6
src/main/resources/db/changelog/02_users.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS public.user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
first_name CHARACTER VARYING(255) NOT NULL,
|
||||
last_name CHARACTER VARYING(255) NOT NULL,
|
||||
email CHARACTER VARYING(255) NOT NULL UNIQUE
|
||||
);
|
||||
16
src/main/resources/db/changelog/03_books_statuses.sql
Normal file
16
src/main/resources/db/changelog/03_books_statuses.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS public.available (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
book_id BIGINT NOT NULL REFERENCES public.book
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.reserved (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
book_id BIGINT NOT NULL REFERENCES public.book,
|
||||
user_id BIGINT NOT NULL REFERENCES public.user
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.borrowed (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
book_id BIGINT NOT NULL REFERENCES public.book,
|
||||
user_id BIGINT NOT NULL REFERENCES public.user
|
||||
);
|
||||
31
src/main/resources/db/changelog/db.changelog-master.yaml
Normal file
31
src/main/resources/db/changelog/db.changelog-master.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 1
|
||||
author: Wojtek
|
||||
comment: Create tables for books and authors
|
||||
changes:
|
||||
- sqlFile:
|
||||
path: 01_books_and_authors.sql
|
||||
relativeToChangelogFile: true
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
- changeSet:
|
||||
id: 2
|
||||
author: Wojtek
|
||||
comment: Create table for users
|
||||
changes:
|
||||
- sqlFile:
|
||||
path: 02_users.sql
|
||||
relativeToChangelogFile: true
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
- changeSet:
|
||||
id: 3
|
||||
author: Wojtek
|
||||
comment: Create tables for books statuses (available, reserved, borrowed)
|
||||
changes:
|
||||
- sqlFile:
|
||||
path: 03_books_statuses.sql
|
||||
relativeToChangelogFile: true
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
@@ -1,24 +1,37 @@
|
||||
package io.wkrzywiec.hexagonal.library;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Author;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.BookIdentification;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn10;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn13;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class TestData {
|
||||
|
||||
public static BookDetailsDTO homoDeusBookDetailsDTO() {
|
||||
return BookDetailsDTO
|
||||
.builder()
|
||||
.bookExternalId("dWYyCwAAQBAJ")
|
||||
.isbn10("1473545374")
|
||||
.isbn13("9781473545373")
|
||||
.title("Homo Deus")
|
||||
.authors(Collections.singletonList("Yuval Noah Harari"))
|
||||
.publisher("Random House")
|
||||
.publishedDate("2016-09-08")
|
||||
.description("<p><b>**THE MILLION COPY BESTSELLER**</b><br> <b></b><br><b> <i>Sapiens </i>showed us where we came from. In uncertain times, <i>Homo Deus</i> shows us where we’re going.</b></p><p> Yuval Noah Harari envisions a near future in which we face a new set of challenges. <i>Homo Deus</i> explores the projects, dreams and nightmares that will shape the twenty-first century and beyond – from overcoming death to creating artificial life.</p><p> It asks the fundamental questions: how can we protect this fragile world from our own destructive power? And what does our future hold?<br> <b></b><br><b> '<i>Homo Deus</i> will shock you. It will entertain you. It will make you think in ways you had not thought before’ Daniel Kahneman, bestselling author of <i>Thinking, Fast and Slow</i></b></p>")
|
||||
.pages(528)
|
||||
.build();
|
||||
public static String homoDeusBookGoogleId() {
|
||||
return "dWYyCwAAQBAJ";
|
||||
}
|
||||
|
||||
public static String homoDeusBookTitle() {
|
||||
return "Homo Deus";
|
||||
}
|
||||
|
||||
public static Book homoDeusBook() {
|
||||
Isbn10 isbn10 = new Isbn10("1473545374");
|
||||
Isbn13 isbn13 = new Isbn13("9781473545373");
|
||||
|
||||
return new Book(
|
||||
new BookIdentification(homoDeusBookGoogleId(), isbn10, isbn13),
|
||||
homoDeusBookTitle(),
|
||||
Collections.singleton(new Author("Yuval Noah Harari")),
|
||||
"Random House",
|
||||
"2016-09-08",
|
||||
"<p><b>**THE MILLION COPY BESTSELLER**</b><br> <b></b><br><b> <i>Sapiens </i>showed us where we came from. In uncertain times, <i>Homo Deus</i> shows us where we’re going.</b></p><p> Yuval Noah Harari envisions a near future in which we face a new set of challenges. <i>Homo Deus</i> explores the projects, dreams and nightmares that will shape the twenty-first century and beyond – from overcoming death to creating artificial life.</p><p> It asks the fundamental questions: how can we protect this fragile world from our own destructive power? And what does our future hold?<br> <b></b><br><b> '<i>Homo Deus</i> will shock you. It will entertain you. It will make you think in ways you had not thought before’ Daniel Kahneman, bestselling author of <i>Thinking, Fast and Slow</i></b></p>",
|
||||
528,
|
||||
"http://books.google.com/books/content?id=dWYyCwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE73PkLs4TNB-W2uhDvXJkIB4-9G9AJ_L1iYTYLEXa3zi2kahdsN9-_0tL7WRWgujNpjMA5ZuJO7_ykFUlCWAyLzcQVcGkqUS-NOkUkEcJ_ZRrgq48URpcfBrJWQCwSWtHo5pEGkp&source=gbs_api"
|
||||
);
|
||||
}
|
||||
|
||||
public static String homoDeusGooleBooksResponse(){
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.wkrzywiec.hexagonal.library.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.BorrowingFacade;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
|
||||
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
|
||||
|
||||
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.borrowing"},
|
||||
importOptions = { ImportOption.DoNotIncludeTests.class })
|
||||
public class BorrowingArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule hexagonalArchInBorrowingDomain = onionArchitecture()
|
||||
.domainModels("io.wkrzywiec.hexagonal.library.borrowing.model..")
|
||||
.domainServices("io.wkrzywiec.hexagonal.library.borrowing..")
|
||||
.applicationServices("io.wkrzywiec.hexagonal.library.borrowing.application..")
|
||||
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.borrowing.infrastructure..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule noSpringDependenciesInBorrowingFacade =
|
||||
noClass(BorrowingFacade.class)
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("org.springframework..");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.wkrzywiec.hexagonal.library.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import io.wkrzywiec.hexagonal.library.email.EmailFacade;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
|
||||
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
|
||||
|
||||
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.email"},
|
||||
importOptions = { ImportOption.DoNotIncludeTests.class })
|
||||
public class EmailArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule hexagonalArchInEmailDomain = onionArchitecture()
|
||||
.domainModels("io.wkrzywiec.hexagonal.library.email.model..")
|
||||
.domainServices("io.wkrzywiec.hexagonal.library.email..")
|
||||
.applicationServices("io.wkrzywiec.hexagonal.library.email.application..")
|
||||
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.email.infrastructure..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule noSpringDependenciesInEmailFacade =
|
||||
noClass(EmailFacade.class)
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("org.springframework..");
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.architecture;
|
||||
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
@AnalyzeClasses(packages = "io.wkrzywiec.hexagonal.library.domain")
|
||||
public class HexagonalArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule noSpringDependenciesInDomainPackage =
|
||||
noClasses().that()
|
||||
.resideInAPackage("..io.wkrzywiec.hexagonal.library.domain..")
|
||||
.should()
|
||||
.dependOnClassesThat().resideInAPackage("org.springframework..");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.wkrzywiec.hexagonal.library.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
|
||||
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
|
||||
|
||||
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.inventory"},
|
||||
importOptions = { ImportOption.DoNotIncludeTests.class })
|
||||
public class InventoryArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule hexagonalArchInInventoryDomain = onionArchitecture()
|
||||
.domainModels("io.wkrzywiec.hexagonal.library.inventory.model..")
|
||||
.domainServices("io.wkrzywiec.hexagonal.library.inventory..")
|
||||
.applicationServices("io.wkrzywiec.hexagonal.library.inventory.application..")
|
||||
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.inventory.infrastructure..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule noSpringDependenciesInInventoryFacade =
|
||||
noClass(InventoryFacade.class)
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("org.springframework..");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
|
||||
|
||||
public class BorrowingEventPublisherFake implements BorrowingEventPublisher {
|
||||
|
||||
@Override
|
||||
public void publish(BookReservedEvent event) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.ActiveUserNotFoundException;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.AvailableBookNotFoundExeption;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.TooManyBooksAssignedToUserException;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class BorrowingFacadeTest {
|
||||
|
||||
private BorrowingFacade facade;
|
||||
private InMemoryBorrowingDatabase database;
|
||||
private BorrowingEventPublisher eventPublisher;
|
||||
|
||||
@BeforeEach
|
||||
public void init(){
|
||||
database = new InMemoryBorrowingDatabase();
|
||||
eventPublisher = new BorrowingEventPublisherFake();
|
||||
facade = new BorrowingFacade(database, eventPublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Make book available")
|
||||
public void whenMakeBookAvailableCommandReceived_thenBookIsOnAvailableStatus() {
|
||||
//given
|
||||
MakeBookAvailableCommand makeBookAvailableCommand =
|
||||
MakeBookAvailableCommand.builder()
|
||||
.bookId(100L)
|
||||
.build();
|
||||
|
||||
//when
|
||||
facade.handle(makeBookAvailableCommand);
|
||||
|
||||
//then
|
||||
assertTrue(database.availableBooks.containsKey(100L));
|
||||
assertTrue(database.availableBooks.containsValue(new AvailableBook(100L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Make successful book reservation")
|
||||
public void givenAvailableBooksAndActiveUser_whenMakingReservation_thenBookIsReserved(){
|
||||
//given
|
||||
BookReservationCommand reservationCommand = ReservationTestData.anyBookReservation(100L, 100L);
|
||||
AvailableBook availableBook = ReservationTestData.anyAvailableBook(reservationCommand.getBookId());
|
||||
ActiveUser activeUser = ReservationTestData.anyActiveUser(reservationCommand.getUserId());
|
||||
|
||||
database.activeUsers.put(activeUser.getIdAsLong(), activeUser);
|
||||
database.availableBooks.put(availableBook.getIdAsLong(), availableBook);
|
||||
|
||||
//when
|
||||
facade.handle(reservationCommand);
|
||||
|
||||
//then
|
||||
assertEquals(1, activeUser.getReservedBookList().size());
|
||||
assertEquals(availableBook.getIdAsLong(), activeUser.getReservedBookList().get(0).getIdAsLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("User can't have more than 3 reservations")
|
||||
public void givenActiveUserAlreadyHas3Books_whenMakingReservation_thenBookIsNotReserved(){
|
||||
//given
|
||||
BookReservationCommand firstReservationCommand = ReservationTestData.anyBookReservation(100L, 100L);
|
||||
BookReservationCommand secondReservationCommand = ReservationTestData.anyBookReservation(101L, 100L);
|
||||
BookReservationCommand thirdReservationCommand = ReservationTestData.anyBookReservation(102L, 100L);
|
||||
BookReservationCommand fourthReservationCommand = ReservationTestData.anyBookReservation(103L, 100L);
|
||||
|
||||
AvailableBook availableBookNo1 = ReservationTestData.anyAvailableBook(firstReservationCommand.getBookId());
|
||||
AvailableBook availableBookNo2 = ReservationTestData.anyAvailableBook(secondReservationCommand.getBookId());
|
||||
AvailableBook availableBookNo3 = ReservationTestData.anyAvailableBook(thirdReservationCommand.getBookId());
|
||||
AvailableBook availableBookNo4 = ReservationTestData.anyAvailableBook(fourthReservationCommand.getBookId());
|
||||
|
||||
ActiveUser activeUser = ReservationTestData.anyActiveUser(firstReservationCommand.getUserId());
|
||||
|
||||
database.availableBooks.put(availableBookNo1.getIdAsLong(), availableBookNo1);
|
||||
database.availableBooks.put(availableBookNo2.getIdAsLong(), availableBookNo2);
|
||||
database.availableBooks.put(availableBookNo3.getIdAsLong(), availableBookNo3);
|
||||
database.availableBooks.put(availableBookNo4.getIdAsLong(), availableBookNo4);
|
||||
database.activeUsers.put(activeUser.getIdAsLong(), activeUser);
|
||||
|
||||
facade.handle(firstReservationCommand);
|
||||
facade.handle(secondReservationCommand);
|
||||
facade.handle(thirdReservationCommand);
|
||||
|
||||
//when & then
|
||||
assertThrows(
|
||||
TooManyBooksAssignedToUserException.class,
|
||||
() -> facade.handle(fourthReservationCommand));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Try to reserve book,but it's not available")
|
||||
public void givenNotAvailableBook_whenMakingReservation_thenThrowException(){
|
||||
//given
|
||||
BookReservationCommand reservationCommand = ReservationTestData.anyBookReservation(100L, 100L);
|
||||
ActiveUser activeUser = ReservationTestData.anyActiveUser(reservationCommand.getUserId());
|
||||
|
||||
database.activeUsers.put(activeUser.getIdAsLong(), activeUser);
|
||||
|
||||
assertThrows(
|
||||
AvailableBookNotFoundExeption.class,
|
||||
() -> facade.handle(reservationCommand));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Try to reserve book, but active user is not found")
|
||||
public void givenNotActiveUser_whenMakingReservation_thenThrowException(){
|
||||
//given
|
||||
BookReservationCommand reservationCommand = ReservationTestData.anyBookReservation(100L, 100L);
|
||||
AvailableBook availableBook = ReservationTestData.anyAvailableBook(reservationCommand.getBookId());
|
||||
|
||||
database.availableBooks.put(availableBook.getIdAsLong(), availableBook);
|
||||
|
||||
//when & then
|
||||
assertThrows(
|
||||
ActiveUserNotFoundException.class,
|
||||
() -> facade.handle(reservationCommand));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationId;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryBorrowingDatabase implements BorrowingDatabase {
|
||||
|
||||
ConcurrentHashMap<Long, ActiveUser> activeUsers = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<Long, AvailableBook> availableBooks = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<Long, ReservedBook> reservedBooks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void setBookAvailable(Long bookId) {
|
||||
availableBooks.put(bookId, new AvailableBook(bookId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AvailableBook> getAvailableBook(Long bookId) {
|
||||
if (availableBooks.containsKey(bookId)) {
|
||||
return Optional.of(availableBooks.get(bookId));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ActiveUser> getActiveUser(Long userId) {
|
||||
if (activeUsers.containsKey(userId)) {
|
||||
return Optional.of(activeUsers.get(userId));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReservationDetails save(ReservedBook reservedBook) {
|
||||
Long reservationId = new Random().nextLong();
|
||||
availableBooks.remove(reservedBook.getIdAsLong());
|
||||
reservedBooks.put(reservationId, reservedBook);
|
||||
return new ReservationDetails(new ReservationId(reservationId), reservedBook);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ReservationTestData {
|
||||
|
||||
public static BookReservationCommand anyBookReservation(Long bookId, Long userId){
|
||||
return BookReservationCommand.builder()
|
||||
.bookId(bookId)
|
||||
.userId(userId)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static AvailableBook anyAvailableBook(Long bookId){
|
||||
return new AvailableBook(bookId);
|
||||
}
|
||||
|
||||
public static ActiveUser anyActiveUser(Long userId){
|
||||
return new ActiveUser(userId, new ArrayList<ReservedBook>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package io.wkrzywiec.hexagonal.library.borrowing.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
|
||||
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
|
||||
|
||||
@SpringBootTest
|
||||
public class BorrowingDatabaseAdapterITCase {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private BorrowingDatabaseAdapter database;
|
||||
|
||||
@BeforeEach
|
||||
public void init(){
|
||||
database = new BorrowingDatabaseAdapter(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Save book as available")
|
||||
@Sql("/book-and-user.sql")
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void shouldSaveAvailableBook(){
|
||||
//given
|
||||
Long bookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
//when
|
||||
database.setBookAvailable(bookId);
|
||||
|
||||
//then
|
||||
Long savedBookId = jdbcTemplate.queryForObject(
|
||||
"SELECT book_id FROM available WHERE book_id = ?",
|
||||
Long.class,
|
||||
bookId);
|
||||
assertEquals(bookId, savedBookId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get available book by id")
|
||||
@Sql({"/book-and-user.sql", "/available-book.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void shouldGetAvailableBook(){
|
||||
//given
|
||||
Long bookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
//when
|
||||
Optional<AvailableBook> availableBookOptional = database.getAvailableBook(bookId);
|
||||
|
||||
//then
|
||||
assertTrue(availableBookOptional.isPresent());
|
||||
assertEquals(bookId, availableBookOptional.get().getIdAsLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get active user by id")
|
||||
@Sql("/book-and-user.sql")
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void shouldGetActiveUser() {
|
||||
//given
|
||||
Long activeUserId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM user WHERE email = ?",
|
||||
Long.class,
|
||||
"john.doe@test.com");
|
||||
|
||||
//when
|
||||
Optional<ActiveUser> activeUserOptional = database.getActiveUser(activeUserId);
|
||||
|
||||
//then
|
||||
assertTrue(activeUserOptional.isPresent());
|
||||
assertEquals(activeUserId, activeUserOptional.get().getIdAsLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Save reserved book")
|
||||
@Sql({"/book-and-user.sql", "/available-book.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void shouldSaveReservedBook(){
|
||||
//given
|
||||
Long bookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
Long activeUserId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM user WHERE email = ?",
|
||||
Long.class,
|
||||
"john.doe@test.com");
|
||||
|
||||
ReservedBook reservedBook = new ReservedBook(bookId, activeUserId);
|
||||
|
||||
//when
|
||||
ReservationDetails reservationDetails = database.save(reservedBook);
|
||||
|
||||
//then
|
||||
assertEquals(bookId, reservationDetails.getReservedBook().getIdAsLong());
|
||||
assertEquals(activeUserId, reservationDetails.getReservedBook().getAssignedUserIdAsLong());
|
||||
assertTrue(reservationDetails.getReservationId().getIdAsLong() > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.BookFacade;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.ExternalBookIdDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.GetBookDetails;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class BookFacadeTest {
|
||||
|
||||
private GetBookDetails getBookDetails;
|
||||
private InMemoryBookDatabase database;
|
||||
private BookFacade facade;
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
database = new InMemoryBookDatabase();
|
||||
getBookDetails = new GetBookDetailsMock();
|
||||
facade = new BookFacade(database, getBookDetails);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Correctly save a new book in a repository")
|
||||
public void correctlySaveBook(){
|
||||
//given
|
||||
BookDetailsDTO expectedBook = TestData.homoDeusBookDetailsDTO();
|
||||
ExternalBookIdDTO externalBookId = ExternalBookIdDTO
|
||||
.builder()
|
||||
.value(expectedBook.getBookExternalId())
|
||||
.build();
|
||||
|
||||
//when
|
||||
facade.handle(externalBookId);
|
||||
|
||||
//then
|
||||
BookDetailsDTO actualBook = database.books.get(1L);
|
||||
assertEquals(expectedBook, actualBook);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.incoming.GetBookDetails;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class GetBookDetailsMock implements GetBookDetails {
|
||||
|
||||
private Map<String, BookDetailsDTO> bookDetails;
|
||||
|
||||
public GetBookDetailsMock() {
|
||||
bookDetails = new HashMap<String, BookDetailsDTO>();
|
||||
bookDetails.put(TestData.homoDeusBookDetailsDTO().getBookExternalId(), TestData.homoDeusBookDetailsDTO());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BookDetailsDTO handle(String bookId) {
|
||||
return bookDetails.get(bookId);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package io.wkrzywiec.hexagonal.library.domain;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
|
||||
import io.wkrzywiec.hexagonal.library.domain.book.ports.outgoing.BookDatabase;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
public class InMemoryBookDatabase implements BookDatabase {
|
||||
|
||||
ConcurrentHashMap<Long, BookDetailsDTO> books = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(BookDetailsDTO newBookDTO) {
|
||||
Long id = books.size() + 1L;
|
||||
books.put(id, newBookDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.wkrzywiec.hexagonal.library.email;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
public class EmailFacadeTest {
|
||||
|
||||
private EmailFacade facade;
|
||||
private EmailSender emailSender;
|
||||
private InMemoryEmailDatabase database;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
database = new InMemoryEmailDatabase();
|
||||
emailSender = new EmailSenderFake();
|
||||
facade = new EmailFacade(emailSender, database);
|
||||
|
||||
database.bookTitles.put(1L, TestData.homoDeusBookTitle());
|
||||
database.emailAddresses.put(1L, "john.doe@test.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Prepare & send reservation confirmation email")
|
||||
public void shouldPrepareAndSendReservationConfirmation(){
|
||||
//given
|
||||
SendReservationConfirmationCommand sendReservationConfirmationCommand
|
||||
= new SendReservationConfirmationCommand(1L, 1L, 1L);
|
||||
|
||||
//when & then
|
||||
assertDoesNotThrow(() -> facade.handle(sendReservationConfirmationCommand));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.wkrzywiec.hexagonal.library.email;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
|
||||
|
||||
public class EmailSenderFake implements EmailSender {
|
||||
|
||||
@Override
|
||||
public void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.wkrzywiec.hexagonal.library.email;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryEmailDatabase implements EmailDatabase {
|
||||
|
||||
ConcurrentHashMap<Long, String> bookTitles = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<Long, String> emailAddresses = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<String> getTitleByBookId(Long bookId) {
|
||||
return Optional.of(bookTitles.get(bookId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getUserEmailAddress(Long userId) {
|
||||
return Optional.of(emailAddresses.get(userId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.infrastructure;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
|
||||
|
||||
@SpringBootTest
|
||||
public class EmailDatabaseAdapterITCase {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private EmailDatabaseAdapter emailDatabase;
|
||||
|
||||
@BeforeEach
|
||||
public void init(){
|
||||
emailDatabase = new EmailDatabaseAdapter(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get book title from db by its id")
|
||||
@Sql({"/book-and-user.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenBookId_whenGetBookTitle_thenGetBookTitle() {
|
||||
//given
|
||||
Long bookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
//when
|
||||
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId);
|
||||
|
||||
//then
|
||||
assertEquals(Optional.of(TestData.homoDeusBookTitle()), bookTitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get empty result when book is not in db")
|
||||
@Sql({"/book-and-user.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenWrongBookId_whenGetBookTitle_thenGetEmptyResult() {
|
||||
//given
|
||||
Long bookId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM book WHERE title = ?",
|
||||
Long.class,
|
||||
TestData.homoDeusBookTitle());
|
||||
|
||||
//when
|
||||
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId + 1124);
|
||||
|
||||
//then
|
||||
assertEquals(Optional.empty(), bookTitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get email from db by user id")
|
||||
@Sql({"/book-and-user.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenUserId_whenGetEmail_thenGetEmailAddress() {
|
||||
//given
|
||||
Long userId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM user WHERE email = ?",
|
||||
Long.class,
|
||||
"john.doe@test.com");
|
||||
|
||||
//when
|
||||
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId);
|
||||
|
||||
//then
|
||||
assertEquals(Optional.of("john.doe@test.com"), emailAddress);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get empty result when book is not in db")
|
||||
@Sql({"/book-and-user.sql"})
|
||||
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
|
||||
public void givenWrongUserId_whenGetEmail_thenGetEmptyResult() {
|
||||
//given
|
||||
Long userId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM user WHERE email = ?",
|
||||
Long.class,
|
||||
"john.doe@test.com");
|
||||
|
||||
//when
|
||||
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId + 1124);
|
||||
|
||||
//then
|
||||
assertEquals(Optional.empty(), emailAddress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.wkrzywiec.hexagonal.library.email.model;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class EmailAddressTest {
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("Create correct EmailAddress")
|
||||
public void givenCorrectEmailString_whenCreateEmailAddress_thenIsCreated(){
|
||||
//given
|
||||
String emailString = "john.doe@test.com";
|
||||
|
||||
//when
|
||||
EmailAddress emailAddress = new EmailAddress(emailString);
|
||||
|
||||
//then
|
||||
assertEquals(emailString, emailAddress.getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Throw IllegalArgument exception for incorrect email")
|
||||
public void givenInCorrectEmailString_whenCreateEmailAddress_thenThrowException(){
|
||||
//given
|
||||
String notAnEmailString = "not an email";
|
||||
String emailWithoutAt = "tom[at]test.com";
|
||||
String emailWithoutDomain = "tom@";
|
||||
|
||||
//when & then
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new EmailAddress(notAnEmailString));
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new EmailAddress(emailWithoutAt));
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new EmailAddress(emailWithoutDomain));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class GetBookDetailsFake implements GetBookDetails {
|
||||
|
||||
private Map<String, Book> books;
|
||||
|
||||
public GetBookDetailsFake() {
|
||||
books = new HashMap<String, Book>();
|
||||
books.put(
|
||||
TestData.homoDeusBookGoogleId(),
|
||||
TestData.homoDeusBook());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Book handle(String bookId) {
|
||||
return books.get(bookId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
public class InMemoryInventoryDatabase implements InventoryDatabase {
|
||||
|
||||
ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Book save(Book book) {
|
||||
Long id = books.size() + 1L;
|
||||
|
||||
try {
|
||||
FieldUtils.writeField(book, "id", id, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
books.put(id, book);
|
||||
return book;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
|
||||
|
||||
public class InvenotryEventPublisherFake implements InventoryEventPublisher {
|
||||
|
||||
@Override
|
||||
public void publishNewBookWasAddedEvent(NewBookWasAddedEvent event) { }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.wkrzywiec.hexagonal.library.inventory;
|
||||
|
||||
import io.wkrzywiec.hexagonal.library.TestData;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
|
||||
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
|
||||
public class InventoryFacadeTest {
|
||||
|
||||
private GetBookDetails getBookDetails;
|
||||
private InMemoryInventoryDatabase database;
|
||||
private InventoryEventPublisher eventPublisher;
|
||||
private InventoryFacade facade;
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
database = new InMemoryInventoryDatabase();
|
||||
getBookDetails = new GetBookDetailsFake();
|
||||
eventPublisher = new InvenotryEventPublisherFake();
|
||||
facade = new InventoryFacade(database, getBookDetails, eventPublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Correctly save a new book in a repository")
|
||||
public void correctlySaveBook(){
|
||||
//given
|
||||
AddNewBookCommand externalBookId = AddNewBookCommand
|
||||
.builder()
|
||||
.googleBookId(TestData.homoDeusBookGoogleId())
|
||||
.build();
|
||||
|
||||
//when
|
||||
facade.handle(externalBookId);
|
||||
|
||||
//then
|
||||
Book actualBook = database.books.get(1L);
|
||||
assertNotNull(actualBook);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user