Compare commits

...

30 Commits

Author SHA1 Message Date
wkrzywiec
6435c351f9 database cleanup 2020-05-24 12:01:22 +02:00
wkrzywiec
fa0c0fe4f4 reservation REST endpoint added 2020-05-24 11:38:05 +02:00
wkrzywiec
672c6f6557 adjust AddNewBookComponentTest to make book available 2020-05-24 10:51:02 +02:00
wkrzywiec
d9b68daa94 architecture tests adjusted 2020-05-24 10:30:13 +02:00
wkrzywiec
1c59f79cde component test naming convention 2020-05-22 12:07:18 +02:00
wkrzywiec
d2f52f849d SpringInventoryEventPublisherAdpater added 2020-05-22 11:44:19 +02:00
wkrzywiec
6f998cf030 publish event added to domain 2020-05-21 16:35:09 +02:00
wkrzywiec
f613a67517 return savedBook by database 2020-05-21 16:24:58 +02:00
wkrzywiec
8bea3936b7 add missing integration test for InventoryDatabaseAdapter 2020-05-21 15:53:37 +02:00
Wojtek Krzywiec
bebd23d22b Merge pull request #9 from wkrzywiec/reserve-book
Reserve book
2020-05-21 12:35:59 +02:00
wkrzywiec
4b28fe3460 sonar analysis fixes 2020-05-21 12:33:01 +02:00
wkrzywiec
b3378b8c43 architecture unit tests 2020-05-21 07:58:56 +02:00
Wojtek Krzywiec
246071b743 Merge pull request #8 from wkrzywiec/reserve-book
Reserve book
2020-05-20 22:15:27 +02:00
wkrzywiec
4b2f22307a architecture test corrected 2020-05-20 22:13:26 +02:00
wkrzywiec
b06d8219a4 inventory - replace dto with model 2020-05-20 22:01:42 +02:00
wkrzywiec
05b4c9e270 refactoring - change naming convention 2020-05-20 20:31:17 +02:00
wkrzywiec
fe0dd60540 repackage stuff into domains 2020-05-20 16:52:06 +02:00
wkrzywiec
4b1fa6048d BorrowingFacade return reservationId 2020-05-20 16:31:21 +02:00
wkrzywiec
3c4527fff4 prepare data for emailSender 2020-05-20 13:43:48 +02:00
wkrzywiec
d325e9196e BorrowingDatabaseAdapter added 2020-05-20 12:18:32 +02:00
wkrzywiec
3f57a0c9c4 handling reservation tuning 2020-05-20 06:38:54 +02:00
wkrzywiec
4b9a0b1987 make book available 2020-05-19 16:32:48 +02:00
wkrzywiec
442bf870c3 add liquibase database init script 2020-05-18 16:05:42 +02:00
wkrzywiec
83f947b70b add liquibase database init script 2020-05-18 10:19:17 +02:00
wkrzywiec
8a66e987ad book reservation domain 2020-05-17 13:33:38 +02:00
wkrzywiec
dc3730f1c9 rename book domain to inventory 2020-05-16 14:19:19 +02:00
Wojtek Krzywiec
9cd190deb7 correct typo in workflow 2020-05-14 16:19:55 +02:00
Wojtek Krzywiec
f450176b20 add integration workflows 2020-05-14 16:17:23 +02:00
Wojtek Krzywiec
2eb6483aba docker set up added 2020-05-14 15:51:35 +02:00
Wojtek Krzywiec
022046139c Merge pull request #7 from wkrzywiec/add-new-book
Add new book
2020-05-14 14:18:23 +02:00
84 changed files with 1805 additions and 367 deletions

View File

@@ -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
View File

@@ -31,3 +31,4 @@ build/
### VS Code ###
.vscode/
sendgrid.env

11
Dockerfile Normal file
View 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"]

View File

@@ -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
View 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:

37
pom.xml
View File

@@ -50,6 +50,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 +67,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 +131,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 +261,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>

View File

