mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-09-13 00:19:13 +00:00
feat: Add the implementation of Onion-Architecture (#3532)
* Add the implementation of Onion-Architecture * Execute maven command: mvn spotless:apply
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
---
|
||||
title: "Onion Architecture in Java: A Layered Approach to Building Maintainable and Testable Applications"
|
||||
shortTitle: Onion Architecture
|
||||
description: "Learn how the Onion Architecture pattern promotes maintainability, testability, and separation of concerns in Java applications. Explore examples, benefits, and best practices."
|
||||
category: Architectural
|
||||
language: en
|
||||
tag:
|
||||
- Decoupling
|
||||
- Enterprise patterns
|
||||
- Integration
|
||||
- Microservices
|
||||
- Scalability
|
||||
- Security
|
||||
---
|
||||
## Intent of Microservices API Gateway Design Pattern
|
||||
|
||||
In this project, the implementation demonstrates **Onion Architecture** with clear dependency direction: infrastructure and application depend on domain, but domain is independent.
|
||||
|
||||
The central intent is to keep business rules in `domain`, orchestrate use cases in `application`, and isolate delivery/persistence/API details in `infrastructure`.
|
||||
|
||||
Current implementation highlights:
|
||||
|
||||
* `domain`: `Person`, `Category`, `PersonRepository`, `DomainException`
|
||||
* `application`: `SavePersonUseCase`, `GetPersonUseCase`, DTO records
|
||||
* `infrastructure`: Spring Boot REST controller, JPA entities, repository adapter, bean wiring
|
||||
|
||||
## Also known as
|
||||
|
||||
* Ports and Adapters Architecture
|
||||
* Hexagonal-style layering (conceptually related)
|
||||
* Dependency-rule-first architecture
|
||||
|
||||
## Detailed Explanation of Onion Architecture Pattern with Real-World Examples
|
||||
|
||||
Real-world example
|
||||
|
||||
> Imagine a people-management service where business validation must stay consistent no matter how data is stored or exposed. In this codebase, `Person` and `Category` enforce invariants (e.g., age >= 18, required email/category), use cases coordinate behavior, and infrastructure adapts HTTP + JPA concerns. This allows the persistence or web layer to evolve without changing core domain rules.
|
||||
|
||||
In plain words
|
||||
|
||||
> The project keeps business logic in the center and treats frameworks as replaceable details around it.
|
||||
|
||||
Wikipedia says
|
||||
|
||||
> Onion Architecture is a software architecture pattern that emphasizes separation of concerns and dependency inversion by organizing code in concentric layers, with the domain model at the center.
|
||||
|
||||
Sequence diagram
|
||||
|
||||

|
||||
|
||||
Request flow in this implementation:
|
||||
|
||||
1. Client calls REST endpoint in `PersonController` (`/api/persons`, `/api/persons/{id}`)
|
||||
2. Controller delegates to `SavePersonUseCase` or `GetPersonUseCase`
|
||||
3. Use case interacts with `PersonRepository` abstraction from `domain`
|
||||
4. `PersonRepositoryAdapter` maps domain <-> JPA and delegates to `SpringDataPersonRepository`
|
||||
5. Response DTO (`PersonResponse`) is returned to the client
|
||||
|
||||
## Programmatic Example of Onion Architecture in Java
|
||||
|
||||
This repository exposes a simple person API backed by use cases and domain models.
|
||||
|
||||
Controller (infrastructure layer):
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PersonController {
|
||||
|
||||
@GetMapping("/persons/{id}")
|
||||
public ResponseEntity<PersonResponse> getPerson(@PathVariable Long id) {
|
||||
var person = getPersonUseCase.execute(id);
|
||||
return ResponseEntity.ok(person);
|
||||
}
|
||||
|
||||
@GetMapping("/persons")
|
||||
public ResponseEntity<List<PersonResponse>> getAllPersons() {
|
||||
var persons = getPersonUseCase.executeAll();
|
||||
return ResponseEntity.ok(persons);
|
||||
}
|
||||
|
||||
@PostMapping("/persons")
|
||||
public ResponseEntity<PersonResponse> savePerson(@RequestBody SavePersonCommand command) {
|
||||
try {
|
||||
var savedPerson = savePersonUseCase.execute(command);
|
||||
return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
|
||||
} catch (DomainException e) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use case (application layer):
|
||||
|
||||
```java
|
||||
public class SavePersonUseCase {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public PersonResponse execute(SavePersonCommand command) {
|
||||
var category = new Category(command.categoryId(), command.categoryType());
|
||||
var person = new Person(
|
||||
null,
|
||||
command.firstName(),
|
||||
command.lastName(),
|
||||
command.age(),
|
||||
command.phoneNumber(),
|
||||
command.email(),
|
||||
category);
|
||||
|
||||
var savedPerson = repository.save(person);
|
||||
return new PersonResponse(
|
||||
savedPerson.getId(),
|
||||
savedPerson.getFirstName(),
|
||||
savedPerson.getLastName(),
|
||||
savedPerson.getAge(),
|
||||
savedPerson.getPhoneNumber(),
|
||||
savedPerson.getEmail(),
|
||||
savedPerson.getCategory().getId(),
|
||||
savedPerson.getCategory().getType()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Domain validation (domain layer):
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
public Person(Long id, String firstName, String lastName, int age,
|
||||
String phoneNumber, String email, Category category) {
|
||||
validateNames(firstName, lastName);
|
||||
validateAge(age);
|
||||
validatePhone(phoneNumber);
|
||||
validateEmail(email);
|
||||
validateCategory(category);
|
||||
// assign fields...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Repository adapter (infrastructure -> domain port):
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public class PersonRepositoryAdapter implements PersonRepository {
|
||||
|
||||
private final SpringDataPersonRepository repository;
|
||||
|
||||
@Override
|
||||
public Optional<Person> findById(Long id) {
|
||||
return repository.findById(id).map(this::mapToDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person save(Person person) {
|
||||
JpaPersonEntity savedEntity = repository.save(mapToEntity(person));
|
||||
return mapToDomain(savedEntity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Maven 3.6.0** or higher
|
||||
|
||||
### Build Steps
|
||||
|
||||
1. **Navigate to the onion-architecture module directory:**
|
||||
```bash
|
||||
cd java-design-patterns/onion-architecture
|
||||
```
|
||||
|
||||
2. **Build all modules:**
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
This will compile the `domain`, `application`, and `infrastructure` modules and package them into a Spring Boot executable JAR.
|
||||
|
||||
3. **Run the Spring Boot application:**
|
||||
```bash
|
||||
mvn -pl infrastructure spring-boot:run
|
||||
```
|
||||
Alternatively, after building, run the JAR directly:
|
||||
```bash
|
||||
java -jar infrastructure/target/infrastructure-1.26.0-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
### Accessing the API
|
||||
|
||||
The application exposes REST endpoints at `http://localhost:8080/api`:
|
||||
There is a Postman collection available in the `etc/postman` folder for testing the API.
|
||||
|
||||
- **Get all persons:**
|
||||
```bash
|
||||
GET http://localhost:8080/api/persons
|
||||
```
|
||||
|
||||
- **Get person by ID:**
|
||||
```bash
|
||||
GET http://localhost:8080/api/persons/{id}
|
||||
```
|
||||
|
||||
- **Create a new person:**
|
||||
```bash
|
||||
POST http://localhost:8080/api/persons
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"age": 30,
|
||||
"phoneNumber": "555-1234",
|
||||
"email": "john.doe@example.com",
|
||||
"address": "123 Main St",
|
||||
"categoryId": 1,
|
||||
"categoryType": "individual"
|
||||
}
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
To execute unit tests across all modules:
|
||||
|
||||
```bash
|
||||
mvn clean test
|
||||
```
|
||||
|
||||
To run tests for a specific module:
|
||||
|
||||
```bash
|
||||
mvn -pl domain test
|
||||
mvn -pl application test
|
||||
mvn -pl infrastructure test
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
The application uses an **H2 in-memory database** for demonstration purposes. Configuration is in `infrastructure/src/main/resources/application.properties`:
|
||||
|
||||
- **JDBC URL:** `jdbc:h2:mem:testdb`
|
||||
- **Username:** `sa`
|
||||
- **Password:** `password`
|
||||
|
||||
Sample data is initialized from `infrastructure/src/main/resources/data.sql` on application startup.
|
||||
|
||||
## When to Use the Onion Architecture Pattern in Java
|
||||
|
||||
* When domain rules must be stable and independent from frameworks.
|
||||
* When you want use cases to be testable without HTTP or database setup.
|
||||
* When infrastructure details (web, JPA, database) should be replaceable.
|
||||
* When dependency direction must be enforced from outer layers toward the domain core.
|
||||
|
||||
## Onion Architecture Pattern Java Tutorials
|
||||
|
||||
* [Clean Architecture with Spring Boot (Baeldung)](https://www.baeldung.com/spring-boot-clean-architecture)
|
||||
* [Hexagonal Architecture Explained (Cockburn)](https://alistair.cockburn.us/hexagonal-architecture)
|
||||
* [Spring Data JPA Reference](https://docs.spring.io/spring-data/jpa/reference/)
|
||||
|
||||
## Benefits and Trade-offs of Microservices API Gateway Pattern
|
||||
|
||||
Benefits:
|
||||
|
||||
* Business validations are centralized in domain constructors (`Person`, `Category`).
|
||||
* Use cases stay independent from Spring, JPA, and transport concerns.
|
||||
* Repository abstraction (`PersonRepository`) keeps application logic persistence-agnostic.
|
||||
* Testability is strong across layers (domain, use case, adapter, controller tests).
|
||||
|
||||
Trade-offs:
|
||||
|
||||
* Additional mapping code between domain models, DTOs, and JPA entities.
|
||||
* More classes and modules than a simple CRUD-by-controller approach.
|
||||
* Requires discipline to avoid leaking infrastructure concerns into domain/application.
|
||||
|
||||
## Real-World Applications of Microservices API Gateway Pattern in Java
|
||||
|
||||
* People/contact management services with strict data validation.
|
||||
* Internal platforms where multiple delivery mechanisms (REST, batch, messaging) can share the same core domain.
|
||||
* Systems that need incremental infrastructure evolution while preserving business logic.
|
||||
|
||||
## Related Java Design Patterns
|
||||
|
||||
* [Repository](https://martinfowler.com/eaaCatalog/repository.html) - `PersonRepository` defines the domain-facing persistence contract.
|
||||
* [Adapter](https://refactoring.guru/design-patterns/adapter) - `PersonRepositoryAdapter` bridges domain model and Spring Data JPA.
|
||||
* [Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) - `ApplicationConfig` wires use case beans with repository implementations.
|
||||
|
||||
## References and Credits
|
||||
|
||||
* Project modules: `domain`, `application`, `infrastructure`
|
||||
* Java 21 + Maven multi-module setup
|
||||
* Spring Boot 3.3 (`spring-boot-starter-web`, `spring-boot-starter-data-jpa`, H2)
|
||||
* Layer-focused tests in each module validating domain invariants and use case behavior
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>onion-architecture</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>application</artifactId>
|
||||
<name>Application</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit 5 for testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito for mocking -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.dto;
|
||||
|
||||
public record PersonResponse(
|
||||
Long id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
int age,
|
||||
String phoneNumber,
|
||||
String email,
|
||||
Long categoryId,
|
||||
String categoryType) {}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.dto;
|
||||
|
||||
public record SavePersonCommand(
|
||||
String firstName,
|
||||
String lastName,
|
||||
int age,
|
||||
String phoneNumber,
|
||||
String email,
|
||||
String address,
|
||||
Long categoryId,
|
||||
String categoryType) {}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.usecase;
|
||||
|
||||
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class GetPersonUseCase {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public GetPersonUseCase(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public PersonResponse execute(Long id) {
|
||||
var person =
|
||||
repository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Person not found with id: " + id));
|
||||
|
||||
return new PersonResponse(
|
||||
person.getId(),
|
||||
person.getFirstName(),
|
||||
person.getLastName(),
|
||||
person.getAge(),
|
||||
person.getPhoneNumber(),
|
||||
person.getEmail(),
|
||||
person.getCategory().getId(),
|
||||
person.getCategory().getType());
|
||||
}
|
||||
|
||||
public List<PersonResponse> executeAll() {
|
||||
return repository.findAll().stream()
|
||||
.flatMap(Collection::stream)
|
||||
.map(
|
||||
person ->
|
||||
new PersonResponse(
|
||||
person.getId(),
|
||||
person.getFirstName(),
|
||||
person.getLastName(),
|
||||
person.getAge(),
|
||||
person.getPhoneNumber(),
|
||||
person.getEmail(),
|
||||
person.getCategory().getId(),
|
||||
person.getCategory().getType()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.usecase;
|
||||
|
||||
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||
import com.iluwatar.onion.domain.model.Category;
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
|
||||
public class SavePersonUseCase {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public SavePersonUseCase(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public PersonResponse execute(SavePersonCommand command) {
|
||||
var category = new Category(command.categoryId(), command.categoryType());
|
||||
var person =
|
||||
new Person(
|
||||
null,
|
||||
command.firstName(),
|
||||
command.lastName(),
|
||||
command.age(),
|
||||
command.phoneNumber(),
|
||||
command.email(),
|
||||
category);
|
||||
|
||||
var savedPerson = repository.save(person);
|
||||
|
||||
return new PersonResponse(
|
||||
savedPerson.getId(),
|
||||
savedPerson.getFirstName(),
|
||||
savedPerson.getLastName(),
|
||||
savedPerson.getAge(),
|
||||
savedPerson.getPhoneNumber(),
|
||||
savedPerson.getEmail(),
|
||||
savedPerson.getCategory().getId(),
|
||||
savedPerson.getCategory().getType());
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.usecase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||
import com.iluwatar.onion.domain.model.Category;
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GetPersonUseCaseTest {
|
||||
|
||||
@Mock private PersonRepository personRepository;
|
||||
|
||||
private GetPersonUseCase getPersonUseCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
getPersonUseCase = new GetPersonUseCase(personRepository);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute by ID - Happy Path")
|
||||
class ExecuteByIdHappyPath {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return PersonResponse when person exists")
|
||||
void shouldReturnPersonResponseWhenPersonExists() {
|
||||
// Arrange
|
||||
var person =
|
||||
new Person(
|
||||
1L,
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john.doe@example.com",
|
||||
new Category(1L, "Professional"));
|
||||
|
||||
when(personRepository.findById(1L)).thenReturn(Optional.of(person));
|
||||
|
||||
// Act
|
||||
var response = getPersonUseCase.execute(1L);
|
||||
|
||||
// Assert
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.id());
|
||||
assertEquals("John", response.firstName());
|
||||
assertEquals("Doe", response.lastName());
|
||||
assertEquals(25, response.age());
|
||||
assertEquals("+1234567890", response.phoneNumber());
|
||||
assertEquals("john.doe@example.com", response.email());
|
||||
assertEquals(1L, response.categoryId());
|
||||
assertEquals("Professional", response.categoryType());
|
||||
|
||||
verify(personRepository, times(1)).findById(1L);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute by ID - Error Cases")
|
||||
class ExecuteByIdErrorCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw RuntimeException when person not found")
|
||||
void shouldThrowExceptionWhenPersonNotFound() {
|
||||
// Arrange
|
||||
when(personRepository.findById(999L)).thenReturn(Optional.empty());
|
||||
|
||||
// Act & Assert
|
||||
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(999L));
|
||||
assertTrue(exception.getMessage().contains("Person not found with id: 999"));
|
||||
|
||||
verify(personRepository, times(1)).findById(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should propagate exception when repository throws exception")
|
||||
void shouldPropagateExceptionWhenRepositoryFails() {
|
||||
// Arrange
|
||||
when(personRepository.findById(anyLong())).thenThrow(new RuntimeException("Database error"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(1L));
|
||||
assertTrue(exception.getMessage().contains("Database error"));
|
||||
|
||||
verify(personRepository, times(1)).findById(1L);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute All - Happy Path")
|
||||
class ExecuteAllHappyPath {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return list of PersonResponses")
|
||||
void shouldReturnListOfPersonResponses() {
|
||||
// Arrange
|
||||
var person1 =
|
||||
new Person(
|
||||
1L,
|
||||
"John",
|
||||
"Doe",
|
||||
30,
|
||||
"+9876543255",
|
||||
"john.doe@example.com",
|
||||
new Category(2L, "Personal"));
|
||||
|
||||
var person2 =
|
||||
new Person(
|
||||
2L,
|
||||
"Jane",
|
||||
"Smith",
|
||||
25,
|
||||
"+9876543210",
|
||||
"jane.smith@example.com",
|
||||
new Category(2L, "Personal"));
|
||||
|
||||
when(personRepository.findAll()).thenReturn(Optional.of(List.of(person1, person2)));
|
||||
|
||||
// Act
|
||||
var responses = getPersonUseCase.executeAll();
|
||||
|
||||
// Assert
|
||||
assertNotNull(responses);
|
||||
assertEquals(2, responses.size());
|
||||
|
||||
PersonResponse response1 = responses.get(0);
|
||||
assertEquals(1L, response1.id());
|
||||
assertEquals("John", response1.firstName());
|
||||
assertEquals("Doe", response1.lastName());
|
||||
|
||||
PersonResponse response2 = responses.get(1);
|
||||
assertEquals(2L, response2.id());
|
||||
assertEquals("Jane", response2.firstName());
|
||||
assertEquals("Smith", response2.lastName());
|
||||
|
||||
verify(personRepository, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty list when no persons found")
|
||||
void shouldReturnEmptyListWhenNoPersonsFound() {
|
||||
// Arrange
|
||||
when(personRepository.findAll()).thenReturn(Optional.of(List.of()));
|
||||
|
||||
// Act
|
||||
List<PersonResponse> responses = getPersonUseCase.executeAll();
|
||||
|
||||
// Assert
|
||||
assertNotNull(responses);
|
||||
assertTrue(responses.isEmpty());
|
||||
|
||||
verify(personRepository, times(1)).findAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.application.usecase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
import com.iluwatar.onion.domain.model.Category;
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SavePersonUseCaseTest {
|
||||
|
||||
@Mock private PersonRepository personRepository;
|
||||
|
||||
@Captor private ArgumentCaptor<Person> personCaptor;
|
||||
|
||||
private SavePersonUseCase savePersonUseCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
savePersonUseCase = new SavePersonUseCase(personRepository);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute - Happy Path")
|
||||
class ExecuteHappyPath {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should save person and return PersonResponse")
|
||||
void shouldSavePersonAndReturnResponse() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john.doe@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Professional");
|
||||
|
||||
var category = new Category(1L, "Professional");
|
||||
var savedPerson =
|
||||
new Person(
|
||||
100L, // ID generated by database
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john.doe@example.com",
|
||||
category);
|
||||
|
||||
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
|
||||
|
||||
// Act
|
||||
var response = savePersonUseCase.execute(command);
|
||||
|
||||
// Assert
|
||||
assertNotNull(response);
|
||||
assertEquals(100L, response.id());
|
||||
assertEquals("John", response.firstName());
|
||||
assertEquals("Doe", response.lastName());
|
||||
assertEquals(25, response.age());
|
||||
assertEquals("+1234567890", response.phoneNumber());
|
||||
assertEquals("john.doe@example.com", response.email());
|
||||
assertEquals(1L, response.categoryId());
|
||||
assertEquals("Professional", response.categoryType());
|
||||
|
||||
// Verify repository interaction
|
||||
verify(personRepository, times(1)).save(any(Person.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should pass correct Person object to repository")
|
||||
void shouldPassCorrectPersonToRepository() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"Jane",
|
||||
"Smith",
|
||||
30,
|
||||
"+9876543210",
|
||||
"jane.smith@example.com",
|
||||
"456 Oak Ave",
|
||||
2L,
|
||||
"Personal");
|
||||
|
||||
var category = new Category(2L, "Personal");
|
||||
var savedPerson =
|
||||
new Person(200L, "Jane", "Smith", 30, "+9876543210", "jane.smith@example.com", category);
|
||||
|
||||
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
|
||||
|
||||
// Act
|
||||
savePersonUseCase.execute(command);
|
||||
|
||||
// Assert - Verify what was passed to repository
|
||||
verify(personRepository).save(personCaptor.capture());
|
||||
var capturedPerson = personCaptor.getValue();
|
||||
|
||||
assertNull(capturedPerson.getId()); // New person should have null ID
|
||||
assertEquals("Jane", capturedPerson.getFirstName());
|
||||
assertEquals("Smith", capturedPerson.getLastName());
|
||||
assertEquals(30, capturedPerson.getAge());
|
||||
assertEquals("+9876543210", capturedPerson.getPhoneNumber());
|
||||
assertEquals("jane.smith@example.com", capturedPerson.getEmail());
|
||||
assertEquals("Personal", capturedPerson.getCategory().getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute - Validation Failures")
|
||||
class ExecuteValidationFailures {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw DomainException when age is less than 18")
|
||||
void shouldThrowExceptionWhenAgeIsInvalid() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"Young",
|
||||
"Person",
|
||||
17, // Invalid age
|
||||
"+1234567890",
|
||||
"young@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Student");
|
||||
|
||||
// Act & Assert
|
||||
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||
assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
|
||||
|
||||
// Verify repository was never called
|
||||
verify(personRepository, never()).save(any(Person.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw DomainException when email is empty")
|
||||
void shouldThrowExceptionWhenEmailIsEmpty() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"", // Empty email
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Professional");
|
||||
|
||||
// Act & Assert
|
||||
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||
|
||||
verify(personRepository, never()).save(any(Person.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw DomainException when category type is invalid")
|
||||
void shouldThrowExceptionWhenCategoryTypeIsInvalid() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"" // Empty category type
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||
|
||||
verify(personRepository, never()).save(any(Person.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Execute - Repository Exceptions")
|
||||
class ExecuteRepositoryExceptions {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should propagate exception when repository fails")
|
||||
void shouldPropagateExceptionWhenRepositoryFails() {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john.doe@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Professional");
|
||||
|
||||
when(personRepository.save(any(Person.class)))
|
||||
.thenThrow(new RuntimeException("Database connection failed"));
|
||||
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(RuntimeException.class, () -> savePersonUseCase.execute(command));
|
||||
assertTrue(exception.getMessage().contains("Database connection failed"));
|
||||
|
||||
verify(personRepository, times(1)).save(any(Person.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>onion-architecture</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>domain</artifactId>
|
||||
<name>Domain</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- JUnit 5 for testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.exception;
|
||||
|
||||
public class DomainException extends RuntimeException {
|
||||
|
||||
public DomainException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.model;
|
||||
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
|
||||
public class Category {
|
||||
|
||||
private final Long id;
|
||||
private final String type;
|
||||
|
||||
public Category(Long id, String type) {
|
||||
if (type == null || type.isEmpty()) {
|
||||
throw new DomainException("Type is null or empty. Category type is required.");
|
||||
}
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.model;
|
||||
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
|
||||
public class Person {
|
||||
|
||||
private final Long id;
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
private final int age;
|
||||
private final String phoneNumber;
|
||||
private final String email;
|
||||
private final Category category;
|
||||
|
||||
public Person(
|
||||
Long id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
int age,
|
||||
String phoneNumber,
|
||||
String email,
|
||||
Category category) {
|
||||
validateNames(firstName, lastName);
|
||||
validateAge(age);
|
||||
validatePhone(phoneNumber);
|
||||
validateEmail(email);
|
||||
validateCategory(category);
|
||||
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.age = age;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.email = email;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
private void validateNames(String firstName, String lastName) {
|
||||
if (firstName == null || lastName == null) {
|
||||
throw new DomainException("First name and last name cannot be null.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAge(int age) {
|
||||
if (age < 18) {
|
||||
throw new DomainException("Age cannot be less than 18.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePhone(String phone) {
|
||||
if (phone == null || phone.isEmpty()) {
|
||||
throw new DomainException("Phone number cannot be null or empty.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEmail(String email) {
|
||||
if (email == null || email.isEmpty()) {
|
||||
throw new DomainException("Email cannot be null or empty.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCategory(Category category) {
|
||||
if (category == null || category.getType().isEmpty()) {
|
||||
throw new DomainException("Category cannot be null or empty.");
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public Category getCategory() {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.repository;
|
||||
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PersonRepository {
|
||||
Optional<Person> findById(Long id);
|
||||
|
||||
Optional<Person> findByFirstName(String firstName);
|
||||
|
||||
Optional<Person> findByLastName(String lastName);
|
||||
|
||||
Optional<List<Person>> findAll();
|
||||
|
||||
Person save(Person person);
|
||||
|
||||
boolean deleteById(Long id);
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CategoryTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create category with valid data")
|
||||
void shouldCreateCategoryWithValidData() {
|
||||
// Arrange & Act
|
||||
var category = new Category(1L, "Professional");
|
||||
|
||||
// Assert
|
||||
assertNotNull(category);
|
||||
assertEquals(1L, category.getId());
|
||||
assertEquals("Professional", category.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create category with null id")
|
||||
void shouldCreateCategoryWithNullId() {
|
||||
// Arrange & Act
|
||||
var category = new Category(null, "Personal");
|
||||
|
||||
// Assert
|
||||
assertNull(category.getId());
|
||||
assertEquals("Personal", category.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when type is null")
|
||||
void shouldThrowExceptionWhenTypeIsNull() {
|
||||
// Act & Assert
|
||||
var exception = assertThrows(DomainException.class, () -> new Category(1L, null));
|
||||
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when type is empty")
|
||||
void shouldThrowExceptionWhenTypeIsEmpty() {
|
||||
// Act & Assert
|
||||
var exception = assertThrows(DomainException.class, () -> new Category(1L, ""));
|
||||
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.domain.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PersonTest {
|
||||
|
||||
private final Category validCategory = new Category(1L, "Professional");
|
||||
|
||||
@Nested
|
||||
@DisplayName("Person Creation - Valid Cases")
|
||||
class ValidPersonCreation {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create person with valid data")
|
||||
void shouldCreatePersonWithValidData() {
|
||||
// Arrange & Act
|
||||
var person =
|
||||
new Person(1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", validCategory);
|
||||
|
||||
// Assert
|
||||
assertNotNull(person);
|
||||
assertEquals(1L, person.getId());
|
||||
assertEquals("John", person.getFirstName());
|
||||
assertEquals("Doe", person.getLastName());
|
||||
assertEquals(25, person.getAge());
|
||||
assertEquals("+1234567890", person.getPhoneNumber());
|
||||
assertEquals("john.doe@example.com", person.getEmail());
|
||||
assertEquals(validCategory, person.getCategory());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create person with minimum valid age (18)")
|
||||
void shouldCreatePersonWithMinimumValidAge() {
|
||||
// Arrange & Act
|
||||
var person =
|
||||
new Person(
|
||||
null,
|
||||
"Jane",
|
||||
"Smith",
|
||||
18, // minimum valid age
|
||||
"+9876543210",
|
||||
"jane@example.com",
|
||||
validCategory);
|
||||
|
||||
// Assert
|
||||
assertEquals(18, person.getAge());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Person Creation - Invalid Cases")
|
||||
class InvalidPersonCreation {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when first name is null")
|
||||
void shouldThrowExceptionWhenFirstNameIsNull() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() ->
|
||||
new Person(
|
||||
1L, null, "Doe", 25, "+1234567890", "john@example.com", validCategory));
|
||||
assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when last name is null")
|
||||
void shouldThrowExceptionWhenLastNameIsNull() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() ->
|
||||
new Person(
|
||||
1L, "John", null, 25, "+1234567890", "john@example.com", validCategory));
|
||||
assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when age is less than 18")
|
||||
void shouldThrowExceptionWhenAgeIsLessThan18() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() ->
|
||||
new Person(
|
||||
1L, "John", "Doe", 17, "+1234567890", "john@example.com", validCategory));
|
||||
assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when phone number is null")
|
||||
void shouldThrowExceptionWhenPhoneIsNull() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() -> new Person(1L, "John", "Doe", 25, null, "john@example.com", validCategory));
|
||||
assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when phone number is empty")
|
||||
void shouldThrowExceptionWhenPhoneIsEmpty() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() -> new Person(1L, "John", "Doe", 25, "", "john@example.com", validCategory));
|
||||
assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when email is null")
|
||||
void shouldThrowExceptionWhenEmailIsNull() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() -> new Person(1L, "John", "Doe", 25, "+1234567890", null, validCategory));
|
||||
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when email is empty")
|
||||
void shouldThrowExceptionWhenEmailIsEmpty() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() -> new Person(1L, "John", "Doe", 25, "+1234567890", "", validCategory));
|
||||
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when category is null")
|
||||
void shouldThrowExceptionWhenCategoryIsNull() {
|
||||
// Act & Assert
|
||||
var exception =
|
||||
assertThrows(
|
||||
DomainException.class,
|
||||
() -> new Person(1L, "John", "Doe", 25, "+1234567890", "john@example.com", null));
|
||||
assertTrue(exception.getMessage().contains("Category cannot be null or empty"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
@@ -0,0 +1,237 @@
|
||||
@startuml onion-architecture
|
||||
|
||||
skinparam packageStyle rectangle
|
||||
skinparam shadowing false
|
||||
skinparam defaultFontName Arial
|
||||
skinparam defaultFontSize 11
|
||||
skinparam classAttributeIconSize 0
|
||||
skinparam ArrowColor #444444
|
||||
skinparam ArrowThickness 1.2
|
||||
|
||||
skinparam package {
|
||||
BackgroundColor<<domain>> #FFF9C4
|
||||
BorderColor<<domain>> #F9A825
|
||||
FontColor<<domain>> #5D4037
|
||||
FontStyle<<domain>> bold
|
||||
}
|
||||
|
||||
skinparam package {
|
||||
BackgroundColor<<application>> #C8E6C9
|
||||
BorderColor<<application>> #388E3C
|
||||
FontColor<<application>> #1B5E20
|
||||
FontStyle<<application>> bold
|
||||
}
|
||||
|
||||
skinparam package {
|
||||
BackgroundColor<<infrastructure>> #BBDEFB
|
||||
BorderColor<<infrastructure>> #1976D2
|
||||
FontColor<<infrastructure>> #0D47A1
|
||||
FontStyle<<infrastructure>> bold
|
||||
}
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' DOMAIN LAYER (innermost)
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
package "Domain Layer" <<domain>> {
|
||||
|
||||
package "model" {
|
||||
class Person {
|
||||
- id : Long
|
||||
- firstName : String
|
||||
- lastName : String
|
||||
- age : int
|
||||
- phoneNumber : String
|
||||
- email : String
|
||||
- category : Category
|
||||
+ getId() : Long
|
||||
+ getFirstName() : String
|
||||
+ getLastName() : String
|
||||
+ getAge() : int
|
||||
+ getPhoneNumber() : String
|
||||
+ getEmail() : String
|
||||
+ getCategory() : Category
|
||||
}
|
||||
|
||||
class Category {
|
||||
- id : Long
|
||||
- type : String
|
||||
+ getId() : Long
|
||||
+ getType() : String
|
||||
}
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
interface PersonRepository {
|
||||
+ findById(id : Long) : Optional<Person>
|
||||
+ findByFirstName(firstName : String) : Optional<Person>
|
||||
+ findByLastName(lastName : String) : Optional<Person>
|
||||
+ findAll() : Optional<List<Person>>
|
||||
+ save(person : Person) : Person
|
||||
+ deleteById(id : Long) : boolean
|
||||
}
|
||||
}
|
||||
|
||||
package "exception" {
|
||||
class DomainException {
|
||||
+ DomainException(message : String)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' APPLICATION LAYER
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
package "Application Layer" <<application>> {
|
||||
|
||||
package "usecase" {
|
||||
class SavePersonUseCase {
|
||||
- repository : PersonRepository
|
||||
+ SavePersonUseCase(repository : PersonRepository)
|
||||
+ execute(command : SavePersonCommand) : PersonResponse
|
||||
}
|
||||
|
||||
class GetPersonUseCase {
|
||||
- repository : PersonRepository
|
||||
+ GetPersonUseCase(repository : PersonRepository)
|
||||
+ execute(id : Long) : PersonResponse
|
||||
+ executeAll() : List<PersonResponse>
|
||||
}
|
||||
}
|
||||
|
||||
package "dto" {
|
||||
class SavePersonCommand <<record>> {
|
||||
firstName : String
|
||||
lastName : String
|
||||
age : int
|
||||
phoneNumber : String
|
||||
email : String
|
||||
address : String
|
||||
categoryId : Long
|
||||
categoryType : String
|
||||
}
|
||||
|
||||
class PersonResponse <<record>> {
|
||||
id : Long
|
||||
firstName : String
|
||||
lastName : String
|
||||
age : int
|
||||
phoneNumber : String
|
||||
email : String
|
||||
categoryId : Long
|
||||
categoryType : String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' INFRASTRUCTURE LAYER (outermost)
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
package "Infrastructure Layer" <<infrastructure>> {
|
||||
|
||||
package "web" {
|
||||
class PersonController {
|
||||
- savePersonUseCase : SavePersonUseCase
|
||||
- getPersonUseCase : GetPersonUseCase
|
||||
+ getPerson(id : Long) : ResponseEntity<PersonResponse>
|
||||
+ getAllPersons() : ResponseEntity<List<PersonResponse>>
|
||||
+ savePerson(command : SavePersonCommand) : ResponseEntity<PersonResponse>
|
||||
}
|
||||
}
|
||||
|
||||
package "persistence" {
|
||||
class PersonRepositoryAdapter {
|
||||
- repository : SpringDataPersonRepository
|
||||
+ findById(id : Long) : Optional<Person>
|
||||
+ findByFirstName(firstName : String) : Optional<Person>
|
||||
+ findByLastName(lastName : String) : Optional<Person>
|
||||
+ findAll() : Optional<List<Person>>
|
||||
+ save(person : Person) : Person
|
||||
+ deleteById(id : Long) : boolean
|
||||
}
|
||||
|
||||
interface SpringDataPersonRepository {
|
||||
+ findByFirstName(firstName : String) : Optional<JpaPersonEntity>
|
||||
+ findByLastName(lastName : String) : Optional<JpaPersonEntity>
|
||||
}
|
||||
|
||||
class JpaPersonEntity {
|
||||
- id : Long
|
||||
- firstName : String
|
||||
- lastName : String
|
||||
- age : int
|
||||
- phoneNumber : String
|
||||
- email : String
|
||||
- category : JpaCategoryEntity
|
||||
}
|
||||
|
||||
class JpaCategoryEntity {
|
||||
- id : Long
|
||||
- type : String
|
||||
}
|
||||
}
|
||||
|
||||
package "config" {
|
||||
class ApplicationConfig <<@Configuration>> {
|
||||
+ savePersonUseCase(repo : PersonRepository) : SavePersonUseCase
|
||||
+ getPersonUseCase(repo : PersonRepository) : GetPersonUseCase
|
||||
}
|
||||
|
||||
class Application <<@SpringBootApplication>> {
|
||||
+ main(args : String[])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' DOMAIN: internal relationships
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
Person "1" *-- "1" Category : has
|
||||
Person ..> DomainException : <<throws>>
|
||||
Category ..> DomainException : <<throws>>
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' APPLICATION → DOMAIN
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
SavePersonUseCase --> PersonRepository : uses
|
||||
SavePersonUseCase ..> Person : creates
|
||||
SavePersonUseCase ..> Category : creates
|
||||
SavePersonUseCase ..> PersonResponse : returns
|
||||
SavePersonUseCase ..> SavePersonCommand : receives
|
||||
|
||||
GetPersonUseCase --> PersonRepository : uses
|
||||
GetPersonUseCase ..> PersonResponse : returns
|
||||
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
' INFRASTRUCTURE → APPLICATION / DOMAIN
|
||||
' ─────────────────────────────────────────────────────────────────────────────
|
||||
PersonController --> SavePersonUseCase : delegates to
|
||||
PersonController --> GetPersonUseCase : delegates to
|
||||
PersonController ..> DomainException : catches
|
||||
|
||||
PersonRepositoryAdapter ..|> PersonRepository : implements
|
||||
PersonRepositoryAdapter --> SpringDataPersonRepository : uses
|
||||
PersonRepositoryAdapter ..> JpaPersonEntity : maps
|
||||
PersonRepositoryAdapter ..> JpaCategoryEntity : maps
|
||||
PersonRepositoryAdapter ..> Person : maps to / from
|
||||
PersonRepositoryAdapter ..> Category : maps to / from
|
||||
|
||||
SpringDataPersonRepository --> JpaPersonEntity : manages
|
||||
JpaPersonEntity "1" *-- "1" JpaCategoryEntity : contains
|
||||
|
||||
ApplicationConfig ..> SavePersonUseCase : <<@Bean>>
|
||||
ApplicationConfig ..> GetPersonUseCase : <<@Bean>>
|
||||
ApplicationConfig --> PersonRepository : injects
|
||||
|
||||
legend right
|
||||
<b>Layer dependency rule</b>
|
||||
Infrastructure → Application → Domain
|
||||
Outer layers depend on inner layers.
|
||||
Domain has NO external dependencies.
|
||||
----
|
||||
<color:#F9A825>■</color> Domain Layer
|
||||
<color:#388E3C>■</color> Application Layer
|
||||
<color:#1976D2>■</color> Infrastructure Layer
|
||||
endlegend
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "79aca374-7080-46c1-938f-89b327a84d6a",
|
||||
"name": "onion-architecture",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "51221021",
|
||||
"_collection_link": "https://go.postman.co/collection/51221021-79aca374-7080-46c1-938f-89b327a84d6a?source=collection_link"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "savePerson",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/api/persons",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"api",
|
||||
"persons"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "getPersons",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/api/persons",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"api",
|
||||
"persons"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "getPersonById",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/api/persons/1",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"api",
|
||||
"persons",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>onion-architecture</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>infrastructure</artifactId>
|
||||
<name>Infrastructure</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- REST Controller -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA + Spring Data JPA-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- H2 Database -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.config;
|
||||
|
||||
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ApplicationConfig {
|
||||
|
||||
@Bean
|
||||
public SavePersonUseCase savePersonUseCase(PersonRepository repository) {
|
||||
return new SavePersonUseCase(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GetPersonUseCase getPersonUseCase(PersonRepository repository) {
|
||||
return new GetPersonUseCase(repository);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.persistence;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "category")
|
||||
public class JpaCategoryEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String type;
|
||||
|
||||
public JpaCategoryEntity() {}
|
||||
|
||||
public JpaCategoryEntity(Long id, String type) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.persistence;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "person")
|
||||
public class JpaPersonEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private int age;
|
||||
private String phoneNumber;
|
||||
private String email;
|
||||
|
||||
@ManyToOne private JpaCategoryEntity category;
|
||||
|
||||
public JpaPersonEntity() {}
|
||||
|
||||
public JpaPersonEntity(
|
||||
Long id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
int age,
|
||||
String phoneNumber,
|
||||
String email,
|
||||
JpaCategoryEntity category) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.age = age;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.email = email;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public JpaCategoryEntity getCategory() {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.persistence;
|
||||
|
||||
import com.iluwatar.onion.domain.model.Category;
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class PersonRepositoryAdapter implements PersonRepository {
|
||||
|
||||
private final SpringDataPersonRepository repository;
|
||||
|
||||
public PersonRepositoryAdapter(SpringDataPersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Person> findById(Long id) {
|
||||
return repository.findById(id).map(this::mapToDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Person> findByFirstName(String firstName) {
|
||||
return repository.findByFirstName(firstName).map(this::mapToDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Person> findByLastName(String lastName) {
|
||||
return repository.findByLastName(lastName).map(this::mapToDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<List<Person>> findAll() {
|
||||
return repository.findAll().stream()
|
||||
.map(this::mapToDomain)
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), Optional::of));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person save(Person person) {
|
||||
JpaPersonEntity entity = mapToEntity(person);
|
||||
JpaPersonEntity savedEntity = repository.save(entity);
|
||||
return mapToDomain(savedEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteById(Long id) {
|
||||
repository.deleteById(id);
|
||||
return repository.findById(id).isEmpty();
|
||||
}
|
||||
|
||||
private JpaPersonEntity mapToEntity(Person person) {
|
||||
return new JpaPersonEntity(
|
||||
person.getId(),
|
||||
person.getFirstName(),
|
||||
person.getLastName(),
|
||||
person.getAge(),
|
||||
person.getPhoneNumber(),
|
||||
person.getEmail(),
|
||||
new JpaCategoryEntity(person.getCategory().getId(), person.getCategory().getType()));
|
||||
}
|
||||
|
||||
private Person mapToDomain(JpaPersonEntity entity) {
|
||||
return new Person(
|
||||
entity.getId(),
|
||||
entity.getFirstName(),
|
||||
entity.getLastName(),
|
||||
entity.getAge(),
|
||||
entity.getPhoneNumber(),
|
||||
entity.getEmail(),
|
||||
new Category(entity.getCategory().getId(), entity.getCategory().getType()));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.persistence;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SpringDataPersonRepository extends JpaRepository<JpaPersonEntity, Long> {
|
||||
|
||||
Optional<JpaPersonEntity> findByFirstName(String firstName);
|
||||
|
||||
Optional<JpaPersonEntity> findByLastName(String lastName);
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.web;
|
||||
|
||||
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PersonController {
|
||||
|
||||
private final SavePersonUseCase savePersonUseCase;
|
||||
private final GetPersonUseCase getPersonUseCase;
|
||||
|
||||
public PersonController(SavePersonUseCase savePersonUseCase, GetPersonUseCase getPersonUseCase) {
|
||||
this.savePersonUseCase = savePersonUseCase;
|
||||
this.getPersonUseCase = getPersonUseCase;
|
||||
}
|
||||
|
||||
@GetMapping("/persons/{id}")
|
||||
public ResponseEntity<PersonResponse> getPerson(@PathVariable("id") Long id) {
|
||||
var person = getPersonUseCase.execute(id);
|
||||
return ResponseEntity.ok(person);
|
||||
}
|
||||
|
||||
@GetMapping("/persons")
|
||||
public ResponseEntity<List<PersonResponse>> getAllPersons() {
|
||||
var persons = getPersonUseCase.executeAll();
|
||||
return ResponseEntity.ok(persons);
|
||||
}
|
||||
|
||||
@PostMapping("/persons")
|
||||
public ResponseEntity<PersonResponse> savePerson(@RequestBody SavePersonCommand command) {
|
||||
try {
|
||||
var savedPerson = savePersonUseCase.execute(command);
|
||||
return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
|
||||
} catch (DomainException e) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# You can also specify the server port in the application.properties file.
|
||||
# Uncomment the following line and set the desired port number to change
|
||||
# the default port (8080) to a different value.
|
||||
#server.port=8080
|
||||
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
|
||||
# Force Spring Boot to always run SQL initialization scripts on startup
|
||||
spring.sql.init.mode=always
|
||||
|
||||
# If using Spring Data JPA / Hibernate, defer data insertion until AFTER entities are generated
|
||||
spring.jpa.defer-datasource-initialization=true
|
||||
|
||||
# Keep Hibernate's DDL generation active (or set to 'update')
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
@@ -0,0 +1,4 @@
|
||||
INSERT INTO category (id, type) VALUES (1, 'Teenage');
|
||||
INSERT INTO category (id, type) VALUES (2, 'Young Adult');
|
||||
INSERT INTO category (id, type) VALUES (3, 'Adult');
|
||||
INSERT INTO category (id, type) VALUES (4, 'Senior');
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.persistence;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.onion.domain.model.Category;
|
||||
import com.iluwatar.onion.domain.model.Person;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PersonRepositoryAdapterTest {
|
||||
|
||||
@Mock private SpringDataPersonRepository springDataRepository;
|
||||
|
||||
@Captor private ArgumentCaptor<JpaPersonEntity> entityCaptor;
|
||||
|
||||
private PersonRepositoryAdapter personRepositoryAdapter;
|
||||
private JpaPersonEntity jpaPersonEntity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
personRepositoryAdapter = new PersonRepositoryAdapter(springDataRepository);
|
||||
|
||||
// Setup test data
|
||||
var jpaCategory = new JpaCategoryEntity(1L, "Professional");
|
||||
jpaPersonEntity =
|
||||
new JpaPersonEntity(
|
||||
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", jpaCategory);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Find By ID")
|
||||
class FindById {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return Person when entity exists")
|
||||
void shouldReturnPersonWhenEntityExists() {
|
||||
// Arrange
|
||||
when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findById(1L);
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isPresent());
|
||||
Person person = result.get();
|
||||
assertEquals(1L, person.getId());
|
||||
assertEquals("John", person.getFirstName());
|
||||
assertEquals("Doe", person.getLastName());
|
||||
assertEquals(25, person.getAge());
|
||||
assertEquals("+1234567890", person.getPhoneNumber());
|
||||
assertEquals("john.doe@example.com", person.getEmail());
|
||||
assertEquals(1L, person.getCategory().getId());
|
||||
assertEquals("Professional", person.getCategory().getType());
|
||||
|
||||
verify(springDataRepository, times(1)).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty Optional when entity not found")
|
||||
void shouldReturnEmptyWhenEntityNotFound() {
|
||||
// Arrange
|
||||
when(springDataRepository.findById(999L)).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findById(999L);
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isEmpty());
|
||||
verify(springDataRepository, times(1)).findById(999L);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Find By First Name")
|
||||
class FindByFirstName {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return Person when entity with first name exists")
|
||||
void shouldReturnPersonWhenEntityExists() {
|
||||
// Arrange
|
||||
when(springDataRepository.findByFirstName("John")).thenReturn(Optional.of(jpaPersonEntity));
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findByFirstName("John");
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("John", result.get().getFirstName());
|
||||
verify(springDataRepository, times(1)).findByFirstName("John");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty Optional when no entity found")
|
||||
void shouldReturnEmptyWhenNotFound() {
|
||||
// Arrange
|
||||
when(springDataRepository.findByFirstName("Unknown")).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findByFirstName("Unknown");
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isEmpty());
|
||||
verify(springDataRepository, times(1)).findByFirstName("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Find By Last Name")
|
||||
class FindByLastName {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return Person when entity with last name exists")
|
||||
void shouldReturnPersonWhenEntityExists() {
|
||||
// Arrange
|
||||
when(springDataRepository.findByLastName("Doe")).thenReturn(Optional.of(jpaPersonEntity));
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findByLastName("Doe");
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("Doe", result.get().getLastName());
|
||||
verify(springDataRepository, times(1)).findByLastName("Doe");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Find All")
|
||||
class FindAll {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return list of Persons when entities exist")
|
||||
void shouldReturnListOfPersons() {
|
||||
// Arrange
|
||||
var category2 = new JpaCategoryEntity(2L, "Personal");
|
||||
var person2 =
|
||||
new JpaPersonEntity(
|
||||
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", category2);
|
||||
|
||||
when(springDataRepository.findAll()).thenReturn(List.of(jpaPersonEntity, person2));
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findAll();
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isPresent());
|
||||
var persons = result.get();
|
||||
assertEquals(2, persons.size());
|
||||
assertEquals("John", persons.get(0).getFirstName());
|
||||
assertEquals("Jane", persons.get(1).getFirstName());
|
||||
|
||||
verify(springDataRepository, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty list when no entities exist")
|
||||
void shouldReturnEmptyListWhenNoEntities() {
|
||||
// Arrange
|
||||
when(springDataRepository.findAll()).thenReturn(List.of());
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.findAll();
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isPresent());
|
||||
assertTrue(result.get().isEmpty());
|
||||
verify(springDataRepository, times(1)).findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Save")
|
||||
class Save {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should save person and return domain model")
|
||||
void shouldSavePersonAndReturnDomainModel() {
|
||||
// Arrange
|
||||
var personToSave =
|
||||
new Person(
|
||||
null, // New person without ID
|
||||
"Jane",
|
||||
"Smith",
|
||||
28,
|
||||
"+1111111111",
|
||||
"jane@example.com",
|
||||
new Category(2L, "Personal"));
|
||||
|
||||
var savedCategory = new JpaCategoryEntity(2L, "Personal");
|
||||
var savedEntity =
|
||||
new JpaPersonEntity(
|
||||
100L, // ID assigned by database
|
||||
"Jane",
|
||||
"Smith",
|
||||
28,
|
||||
"+1111111111",
|
||||
"jane@example.com",
|
||||
savedCategory);
|
||||
|
||||
when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
|
||||
|
||||
// Act
|
||||
var result = personRepositoryAdapter.save(personToSave);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(100L, result.getId());
|
||||
assertEquals("Jane", result.getFirstName());
|
||||
assertEquals("Smith", result.getLastName());
|
||||
assertEquals(28, result.getAge());
|
||||
|
||||
verify(springDataRepository, times(1)).save(any(JpaPersonEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should correctly map domain model to JPA entity")
|
||||
void shouldCorrectlyMapDomainToEntity() {
|
||||
// Arrange
|
||||
var personToSave =
|
||||
new Person(
|
||||
null,
|
||||
"Bob",
|
||||
"Johnson",
|
||||
35,
|
||||
"+2222222222",
|
||||
"bob@example.com",
|
||||
new Category(3L, "Business"));
|
||||
|
||||
var savedEntity =
|
||||
new JpaPersonEntity(
|
||||
200L,
|
||||
"Bob",
|
||||
"Johnson",
|
||||
35,
|
||||
"+2222222222",
|
||||
"bob@example.com",
|
||||
new JpaCategoryEntity(3L, "Business"));
|
||||
|
||||
when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
|
||||
|
||||
// Act
|
||||
personRepositoryAdapter.save(personToSave);
|
||||
|
||||
// Assert - Verify what was passed to Spring Data repository
|
||||
verify(springDataRepository).save(entityCaptor.capture());
|
||||
JpaPersonEntity capturedEntity = entityCaptor.getValue();
|
||||
|
||||
assertEquals("Bob", capturedEntity.getFirstName());
|
||||
assertEquals("Johnson", capturedEntity.getLastName());
|
||||
assertEquals(35, capturedEntity.getAge());
|
||||
assertEquals("+2222222222", capturedEntity.getPhoneNumber());
|
||||
assertEquals("bob@example.com", capturedEntity.getEmail());
|
||||
assertEquals(3L, capturedEntity.getCategory().getId());
|
||||
assertEquals("Business", capturedEntity.getCategory().getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Delete By ID")
|
||||
class DeleteById {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return true when person is successfully deleted")
|
||||
void shouldReturnTrueWhenDeleted() {
|
||||
// Arrange
|
||||
doNothing().when(springDataRepository).deleteById(1L);
|
||||
when(springDataRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
boolean result = personRepositoryAdapter.deleteById(1L);
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
verify(springDataRepository, times(1)).deleteById(1L);
|
||||
verify(springDataRepository, times(1)).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return false when person still exists after deletion")
|
||||
void shouldReturnFalseWhenStillExists() {
|
||||
// Arrange
|
||||
doNothing().when(springDataRepository).deleteById(1L);
|
||||
when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
|
||||
|
||||
// Act
|
||||
boolean result = personRepositoryAdapter.deleteById(1L);
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
verify(springDataRepository, times(1)).deleteById(1L);
|
||||
verify(springDataRepository, times(1)).findById(1L);
|
||||
}
|
||||
}
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
*
|
||||
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
* *
|
||||
* * The MIT License
|
||||
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||
* *
|
||||
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* * of this software and associated documentation files (the "Software"), to deal
|
||||
* * in the Software without restriction, including without limitation the rights
|
||||
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* * copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
* *
|
||||
* * The above copyright notice and this permission notice shall be included in
|
||||
* * all copies or substantial portions of the Software.
|
||||
* *
|
||||
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* * THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.iluwatar.onion.infrastructure.web;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||
import com.iluwatar.onion.domain.exception.DomainException;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@WebMvcTest(PersonController.class)
|
||||
class PersonControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
@MockitoBean private SavePersonUseCase savePersonUseCase;
|
||||
|
||||
@MockitoBean private GetPersonUseCase getPersonUseCase;
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /api/persons/{id}")
|
||||
class GetPersonById {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return person when person exists")
|
||||
void shouldReturnPersonWhenExists() throws Exception {
|
||||
// Arrange
|
||||
var response =
|
||||
new PersonResponse(
|
||||
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
|
||||
|
||||
when(getPersonUseCase.execute(1L)).thenReturn(response);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(get("/api/persons/1").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.firstName").value("John"))
|
||||
.andExpect(jsonPath("$.lastName").value("Doe"))
|
||||
.andExpect(jsonPath("$.age").value(25))
|
||||
.andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
|
||||
.andExpect(jsonPath("$.email").value("john.doe@example.com"))
|
||||
.andExpect(jsonPath("$.categoryId").value(1))
|
||||
.andExpect(jsonPath("$.categoryType").value("Professional"));
|
||||
|
||||
verify(getPersonUseCase, times(1)).execute(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle different person IDs")
|
||||
void shouldHandleDifferentPersonIds() throws Exception {
|
||||
// Arrange
|
||||
var response =
|
||||
new PersonResponse(
|
||||
42L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||
|
||||
when(getPersonUseCase.execute(42L)).thenReturn(response);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(get("/api/persons/42").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(42))
|
||||
.andExpect(jsonPath("$.firstName").value("Jane"));
|
||||
|
||||
verify(getPersonUseCase, times(1)).execute(42L);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /api/persons")
|
||||
class GetAllPersons {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return list of persons")
|
||||
void shouldReturnListOfPersons() throws Exception {
|
||||
// Arrange
|
||||
var person1 =
|
||||
new PersonResponse(
|
||||
1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
|
||||
var person2 =
|
||||
new PersonResponse(
|
||||
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||
|
||||
var persons = List.of(person1, person2);
|
||||
when(getPersonUseCase.executeAll()).thenReturn(persons);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$", hasSize(2)))
|
||||
.andExpect(jsonPath("$[0].id").value(1))
|
||||
.andExpect(jsonPath("$[0].firstName").value("John"))
|
||||
.andExpect(jsonPath("$[1].id").value(2))
|
||||
.andExpect(jsonPath("$[1].firstName").value("Jane"));
|
||||
|
||||
verify(getPersonUseCase, times(1)).executeAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty list when no persons exist")
|
||||
void shouldReturnEmptyListWhenNoPersons() throws Exception {
|
||||
// Arrange
|
||||
when(getPersonUseCase.executeAll()).thenReturn(List.of());
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$", hasSize(0)));
|
||||
|
||||
verify(getPersonUseCase, times(1)).executeAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle large list of persons")
|
||||
void shouldHandleLargeListOfPersons() throws Exception {
|
||||
// Arrange
|
||||
var persons =
|
||||
List.of(
|
||||
new PersonResponse(1L, "Person1", "Last1", 25, "+1", "p1@example.com", 1L, "Cat1"),
|
||||
new PersonResponse(2L, "Person2", "Last2", 26, "+2", "p2@example.com", 1L, "Cat1"),
|
||||
new PersonResponse(3L, "Person3", "Last3", 27, "+3", "p3@example.com", 1L, "Cat1"));
|
||||
|
||||
when(getPersonUseCase.executeAll()).thenReturn(persons);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(3)));
|
||||
|
||||
verify(getPersonUseCase, times(1)).executeAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/persons")
|
||||
class SavePerson {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create person with valid data")
|
||||
void shouldCreatePersonWithValidData() throws Exception {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"John",
|
||||
"Doe",
|
||||
25,
|
||||
"+1234567890",
|
||||
"john.doe@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Professional");
|
||||
|
||||
var response =
|
||||
new PersonResponse(
|
||||
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
|
||||
|
||||
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(command)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.firstName").value("John"))
|
||||
.andExpect(jsonPath("$.lastName").value("Doe"))
|
||||
.andExpect(jsonPath("$.age").value(25))
|
||||
.andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
|
||||
.andExpect(jsonPath("$.email").value("john.doe@example.com"))
|
||||
.andExpect(jsonPath("$.categoryId").value(1))
|
||||
.andExpect(jsonPath("$.categoryType").value("Professional"));
|
||||
|
||||
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle validation errors from use case")
|
||||
void shouldPropagateValidationErrors() throws Exception {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"Young",
|
||||
"Person",
|
||||
17, // Invalid age
|
||||
"+1234567890",
|
||||
"young@example.com",
|
||||
"123 Main St",
|
||||
1L,
|
||||
"Student");
|
||||
|
||||
when(savePersonUseCase.execute(any(SavePersonCommand.class)))
|
||||
.thenThrow(new DomainException("Age cannot be less than 18"));
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(command)))
|
||||
.andExpect(status().is4xxClientError());
|
||||
|
||||
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle malformed JSON")
|
||||
void shouldHandleMalformedJson() throws Exception {
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{invalid json}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(savePersonUseCase, never()).execute(any(SavePersonCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept all required fields in command")
|
||||
void shouldAcceptAllRequiredFields() throws Exception {
|
||||
// Arrange
|
||||
var command =
|
||||
new SavePersonCommand(
|
||||
"Complete",
|
||||
"Person",
|
||||
30,
|
||||
"+1111111111",
|
||||
"complete@example.com",
|
||||
"456 Oak Ave",
|
||||
5L,
|
||||
"Business");
|
||||
|
||||
var response =
|
||||
new PersonResponse(
|
||||
10L, "Complete", "Person", 30, "+1111111111", "complete@example.com", 5L, "Business");
|
||||
|
||||
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
|
||||
|
||||
// Act & Assert
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(command)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.categoryType").value("Business"));
|
||||
|
||||
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Controller Integration")
|
||||
class ControllerIntegration {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle multiple requests sequentially")
|
||||
void shouldHandleMultipleRequestsSequentially() throws Exception {
|
||||
// Arrange
|
||||
var getResponse =
|
||||
new PersonResponse(
|
||||
1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
|
||||
|
||||
var savePersonCommand =
|
||||
new SavePersonCommand(
|
||||
"Jane", "Smith", 30, "+9876543210", "jane@example.com", "456 Oak", 2L, "Personal");
|
||||
|
||||
var savePersonResponse =
|
||||
new PersonResponse(
|
||||
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||
|
||||
when(getPersonUseCase.execute(1L)).thenReturn(getResponse);
|
||||
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(savePersonResponse);
|
||||
|
||||
// Act & Assert - GET request
|
||||
mockMvc
|
||||
.perform(get("/api/persons/1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("John"));
|
||||
|
||||
// Act & Assert - POST request
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(savePersonCommand)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("Jane"));
|
||||
|
||||
verify(getPersonUseCase, times(1)).execute(1L);
|
||||
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>onion-architecture</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<junit-jupiter.version>5.10.0</junit-jupiter.version>
|
||||
<mockito.version>5.23.0</mockito.version>
|
||||
<assertj.version>3.24.2</assertj.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>domain</module>
|
||||
<module>application</module>
|
||||
<module>infrastructure</module>
|
||||
</modules>
|
||||
</project>
|
||||
@@ -106,7 +106,7 @@
|
||||
<module>converter</module>
|
||||
<module>curiously-recurring-template-pattern</module>
|
||||
<module>currying</module>
|
||||
<module>dao-factory</module>
|
||||
<module>dao-factory</module>
|
||||
<module>data-access-object</module>
|
||||
<module>data-bus</module>
|
||||
<module>data-locality</module>
|
||||
@@ -252,6 +252,7 @@
|
||||
<module>backpressure</module>
|
||||
<module>actor-model</module>
|
||||
<module>rate-limiting-pattern</module>
|
||||
<module>onion-architecture</module>
|
||||
</modules>
|
||||
<repositories>
|
||||
<repository>
|
||||
@@ -346,6 +347,11 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>bson</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user