@@ -0,0 +1,42 @@
package io.wkrzywiec.hexagonal.library;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Table;
import java.util.List;
import java.util.stream.Collectors;
//https://gist.github.com/JorgenRingen/a56837fc07e630c32280b8e3d14c2d24
@Service
public class DatabaseCleanup implements InitializingBean {
@PersistenceContext
private EntityManager entityManager;
private List<String> tableNames;
@Transactional
public void execute() {
entityManager.flush();
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
for (final String tableName : tableNames) {
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
@Override
public void afterPropertiesSet() throws Exception {
tableNames = entityManager.getMetamodel().getEntities().stream()
.filter(e -> e.getJavaType().getAnnotation(Table.class) != null)
.map(e -> e.getJavaType().getAnnotation(Table.class).name())
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,96 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.DatabaseCleanup;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import org.junit.jupiter.api.AfterEach;
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 static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MakeReservationComponentTest {
@LocalServerPort
private int port;
@Autowired
private BookRepository bookRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseCleanup databaseCleanup;
private String baseURL;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
Book book = bookRepository.save(TestData.homoDeusBook());
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
book.getIdAsLong());
jdbcTemplate.update(
"INSERT INTO user (first_name, last_name, email) VALUES (?, ?, ?)",
"John",
"Doe",
"john.doe@test.com");
}
@AfterEach
public void after() {
databaseCleanup.execute();
}
@Test
@DisplayName("Reserve available book")
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);
}
}

View File

@@ -1,11 +1,11 @@
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.DatabaseCleanup;
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.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -13,15 +13,14 @@ 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 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;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddNewBookTest {
public class AddNewBookComponentTest {
@LocalServerPort
private int port;
@@ -29,6 +28,9 @@ public class AddNewBookTest {
@Autowired
private JdbcTemplate jdbc;
@Autowired
private DatabaseCleanup databaseCleanup;
private String baseURL;
@BeforeEach
@@ -37,6 +39,11 @@ public class AddNewBookTest {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@AfterEach
public void after() {
databaseCleanup.execute();
}
@Test
@DisplayName("Search for a new book in Google Books")
public void whenSearchForBook_thenGetList(){
@@ -55,29 +62,36 @@ 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")
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);
}
}

View File

@@ -0,0 +1,23 @@
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.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class BorrowingDomainConfig {
@Bean
public BorrowingDatabase borrowingDatabase(JdbcTemplate jdbcTemplate) {
return new BorrowingDatabaseAdapter(jdbcTemplate);
}
@Bean
public MakeBookAvailable makeBookAvailable(BorrowingDatabase database) {
return new BorrowingFacade(database);
}
}

View File

@@ -0,0 +1,29 @@
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.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
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));
}
}

View File

@@ -0,0 +1,14 @@
package io.wkrzywiec.hexagonal.library;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class LibraryHexagonalConfig {
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}

View File

@@ -0,0 +1,42 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
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;
public class BorrowingFacade implements MakeBookAvailable, ReserveBook {
private BorrowingDatabase database;
public BorrowingFacade(BorrowingDatabase database) {
this.database = database;
}
@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);
return reservationDetails.getReservationId().getIdAsLong();
}
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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.AllArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@AllArgsConstructor
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
interface Book {
Long getIdAsLong();
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,4 @@
package io.wkrzywiec.hexagonal.library.email;
public class EmailContent {
}

View File

@@ -0,0 +1,10 @@
package io.wkrzywiec.hexagonal.library.email;
class EmailCreator {
// EmailContent prepareEmailContent() {
// // email to
// // email from
// //
// }
}

View File

@@ -0,0 +1,6 @@
package io.wkrzywiec.hexagonal.library.email;
public interface EmailSender {
// void sendReservationConfirmationEmail(ReservationDetails reservationDetails);
}

View File

@@ -0,0 +1,33 @@
package io.wkrzywiec.hexagonal.library.email;
//import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
public class SendGridEmailSender implements EmailSender {
// @Override
// public void sendReservationConfirmationEmail(ReservationDetails reservationDetails) {
// email address - to
// repl9oservation id
// book title`
// Email from = new Email("test@example.com");
// String subject = "Sending with SendGrid is Fun";
// Email to = new Email("test@example.com");
// Content content = new Content("text/plain", "and easy to do anywhere, even with Java");
// Mail mail = new Mail(from, subject, 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());
// Response response = sg.api(request);
// System.out.println(response.getStatusCode());
// System.out.println(response.getBody());
// System.out.println(response.getHeaders());
// } catch (IOException ex) {
//
// }
// }
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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> {
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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

View 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

View File

@@ -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

View 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
);

View 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
);

View 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
);

View 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

View File

@@ -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 were 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 were 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(){

View File

@@ -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..");
}

View File

@@ -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..");
}

View File

@@ -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..");
}

View File

@@ -0,0 +1,125 @@
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 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;
@BeforeEach
public void init(){
database = new InMemoryBorrowingDatabase();
facade = new BorrowingFacade(database);
}
@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));
}
}

View File

@@ -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);
}
}

View File

@@ -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>());
}
}

View File

@@ -0,0 +1,127 @@
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.autoconfigure.jdbc.JdbcTest;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@JdbcTest
public class BorrowingDatabaseAdapterITCase {
@Autowired
private JdbcTemplate jdbcTemplate;
private BorrowingDatabaseAdapter database;
@BeforeEach
public void init(){
database = new BorrowingDatabaseAdapter(jdbcTemplate);
jdbcTemplate.update(
"INSERT INTO book (book_external_id, title) VALUES (?, ?)",
TestData.homoDeusBookGoogleId(),
TestData.homoDeusBookTitle());
jdbcTemplate.update(
"INSERT INTO user (first_name, last_name, email) VALUES (?, ?, ?)",
"John",
"Doe",
"john.doe@test.com");
}
@Test
@DisplayName("Save book as available")
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")
public void shouldGetAvailableBook(){
//given
Long bookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
TestData.homoDeusBookTitle());
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
bookId);
//when
Optional<AvailableBook> availableBookOptional = database.getAvailableBook(bookId);
//then
assertTrue(availableBookOptional.isPresent());
assertEquals(bookId, availableBookOptional.get().getIdAsLong());
}
@Test
@DisplayName("Get active user by id")
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")
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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) { }
}

View File

@@ -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);
}
}

View File

@@ -1,7 +1,7 @@
package io.wkrzywiec.hexagonal.library.infrastructure.adapter;
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.domain.book.dto.BookDetailsDTO;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -32,15 +32,15 @@ public class GoogleBooksAdapterITCase {
public void givenCorrectBookId_whenGetBookDetails_thenReturnBookDetailsDTO(){
//given
String homoDeusResponse = TestData.homoDeusGooleBooksResponse();
BookDetailsDTO homoDeusBookDetails = TestData.homoDeusBookDetailsDTO();
Book homoDeusBook = TestData.homoDeusBook();
server.expect(requestTo(
"https://www.googleapis.com/books/v1/volumes/" + homoDeusBookDetails.getBookExternalId()))
"https://www.googleapis.com/books/v1/volumes/" + TestData.homoDeusBookGoogleId()))
.andRespond(withSuccess(homoDeusResponse, MediaType.APPLICATION_JSON));
//when
BookDetailsDTO actualBookDetails = googleBooks.handle(homoDeusBookDetails.getBookExternalId());
Book actualBook= googleBooks.handle(TestData.homoDeusBookGoogleId());
//then
assertEquals(homoDeusBookDetails, actualBookDetails);
assertEquals(homoDeusBook, actualBook);
}
}

View File

@@ -0,0 +1,47 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
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.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DataJpaTest
public class InventoryDatabaseAdapterITCase {
@Autowired
private BookRepository bookRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
private InventoryDatabaseAdapter database;
@BeforeEach
public void init() {
database = new InventoryDatabaseAdapter(bookRepository);
}
@Test
@DisplayName("Save new book in database")
public void givenBook_whenSaveIt_thenBookIsSaved() {
//given
Book homoDeusBook = TestData.homoDeusBook();
//when
Book savedBook = database.save(homoDeusBook);
//then
Long savedBookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE id = ?",
Long.class,
savedBook.getIdAsLong());
assertEquals(savedBook.getIdAsLong(), savedBookId);
}
}

View File

@@ -0,0 +1,40 @@
package io.wkrzywiec.hexagonal.library.inventory.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 IsbnTest {
@Test
@DisplayName("ISBN-10 is created correctly")
public void shouldCreateCorrectISBN10(){
assertEquals("1473545374", new Isbn10("1473545374").getAsString());
}
@Test
@DisplayName("ISBN-10 is not created")
public void shouldNotCreateCorrectISBN10(){
assertThrows(
IllegalArgumentException.class,
() -> new Isbn10("9781473545373").getAsString()
);
}
@Test
@DisplayName("ISBN-13 is created correctly")
public void shouldCreateCorrectISBN13(){
assertEquals("9781473545373", new Isbn13("9781473545373").getAsString());
}
@Test
@DisplayName("ISBN-13 is not created")
public void shouldNotCreateCorrectISBN13(){
assertThrows(
IllegalArgumentException.class,
() -> new Isbn13("1473545374").getAsString()
);
}
}