diff --git a/.all-contributorsrc b/.all-contributorsrc index b2fef6640890..ed336828d406 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3493,6 +3493,88 @@ "contributions": [ "code" ] + }, + { + "login": "ssrijan-007-sys", + "name": "ssrijan-007-sys", + "avatar_url": "https://avatars.githubusercontent.com/u/137605821?v=4", + "profile": "https://github.com/ssrijan-007-sys", + "contributions": [ + "code" + ] + }, + { + "login": "e5LA", + "name": "e5LA", + "avatar_url": "https://avatars.githubusercontent.com/u/208197507?v=4", + "profile": "https://github.com/e5LA", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "maziyar-gerami", + "name": "Maziyar Gerami", + "avatar_url": "https://avatars.githubusercontent.com/u/122622721?v=4", + "profile": "http://maziyar-gerami.github.io/portfolio/", + "contributions": [ + "translation" + ] + }, + { + "login": "yybmion", + "name": "yoobin_mion", + "avatar_url": "https://avatars.githubusercontent.com/u/113106136?v=4", + "profile": "https://github.com/yybmion", + "contributions": [ + "code" + ] + }, + { + "login": "ronodhirSoumik", + "name": "Soumik Sarker", + "avatar_url": "https://avatars.githubusercontent.com/u/46843689?v=4", + "profile": "https://ronodhirsoumik.github.io", + "contributions": [ + "doc" + ] + }, + { + "login": "naman-sriv", + "name": "Naman Srivastava", + "avatar_url": "https://avatars.githubusercontent.com/u/82610773?v=4", + "profile": "https://github.com/naman-sriv", + "contributions": [ + "code" + ] + }, + { + "login": "letdtcode", + "name": "Thanh Nguyen Duc", + "avatar_url": "https://avatars.githubusercontent.com/u/92111552?v=4", + "profile": "https://github.com/letdtcode", + "contributions": [ + "code" + ] + }, + { + "login": "skamble2", + "name": "Soham Kamble", + "avatar_url": "https://avatars.githubusercontent.com/u/121136639?v=4", + "profile": "https://github.com/skamble2", + "contributions": [ + "code" + ] + }, + { + "login": "Olexandr88", + "name": "Olexandr88", + "avatar_url": "https://avatars.githubusercontent.com/u/93856062?v=4", + "profile": "https://github.com/Olexandr88", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 6, diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml index cac1250b70e8..bb0ed7e116ae 100644 --- a/.github/workflows/presubmit.yml +++ b/.github/workflows/presubmit.yml @@ -1,35 +1,28 @@ name: Presubmit.ai permissions: - contents: read - pull-requests: write - issues: write + contents: read + pull-requests: write + issues: write on: - pull_request_target: # Handle forked repository PRs in the base repository context - types: [opened, synchronize] - pull_request_review_comment: # Handle review comments - types: [created] + pull_request_target: + types: [opened, synchronize] + pull_request_review_comment: + types: [created] jobs: - review: - runs-on: ubuntu-latest - steps: - - name: Check required secrets - run: | - if [ -z "${{ secrets.LLM_API_KEY }}" ]; then - echo "Error: LLM_API_KEY secret is not configured" - exit 1 - fi - - - name: Check out PR code - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Run AI Reviewer - uses: presubmit/ai-reviewer@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - LLM_API_KEY: ${{ secrets.LLM_API_KEY }} - LLM_MODEL: "gemini-1.5-flash" + review: + runs-on: ubuntu-latest + steps: + - name: Check required secrets + run: | + if [ -z "${{ secrets.LLM_API_KEY }}" ]; then + echo "Error: LLM_API_KEY secret is not configured" + exit 1 + fi + - uses: presubmit/ai-reviewer@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_MODEL: "gpt-5-nano" \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..b30cbdd19b06 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# Java Design Patterns - Priming Context for AI Agents + +## Quick Overview +- This repository is a comprehensive collection of design patterns implemented in Java. +- The project demonstrates how to solve common software design problems using standard patterns. +- The code for each pattern lives directly in this repository, alongside its explanatory README. +- These readmes are published on the java-design-patterns.com website. +- Another repository (https://github.com/iluwatar/java-design-patterns-vuepress-web) handles the deployment of the website. + +## Stack +- **Java 21**: The primary programming language used for pattern implementations. +- **Maven**: Dependency management and build tool. +- **JUnit 5**: The testing framework used to verify pattern behaviors. +- **Mockito**: Used for mocking dependencies in unit tests. +- **Lombok**: Used to reduce boilerplate code (getters, setters, etc.). +- **Spotless**: Enforces consistent code formatting via Google Java Format. + +## Trusted Sources +- [Java SE 21 Documentation](https://docs.oracle.com/en/java/javase/21/docs/api/) +- [Maven Official Documentation](https://maven.apache.org/guides/index.html) +- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) +- [Project Wiki](https://github.com/iluwatar/java-design-patterns/wiki) + +## Structure +- `/pom.xml`: The root Maven configuration file that defines global dependencies and lists all pattern modules. +- `/[pattern-name]/`: Individual folders for each design pattern (e.g., `/abstract-factory`, `/builder`), acting as standalone Maven modules. +- `/[pattern-name]/src/main/java/`: Contains the actual Java implementation classes of the specific design pattern. +- `/[pattern-name]/src/test/java/`: Contains the JUnit tests verifying the pattern's behavior. +- `/[pattern-name]/README.md`: The documentation for the pattern, which gets published to the main website. + +## Patterns +- Keep pattern implementations simple, atomic, and easy to understand. +- Write descriptive and meaningful names for classes, interfaces, and methods. +- Always include comprehensive unit tests for every new pattern or code modification. +- Follow the Google Java Format strictly (enforced by Spotless). +- Document the intent, explanation, and real-world usage clearly in each module's `README.md`. + +## Anti-patterns +- Avoid overcomplicating patterns with unnecessary external dependencies or complex frameworks. +- Do not introduce business logic that distracts from the core mechanism of the design pattern itself. +- Submitting new patterns or features without corresponding unit tests is strictly discouraged. +- Avoid large monolithic packages; each pattern should reside in its own isolated module. + +## Example Design Pattern +When a new design pattern is added to the repository, it generally follows these steps: +- **Create a Module**: Create a new folder for the pattern in the root directory (e.g., `/my-new-pattern`). +- **Update root pom.xml**: Add `my-new-pattern` to the `` section of the root `pom.xml`. +- **Add Module pom.xml**: Create a `pom.xml` inside the new folder that inherits from the parent project. +- **Write the Code**: Implement the pattern logic under `src/main/java/com/iluwatar/mynewpattern`, usually including an `App.java` class to demonstrate its usage. +- **Write the Tests**: Add comprehensive unit tests under `src/test/java/com/iluwatar/mynewpattern`. +- **Document**: Create a `README.md` at the root of the new module, structured with standard sections like Intent, Explanation, Class diagram, Applicability, and Real world examples. diff --git a/README.md b/README.md index 881563902e26..bf0e4ec9f073 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Design Patterns Implemented in Java -![Java CI](https://github.com/iluwatar/java-design-patterns/workflows/Java%20CI/badge.svg) +[![Java CI](https://github.com/iluwatar/java-design-patterns/workflows/Java%20CI/badge.svg)](https://github.com/iluwatar/java-design-patterns/actions/workflows/maven-ci.yml) [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/iluwatar/java-design-patterns/master/LICENSE.md) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=ncloc)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-383-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-392-orange.svg?style=flat-square)](#contributors-)
@@ -45,7 +45,7 @@ If you are willing to contribute to the project you will find the relevant infor # The Book -The design patterns are now available as an e-book. Find out more about "Open Source Java Design Patterns" here: https://payhip.com/b/kcaF9 +The design patterns are now available as an e-book. Find out more about "Open Source Java Design Patterns" here: https://payhip.com/b/bNQFX The project contributors can get the book for free. Contact the maintainer via [Gitter chatroom](https://gitter.im/iluwatar/java-design-patterns) or email (iluwatar (at) gmail (dot) com ). Send a message that contains your email address, Github username, and a link to an accepted pull request. @@ -570,6 +570,19 @@ This project is licensed under the terms of the MIT license. Sanura Hettiarachchi
Sanura Hettiarachchi

💻 Kim Gi Uk
Kim Gi Uk

💻 Suchismita Deb
Suchismita Deb

💻 + ssrijan-007-sys
ssrijan-007-sys

💻 + + + e5LA
e5LA

💻 📖 + Maziyar Gerami
Maziyar Gerami

🌍 + yoobin_mion
yoobin_mion

💻 + Soumik Sarker
Soumik Sarker

📖 + Naman Srivastava
Naman Srivastava

💻 + Thanh Nguyen Duc
Thanh Nguyen Duc

💻 + + + Soham Kamble
Soham Kamble

💻 + Olexandr88
Olexandr88

📖 diff --git a/actor-model/README.md b/actor-model/README.md new file mode 100644 index 000000000000..be8065ffefef --- /dev/null +++ b/actor-model/README.md @@ -0,0 +1,201 @@ +--- +title: "Actor Model Pattern in Java: Building Concurrent Systems with Elegance" +shortTitle: Actor Model +description: "Explore the Actor Model pattern in Java with real-world examples and practical implementation. Learn how to build scalable, message-driven systems using actors, messages, and asynchronous communication." +category: Concurrency +language: en +tag: + - Concurrency + - Messaging + - Isolation + - Asynchronous + - Distributed Systems + - Actor Model +--- + +## Also Known As + +- Message-passing concurrency +- Actor-based concurrency + +--- + +## Intent of Actor Model Pattern + +The Actor Model pattern enables the construction of highly concurrent, distributed, and fault-tolerant systems by using isolated components (actors) that interact exclusively through asynchronous message passing. + +--- + +## Detailed Explanation of Actor Model Pattern with Real-World Examples + +### 📦 Real-world Example + +Imagine a customer service system: +- Each **customer support agent** is an **actor**. +- Customers **send questions (messages)** to agents. +- Each agent handles one request at a time and can **respond asynchronously** without interfering with other agents. + +--- + +### 🧠 In Plain Words + +> "Actors are like independent workers that never share memory and only communicate through messages." + +--- + +### 📖 Wikipedia Says + +> [Actor model](https://en.wikipedia.org/wiki/Actor_model) is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent computation. + +--- + +### 🧹 Architecture Diagram + +![UML Class Diagram](./etc/Actor_Model_UML_Class_Diagram.png) + +--- + +## Programmatic Example of Actor Model Pattern in Java + +### Actor.java + +```java +public abstract class Actor implements Runnable { + + @Setter @Getter private String actorId; + private final BlockingQueue mailbox = new LinkedBlockingQueue<>(); + private volatile boolean active = true; + + + public void send(Message message) { + mailbox.add(message); + } + + public void stop() { + active = false; + } + + @Override + public void run() { + + } + + protected abstract void onReceive(Message message); +} + +``` + +### Message.java + +```java + +@AllArgsConstructor +@Getter +@Setter +public class Message { + private final String content; + private final String senderId; +} +``` + +### ActorSystem.java + +```java +public class ActorSystem { + public void startActor(Actor actor) { + String actorId = "actor-" + idCounter.incrementAndGet(); // Generate a new and unique ID + actor.setActorId(actorId); // assign the actor it's ID + actorRegister.put(actorId, actor); // Register and save the actor with it's ID + executor.submit(actor); // Run the actor in a thread + } + public Actor getActorById(String actorId) { + return actorRegister.get(actorId); // Find by Id + } + + public void shutdown() { + executor.shutdownNow(); // Stop all threads + } +} +``` + +### App.java + +```java +public class App { + public static void main(String[] args) { + ActorSystem system = new ActorSystem(); + Actor srijan = new ExampleActor(system); + Actor ansh = new ExampleActor2(system); + + system.startActor(srijan); + system.startActor(ansh); + ansh.send(new Message("Hello ansh", srijan.getActorId())); + srijan.send(new Message("Hello srijan!", ansh.getActorId())); + + Thread.sleep(1000); // Give time for messages to process + + srijan.stop(); // Stop the actor gracefully + ansh.stop(); + system.shutdown(); // Stop the actor system + } +} +``` + +--- + +## When to Use the Actor Model Pattern in Java + +- When building **concurrent or distributed systems** +- When you want **no shared mutable state** +- When you need **asynchronous, message-driven communication** +- When components should be **isolated and loosely coupled** + +--- + +## Actor Model Pattern Java Tutorials + +- [Baeldung – Akka with Java](https://www.baeldung.com/java-akka) +- [Vaughn Vernon – Reactive Messaging Patterns](https://vaughnvernon.co/?p=1143) + +--- + +## Real-World Applications of Actor Model Pattern in Java + +- [Akka Framework](https://akka.io/) +- [Erlang and Elixir concurrency](https://www.erlang.org/) +- [Microsoft Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/) +- JVM-based game engines and simulators + +--- + +## Benefits and Trade-offs of Actor Model Pattern + +### ✅ Benefits +- High concurrency support +- Easy scaling across threads or machines +- Fault isolation and recovery +- Message ordering within actors + +### ⚠️ Trade-offs +- Harder to debug due to asynchronous behavior +- Slight performance overhead due to message queues +- More complex to design than simple method calls + +--- + +## Related Java Design Patterns + +- [Command Pattern](../command) +- [Mediator Pattern](../mediator) +- [Event-Driven Architecture](../event-driven-architecture) +- [Observer Pattern](../observer) + +--- + +## References and Credits + +- *Programming Erlang*, Joe Armstrong +- *Reactive Design Patterns*, Roland Kuhn +- *The Actor Model in 10 Minutes*, [InfoQ Article](https://www.infoq.com/articles/actor-model/) +- [Akka Documentation](https://doc.akka.io/docs/akka/current/index.html) + diff --git a/actor-model/etc/Actor_Model_UML_Class_Diagram.png b/actor-model/etc/Actor_Model_UML_Class_Diagram.png new file mode 100644 index 000000000000..a4c34d7a75fd Binary files /dev/null and b/actor-model/etc/Actor_Model_UML_Class_Diagram.png differ diff --git a/actor-model/etc/actor-model.urm.puml b/actor-model/etc/actor-model.urm.puml new file mode 100644 index 000000000000..020c1fc735a4 --- /dev/null +++ b/actor-model/etc/actor-model.urm.puml @@ -0,0 +1,35 @@ +@startuml actor-model + +title Actor Model - UML Class Diagram + +class ActorSystem { + +actorOf(actor: Actor): Actor + +shutdown(): void +} + +class Actor { + -mailbox: BlockingQueue + -active: boolean + +send(message: Message): void + +stop(): void + +run(): void + #onReceive(message: Message): void +} + +class ExampleActor { + +onReceive(message: Message): void +} + +class Message { + -content: String + -sender: Actor + +getContent(): String + +getSender(): Actor +} + +ActorSystem --> Actor : creates +Actor <|-- ExampleActor : extends +Actor --> Message : processes +ExampleActor --> Message : uses + +@enduml diff --git a/actor-model/pom.xml b/actor-model/pom.xml new file mode 100644 index 000000000000..76c288829b8d --- /dev/null +++ b/actor-model/pom.xml @@ -0,0 +1,114 @@ + + + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + actor-model + Actor Model + + + + + + org.junit + junit-bom + 5.11.0 + pom + import + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.platform + junit-platform-launcher + test + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + jar-with-dependencies + + + + com.iluwatar.actormodel.App + + + + + + make-assembly + package + + single + + + + + + + + + diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/Actor.java b/actor-model/src/main/java/com/iluwatar/actormodel/Actor.java new file mode 100644 index 000000000000..6e2aaccd1937 --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/Actor.java @@ -0,0 +1,63 @@ +/* + * 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.actormodel; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import lombok.Getter; +import lombok.Setter; + +public abstract class Actor implements Runnable { + + @Setter @Getter private String actorId; + private final BlockingQueue mailbox = new LinkedBlockingQueue<>(); + private volatile boolean active = + true; // always read from main memory and written back to main memory, + + // rather than being cached in a thread's local memory. To make it consistent to all Actors + + public void send(Message message) { + mailbox.add(message); // Add message to queue + } + + public void stop() { + active = false; // Stop the actor loop + } + + @Override + public void run() { + while (active) { + try { + Message message = mailbox.take(); // Wait for a message + onReceive(message); // Process it + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + // Child classes must define what to do with a message + protected abstract void onReceive(Message message); +} diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/ActorSystem.java b/actor-model/src/main/java/com/iluwatar/actormodel/ActorSystem.java new file mode 100644 index 000000000000..db7c21cb6088 --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/ActorSystem.java @@ -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.actormodel; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +public class ActorSystem { + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final ConcurrentHashMap actorRegister = new ConcurrentHashMap<>(); + private final AtomicInteger idCounter = new AtomicInteger(0); + + public void startActor(Actor actor) { + String actorId = "actor-" + idCounter.incrementAndGet(); // Generate a new and unique ID + actor.setActorId(actorId); // assign the actor it's ID + actorRegister.put(actorId, actor); // Register and save the actor with it's ID + executor.submit(actor); // Run the actor in a thread + } + + public Actor getActorById(String actorId) { + return actorRegister.get(actorId); // Find by Id + } + + public void shutdown() { + executor.shutdownNow(); // Stop all threads + } +} diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/App.java b/actor-model/src/main/java/com/iluwatar/actormodel/App.java new file mode 100644 index 000000000000..79fe79e48a6f --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/App.java @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/** + * The Actor Model is a design pattern used to handle concurrency in a safe, scalable, and + * message-driven way. + * + *

In the Actor Model: - An **Actor** is an independent unit that has its own state and behavior. + * - Actors **communicate only through messages** — they do not share memory. - An **ActorSystem** + * is responsible for creating, starting, and managing the lifecycle of actors. - Messages are + * delivered asynchronously, and each actor processes them one at a time. + * + *

💡 Key benefits: - No shared memory = no need for complex thread-safety - Easy to scale with + * many actors - Suitable for highly concurrent or distributed systems + * + *

🔍 This example demonstrates the Actor Model: - `ActorSystem` starts two actors: `srijan` and + * `ansh`. - `ExampleActor` and `ExampleActor2` extend the `Actor` class and override the + * `onReceive()` method to handle messages. - Actors communicate using `send()` to pass `Message` + * objects that include the message content and sender's ID. - The actors process messages + * **asynchronously in separate threads**, and we allow a short delay (`Thread.sleep`) to let them + * run. - The system is shut down gracefully at the end. + */ +package com.iluwatar.actormodel; + +public class App { + public static void main(String[] args) throws InterruptedException { + ActorSystem system = new ActorSystem(); + Actor srijan = new ExampleActor(system); + Actor ansh = new ExampleActor2(system); + + system.startActor(srijan); + system.startActor(ansh); + ansh.send(new Message("Hello ansh", srijan.getActorId())); + srijan.send(new Message("Hello srijan!", ansh.getActorId())); + + Thread.sleep(1000); // Give time for messages to process + + srijan.stop(); // Stop the actor gracefully + ansh.stop(); + system.shutdown(); // Stop the actor system + } +} diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor.java b/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor.java new file mode 100644 index 000000000000..fd49325f44bd --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor.java @@ -0,0 +1,53 @@ +/* + * 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.actormodel; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ExampleActor extends Actor { + private final ActorSystem actorSystem; + @Getter private final List receivedMessages = new ArrayList<>(); + + public ExampleActor(ActorSystem actorSystem) { + this.actorSystem = actorSystem; + } + + // Logger log = Logger.getLogger(getClass().getName()); + + @Override + protected void onReceive(Message message) { + LOGGER.info( + "[{}]Received : {} from : [{}]", getActorId(), message.getContent(), message.getSenderId()); + Actor sender = actorSystem.getActorById(message.getSenderId()); // sender actor id + // Reply of the message + if (sender != null && !message.getSenderId().equals(getActorId())) { + sender.send(new Message("I got your message ", getActorId())); + } + } +} diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor2.java b/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor2.java new file mode 100644 index 000000000000..037f96716558 --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/ExampleActor2.java @@ -0,0 +1,46 @@ +/* + * 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.actormodel; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ExampleActor2 extends Actor { + private final ActorSystem actorSystem; + @Getter private final List receivedMessages = new ArrayList<>(); + + public ExampleActor2(ActorSystem actorSystem) { + this.actorSystem = actorSystem; + } + + @Override + protected void onReceive(Message message) { + receivedMessages.add(message.getContent()); + LOGGER.info("[{}]Received : {}", getActorId(), message.getContent()); + } +} diff --git a/actor-model/src/main/java/com/iluwatar/actormodel/Message.java b/actor-model/src/main/java/com/iluwatar/actormodel/Message.java new file mode 100644 index 000000000000..03ca6e02cac0 --- /dev/null +++ b/actor-model/src/main/java/com/iluwatar/actormodel/Message.java @@ -0,0 +1,35 @@ +/* + * 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.actormodel; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class Message { + private final String content; + private final String senderId; +} diff --git a/actor-model/src/test/java/com/iluwatar/actor/ActorModelTest.java b/actor-model/src/test/java/com/iluwatar/actor/ActorModelTest.java new file mode 100644 index 000000000000..a4a0dee569ab --- /dev/null +++ b/actor-model/src/test/java/com/iluwatar/actor/ActorModelTest.java @@ -0,0 +1,63 @@ +/* + * 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.actor; + +import static org.junit.jupiter.api.Assertions.*; + +import com.iluwatar.actormodel.ActorSystem; +import com.iluwatar.actormodel.App; +import com.iluwatar.actormodel.ExampleActor; +import com.iluwatar.actormodel.ExampleActor2; +import com.iluwatar.actormodel.Message; +import org.junit.jupiter.api.Test; + +public class ActorModelTest { + @Test + void testMainMethod() throws InterruptedException { + App.main(new String[] {}); + } + + @Test + public void testMessagePassing() throws InterruptedException { + ActorSystem system = new ActorSystem(); + + ExampleActor srijan = new ExampleActor(system); + ExampleActor2 ansh = new ExampleActor2(system); + + system.startActor(srijan); + system.startActor(ansh); + + // Ansh recieves a message from Srijan + ansh.send(new Message("Hello ansh", srijan.getActorId())); + + // Wait briefly to allow async processing + Thread.sleep(200); + + // Check that Srijan received the message + assertTrue( + ansh.getReceivedMessages().contains("Hello ansh"), + "ansh should receive the message from Srijan"); + } +} diff --git a/backpressure/pom.xml b/backpressure/pom.xml index fcc15892fb8a..8f6a54178799 100644 --- a/backpressure/pom.xml +++ b/backpressure/pom.xml @@ -55,7 +55,7 @@ io.projectreactor reactor-test - 3.8.0-M1 + 3.8.0-RC1 test diff --git a/dao-factory/README.md b/dao-factory/README.md new file mode 100644 index 000000000000..c3ee3c5e893d --- /dev/null +++ b/dao-factory/README.md @@ -0,0 +1,360 @@ +--- +title: "DAO Factory Pattern: Flexible Data Access Layer for Seamless Data Source Switching" +shortTitle: DAO Factory +description: "Learn the Data Access Object Pattern combine with Abstract Factory Pattern in Java with real-world examples, class diagrams, and tutorials. Understand its intent, applicability, benefits, and known uses to enhance your design pattern knowledge." +category: Structural +language: en +tag: + - Abstraction + - Data access + - Layer architecture + - Persistence +--- + +## Also known as + +* DAO Factory +* Factory for Data Access Object strategy using Abstract Factory + + +## Intent of Data Access Object Factory Design Pattern + +The DAO Factory combines the Data Access Object and Abstract Factory patterns to seperate business logic from data access logic, while increasing flexibility when switching between different data sources. + +## Detailed Explanation of Data Access Object Factory Pattern with Real-World Examples + +Real-world example + +> A real-world analogy for the DAO Factory pattern is a multilingual customer service center. Imagine a bank that serves customers speaking different languages—English, French, and Spanish. When a customer calls, an automated system first detects the customer's preferred language, then routes the call to the appropriate support team that speaks that language. Each team follows the same company policies (standard procedures), but handles interactions in a language-specific way. +> +> In the same way, the DAO Factory pattern uses a factory to determine the correct set of DAO implementations based on the data source (e.g., MySQL, MongoDB). Each DAO factory returns a group of DAOs tailored to a specific data source, all conforming to the same interfaces. This allows the application to interact with any supported database in a consistent manner, without changing the business logic—just like how the customer service system handles multiple languages while following the same support protocols. + +In plain words + +> The DAO Factory pattern abstracts the creation of Data Access Objects (DAOs), allowing you to request a specific DAO from a central factory without worrying about its underlying implementation. This makes the code easier to maintain and flexible to change, especially when switching between databases or storage mechanisms. + +Wikipedia says + +> The Data Access Object (DAO) design pattern is a structural pattern that provides an abstract interface to some type of database or other persistence mechanism. By mapping application calls to the persistence layer, the DAO provides some specific data operations without exposing details of the database. The DAO Factory is an extension of this concept, responsible for generating the required DAO implementations. + +Class diagram + +![Data Access Object Factory class diagram](./etc/dao-factory.png "Data Access Object Factory class diagram") + +## Programmatic Example of Data Access Object Factory in Java + +In this example, the persistence object represents a Customer. + +We are considering a flexible storage strategy where the application should be able to work with three different types of data sources: an H2 in-memory relational database (RDBMS), a MongoDB (object-oriented database), and a JSON flat file (flat file storage). + +``` java +public enum DataSourceType { +H2, +Mongo, +FlatFile +} +``` + +First, we define a Customer class that will be persisted in different storage systems. The ID field is generic to maintain compatibility with both relational and object-oriented databases. + +``` java +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Customer implements Serializable { +private T id; +private String name; +} +``` + +Next, we define a CustomerDAO interface that outlines the standard CRUD operations on the Customer model. This interface will have three concrete implementations, each corresponding to a specific data source: H2 in-memory database, MongoDB, and JSON file. + +``` java +public interface CustomerDAO { + + void save(Customer customer); + + void update(Customer customer); + + void delete(T id); + + List> findAll(); + + Optional> findById(T id); +} +``` + +Here is the implementations + +``` java +@Slf4j +@RequiredArgsConstructor +public class H2CustomerDAO implements CustomerDAO { +private final DataSource dataSource; +private final String INSERT_CUSTOMER = "INSERT INTO customer(id, name) VALUES (?, ?)"; +private final String UPDATE_CUSTOMER = "UPDATE customer SET name = ? WHERE id = ?"; +private final String DELETE_CUSTOMER = "DELETE FROM customer WHERE id = ?"; +private final String SELECT_CUSTOMER_BY_ID = "SELECT * FROM customer WHERE id= ?"; +private final String SELECT_ALL_CUSTOMERS = "SELECT * FROM customer"; +private final String CREATE_SCHEMA = +"CREATE TABLE IF NOT EXISTS customer (id BIGINT PRIMARY KEY, name VARCHAR(255))"; +private final String DROP_SCHEMA = "DROP TABLE IF EXISTS customer"; + + @Override + public void save(Customer customer) { + // Implement operation save for H2 + } + + @Override + public void update(Customer customer) { + // Implement operation save for H2 + } + + @Override + public void delete(Long id) { + // Implement operation delete for H2 + } + + @Override + public List> findAll() { + // Implement operation find all for H2 + } + + @Override + public Optional> findById(Long id) { + // Implement operation find by id for H2 + } +} +``` + +``` java +@Slf4j +@RequiredArgsConstructor +public class MongoCustomerDAO implements CustomerDAO { +private final MongoCollection customerCollection; + + // Implement CRUD operation with MongoDB data source +} +``` + +``` java +@Slf4j +@RequiredArgsConstructor +public class FlatFileCustomerDAO implements CustomerDAO { + private final Path filePath; + private final Gson gson; + Type customerListType = new TypeToken>>() { + }.getType(); + + // Implement CRUD operation with Flat file data source +} +``` + +After that, we create an abstract class DAOFactory that defines two key methods: a static method getDataSource() and an abstract method createCustomerDAO(). + +- The getDataSource() method is a factory selector—it returns a concrete DAOFactory instance based on the type of data source requested. + +- Each subclass of DAOFactory will implement the createCustomerDAO() method to provide the corresponding CustomerDAO implementation. + +``` java +public abstract class DAOFactory { + public static DAOFactory getDataSource(DataSourceType dataSourceType) { + return switch (dataSourceType) { + case H2 -> new H2DataSourceFactory(); + case Mongo -> new MongoDataSourceFactory(); + case FlatFile -> new FlatFileDataSourceFactory(); + }; + } + + public abstract CustomerDAO createCustomerDAO(); +} +``` + +We then implement three specific factory classes: + +H2DataSourceFactory for H2 in-memory RDBMS +``` java +public class H2DataSourceFactory extends DAOFactory { + private final String DB_URL = "jdbc:h2:~/test"; + private final String USER = "sa"; + private final String PASS = ""; + + @Override + public CustomerDAO createCustomerDAO() { + return new H2CustomerDAO(createDataSource()); + } + + private DataSource createDataSource() { + var dataSource = new JdbcDataSource(); + dataSource.setURL(DB_URL); + dataSource.setUser(USER); + dataSource.setPassword(PASS); + return dataSource; + } +} +``` + +MongoDataSourceFactory for MongoDB +``` java +public class MongoDataSourceFactory extends DAOFactory { + private final String CONN_STR = "mongodb://localhost:27017/"; + private final String DB_NAME = "dao_factory"; + private final String COLLECTION_NAME = "customer"; + + @Override + public CustomerDAO createCustomerDAO() { + try { + MongoClient mongoClient = MongoClients.create(CONN_STR); + MongoDatabase database = mongoClient.getDatabase(DB_NAME); + MongoCollection customerCollection = database.getCollection(COLLECTION_NAME); + return new MongoCustomerDAO(customerCollection); + } catch (RuntimeException e) { + throw new RuntimeException("Error: " + e); + } + } +} +``` + +FlatFileDataSourceFactory for flat file storage using JSON +``` java +public class FlatFileDataSourceFactory extends DAOFactory { + private final String FILE_PATH = System.getProperty("user.home") + "/Desktop/customer.json"; + @Override + public CustomerDAO createCustomerDAO() { + Path filePath = Paths.get(FILE_PATH); + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .create(); + return new FlatFileCustomerDAO(filePath, gson); + } +} +``` + +Finally, in the main function of client code, we will demonstrate CRUD operations on the Customer using three data source type. +``` java + // Perform CRUD H2 Database + LOGGER.debug("H2 - Create customer"); + performCreateCustomer(customerDAO, + List.of(customerInmemory1, customerInmemory2, customerInmemory3)); + LOGGER.debug("H2 - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateInmemory); + LOGGER.debug("H2 - Delete customer"); + performDeleteCustomer(customerDAO, 3L); + deleteSchema(customerDAO); + + // Perform CRUD MongoDb + daoFactory = DAOFactory.getDataSource(DataSourceType.Mongo); + customerDAO = daoFactory.createCustomerDAO(); + LOGGER.debug("Mongo - Create customer"); + performCreateCustomer(customerDAO, List.of(customer4, customer5)); + LOGGER.debug("Mongo - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateMongo); + LOGGER.debug("Mongo - Delete customer"); + performDeleteCustomer(customerDAO, idCustomerMongo2); + deleteSchema(customerDAO); + + // Perform CRUD Flat file + daoFactory = DAOFactory.getDataSource(DataSourceType.FlatFile); + customerDAO = daoFactory.createCustomerDAO(); + LOGGER.debug("Flat file - Create customer"); + performCreateCustomer(customerDAO, + List.of(customerFlatFile1, customerFlatFile2, customerFlatFile3)); + LOGGER.debug("Flat file - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateFlatFile); + LOGGER.debug("Flat file - Delete customer"); + performDeleteCustomer(customerDAO, 3L); + deleteSchema(customerDAO); +``` + +The program output +``` java +17:17:24.368 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Create customer +17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Green) +17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red) +17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Blue) +17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Update customer +17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Yellow) +17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red) +17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Blue) +17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Delete customer +17:17:24.632 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Yellow) +17:17:24.632 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red) +17:17:24.747 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Create customer +17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca) +17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c2, name=Elliot) +17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Update customer +17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca) +17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c2, name=Henry) +17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Delete customer +17:17:24.850 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca) +17:17:24.876 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Create customer +17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Duc) +17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang) +17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Nhat) +17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Update customer +17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Thanh) +17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang) +17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Nhat) +17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Delete customer +17:17:24.898 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Thanh) +17:17:24.898 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang) +``` +## When to Use the Data Access Object Factory Pattern in Java + +Use the DAO Factory Pattern when: + +* The application needs to support multiple types of storage (RDBMS, NoSQL, file system, etc.) with minimal changes to business logic. +* You want to abstract and isolate persistence logic from the core application logic. +* You aim to make your data access layer pluggable and easy to extend with new storage technologies. +* You want to enable easier unit testing and dependency injection by providing mock implementations of DAOs. +* Runtime configuration (e.g., via environment variables or application settings) determines which data source to use. + +## Data Access Object Factory Pattern Java Tutorials + +* [Core J2EE Patterns - Data Access Object (Oracle)](https://www.oracle.com/java/technologies/dataaccessobject.html) +* [DAO Factories: Java Design Patterns (Youtube)](https://www.youtube.com/watch?v=5HGe9s9qM-o) +* [Java Design Patterns and Architecture (CaveofProgramming)](https://caveofprogramming.teachable.com/courses/2084/lectures/39549) + +## Real-World Applications of Data Access Object Factory Pattern in Java + +* Enterprise Java Applications: Where switching between test, dev, and production databases is common (e.g., MySQL ↔ MongoDB ↔ In-Memory). +* Spring Data JPA & Repository Abstraction: Though Spring provides its own abstraction, the concept is similar to DAO factory for modular and pluggable persistence. +* Microservices with Varying Storage Backends: Different microservices might store data in SQL, NoSQL, or even flat files; using a DAO Factory per service ensures consistency. +* Data Integration Tools: Tools that support importing/exporting from various formats (CSV, JSON, databases) often use DAO factories behind the scenes. +* Framework-Level Implementations: Custom internal frameworks where persistence layers need to support multiple database types. + +## Benefits and Trade-offs of Data Access Object Factory Pattern + +Benefits: + +* Abstraction of Data Source Logic: Client code interacts only with DAO interfaces, completely decoupled from how and where the data is stored. +* Flexibility in Persistence Strategy: Easily switch between databases (e.g., H2, MongoDB, flat files) by changing the factory configuration. +* Improved Maintainability: Storage logic for each data source is encapsulated within its own DAO implementation and factory, making it easier to update or extend. +* Code Reusability: Common data access logic (e.g., CRUD operations) can be reused across different implementations and projects. +* Testability: DAOs and factories can be mocked or stubbed easily, which supports unit testing and dependency injection. + +Trade-offs: +* Increased Complexity: Introducing abstract DAOs and multiple factory classes adds structural complexity to the codebase. +* Boilerplate Code: Requires defining many interfaces and implementations, even for simple data access needs. +* Less Transparent Behavior: Since clients access DAOs indirectly via factories, understanding the concrete data source behavior may require deeper inspection. + +## Related Java Design Patterns + +* [Factory Method](https://java-design-patterns.com/patterns/factory-method/): DAO Factory is a concrete application of the Factory Pattern, used to create DAO objects in a flexible way. +* [Abstract Factory](https://java-design-patterns.com/patterns/abstract-factory/): When supporting multiple data sources (e.g., MySQLDAO, OracleDAO), DAO Factory can act as an Abstract Factory. +* [Data Access Object (DAO)](https://java-design-patterns.com/patterns/data-access-object/): The core pattern managed by DAO Factory, it separates data access logic from business logic. +* [Singleton](https://java-design-patterns.com/patterns/singleton/): DAO Factory is often implemented as a Singleton to ensure only one instance manages DAO creation. +* [Service Locator](https://java-design-patterns.com/patterns/service-locator/): Can be used alongside DAO Factory to retrieve DAO services efficiently. +* [Dependency Injection](https://java-design-patterns.com/patterns/dependency-injection/): In frameworks like Spring, DAOs are typically injected into the service layer instead of being retrieved from a factory. + + +## References and Credits + +* [DAO Factory - J2EE Design Patterns Book](https://www.oreilly.com/library/view/j2ee-design-patterns/0596004273/re15.html) +* [DAO Factory patterns with Hibernate](http://www.giuseppeurso.eu/en/dao-factory-patterns-with-hibernate/) +* [Design Patterns - Java Means DURGA SOFT](https://www.scribd.com/document/407219980/2-DAO-Factory-Design-Pattern) +* [Generic DAO pattern - Hibernate](https://in.relation.to/2005/09/09/generic-dao-pattern-with-jdk-50/) + \ No newline at end of file diff --git a/dao-factory/etc/dao-factory.png b/dao-factory/etc/dao-factory.png new file mode 100644 index 000000000000..d93547d79957 Binary files /dev/null and b/dao-factory/etc/dao-factory.png differ diff --git a/dao-factory/etc/dao-factory.puml b/dao-factory/etc/dao-factory.puml new file mode 100644 index 000000000000..5196c5a1ebd9 --- /dev/null +++ b/dao-factory/etc/dao-factory.puml @@ -0,0 +1,74 @@ +@startuml +package com.iluwatar.daofactory { + class App { + {static} void main(String[] args) + } + + class Customer { + T id + String name + } + + interface CustomerDAO { + void save(Customer customer) + void update(Customer customer) + void delete(ID id) + List> findAll() + Optional> findById(ID id) + void deleteSchema() + } + + abstract class DAOFactory { + {static} DAOFactory getDataSource(DataSourceType dataType) + {abstract} CustomerDAO createCustomerDAO() + } + + enum DataSourceType { + H2 + Mongo + FlatFile + } + + class FlatFileCustomerDAO implements CustomerDAO { + void save(Customer customer) + void update(Customer customer) + void delete(Long id) + List> findAll() + Optional> findById(Long id) + void deleteSchema() + } + + class H2CustomerDAO implements CustomerDAO { + void save(Customer customer) + void update(Customer customer) + void delete(Long id) + List> findAll() + Optional> findById(Long id) + void deleteSchema() + } + + class FlatFileDataSourceFactory extends DAOFactory { + CustomerDAO createCustomerDAO() + } + + class H2DataSourceFactory extends DAOFactory { + CustomerDAO createCustomerDAO() + } + + class MongoCustomerDAO implements CustomerDAO { + void save(Customer customer) + void update(Customer customer) + void delete(ObjectId id) + List> findAll() + Optional> findById(ObjectId id) + void deleteSchema() + } + class MongoDataSourceFactory extends DAOFactory { + CustomerDAO createCustomerDAO() + } + + DataSourceType ..+ DAOFactory + DAOFactory ..+ App + App --> Customer + } +@enduml \ No newline at end of file diff --git a/dao-factory/pom.xml b/dao-factory/pom.xml new file mode 100644 index 000000000000..2e771e736688 --- /dev/null +++ b/dao-factory/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + dao-factory + + + 21 + 21 + UTF-8 + + + + + com.h2database + h2 + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.mongodb + mongodb-driver-legacy + + + com.google.code.gson + gson + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/App.java b/dao-factory/src/main/java/com/iluwatar/daofactory/App.java new file mode 100644 index 000000000000..b80d3c5ac56a --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/App.java @@ -0,0 +1,124 @@ +/* + * 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.daofactory; + +import java.io.Serializable; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; + +@Slf4j +public class App { + + public static void main(String[] args) { + var daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.H2); + CustomerDAO customerDAO = daoFactory.createCustomerDAO(); + + // Perform CRUD H2 Database + if (customerDAO instanceof H2CustomerDAO h2CustomerDAO) { + h2CustomerDAO.deleteSchema(); + h2CustomerDAO.createSchema(); + } + Customer customerInmemory1 = new Customer<>(1L, "Green"); + Customer customerInmemory2 = new Customer<>(2L, "Red"); + Customer customerInmemory3 = new Customer<>(3L, "Blue"); + Customer customerUpdateInmemory = new Customer<>(1L, "Yellow"); + + LOGGER.debug("H2 - Create customer"); + performCreateCustomer( + customerDAO, List.of(customerInmemory1, customerInmemory2, customerInmemory3)); + LOGGER.debug("H2 - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateInmemory); + LOGGER.debug("H2 - Delete customer"); + performDeleteCustomer(customerDAO, 3L); + deleteSchema(customerDAO); + + // Perform CRUD MongoDb + daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.MONGO); + customerDAO = daoFactory.createCustomerDAO(); + ObjectId idCustomerMongo1 = new ObjectId(); + ObjectId idCustomerMongo2 = new ObjectId(); + Customer customer4 = new Customer<>(idCustomerMongo1, "Masca"); + Customer customer5 = new Customer<>(idCustomerMongo2, "Elliot"); + Customer customerUpdateMongo = new Customer<>(idCustomerMongo2, "Henry"); + + LOGGER.debug("Mongo - Create customer"); + performCreateCustomer(customerDAO, List.of(customer4, customer5)); + LOGGER.debug("Mongo - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateMongo); + LOGGER.debug("Mongo - Delete customer"); + performDeleteCustomer(customerDAO, idCustomerMongo2); + deleteSchema(customerDAO); + + // Perform CRUD Flat file + daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.FLAT_FILE); + customerDAO = daoFactory.createCustomerDAO(); + Customer customerFlatFile1 = new Customer<>(1L, "Duc"); + Customer customerFlatFile2 = new Customer<>(2L, "Quang"); + Customer customerFlatFile3 = new Customer<>(3L, "Nhat"); + Customer customerUpdateFlatFile = new Customer<>(1L, "Thanh"); + LOGGER.debug("Flat file - Create customer"); + performCreateCustomer( + customerDAO, List.of(customerFlatFile1, customerFlatFile2, customerFlatFile3)); + LOGGER.debug("Flat file - Update customer"); + performUpdateCustomer(customerDAO, customerUpdateFlatFile); + LOGGER.debug("Flat file - Delete customer"); + performDeleteCustomer(customerDAO, 3L); + deleteSchema(customerDAO); + } + + public static void deleteSchema(CustomerDAO customerDAO) { + customerDAO.deleteSchema(); + } + + public static void performCreateCustomer( + CustomerDAO customerDAO, List> customerList) { + for (Customer customer : customerList) { + customerDAO.save(customer); + } + List> customers = customerDAO.findAll(); + for (Customer customer : customers) { + LOGGER.debug(customer.toString()); + } + } + + public static void performUpdateCustomer( + CustomerDAO customerDAO, Customer customerUpdate) { + customerDAO.update(customerUpdate); + List> customers = customerDAO.findAll(); + for (Customer customer : customers) { + LOGGER.debug(customer.toString()); + } + } + + public static void performDeleteCustomer( + CustomerDAO customerDAO, T customerId) { + customerDAO.delete(customerId); + List> customers = customerDAO.findAll(); + for (Customer customer : customers) { + LOGGER.debug(customer.toString()); + } + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/CustomException.java b/dao-factory/src/main/java/com/iluwatar/daofactory/CustomException.java new file mode 100644 index 000000000000..9559e765c7d4 --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/CustomException.java @@ -0,0 +1,36 @@ +/* + * 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.daofactory; + +/** Customer exception */ +public class CustomException extends RuntimeException { + public CustomException(String message) { + super(message); + } + + public CustomException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/Customer.java b/dao-factory/src/main/java/com/iluwatar/daofactory/Customer.java new file mode 100644 index 000000000000..95b675487d27 --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/Customer.java @@ -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.daofactory; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * A customer generic POJO that represents the data that can be stored in any supported data source. + * This class is designed t work with various ID types (e.g., Long, String, or ObjectId) through + * generic, making it adaptable to different persistence system. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Customer implements Serializable { + private T id; + private String name; +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/CustomerDAO.java b/dao-factory/src/main/java/com/iluwatar/daofactory/CustomerDAO.java new file mode 100644 index 000000000000..34316b4c49af --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/CustomerDAO.java @@ -0,0 +1,85 @@ +/* + * 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.daofactory; + +import java.io.Serializable; +import java.util.List; +import java.util.Optional; + +/** + * The Data Access Object (DAO) pattern provides an abstraction layer between the application and + * the database. It encapsulates data access logic, allowing the application to work with domain + * objects instead of direct database operations. + * + *

Implementations handle specific storage mechanisms (e.g., in-memory, databases) while keeping + * client code unchanged. + * + * @see H2CustomerDAO + * @see MongoCustomerDAO + * @see FlatFileCustomerDAO + */ +public interface CustomerDAO { + /** + * Persist the given customer + * + * @param customer the customer to persist + */ + void save(Customer customer); + + /** + * Update the given customer + * + * @param customer the customer to update + */ + void update(Customer customer); + + /** + * Delete the customer with the given id + * + * @param id the id of the customer to delete + */ + void delete(T id); + + /** + * Find all customers + * + * @return a list of customers + */ + List> findAll(); + + /** + * Find the customer with the given id + * + * @param id the id of the customer to find + * @return the customer with the given id + */ + Optional> findById(T id); + + /** + * Delete the customer schema. After executing the statements, this function will be called to + * clean up the data and delete the records. + */ + void deleteSchema(); +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactory.java b/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactory.java new file mode 100644 index 000000000000..e7d33186bec5 --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactory.java @@ -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.daofactory; + +/** + * An abstract factory class that provides a way to create concrete DAO (Data Access Object) + * factories for different data sources types (e.g., H2, Mongo, FlatFile). + * + *

This class follows the Abstract Factory design pattern, allowing applications to retrieve the + * approriate DAO implementation without being tightly coupled to a specific data source. + * + * @see H2DataSourceFactory + * @see MongoDataSourceFactory + * @see FlatFileDataSourceFactory + */ +public abstract class DAOFactory { + /** + * Retrieves a {@link CustomerDAO} implementation specific to the underlying data source.. + * + * @return A data source-specific implementation of {@link CustomerDAO} + */ + public abstract CustomerDAO createCustomerDAO(); +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactoryProvider.java b/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactoryProvider.java new file mode 100644 index 000000000000..08585622d00d --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactoryProvider.java @@ -0,0 +1,62 @@ +/* + * 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.daofactory; + +/** + * {@code DAOFactoryProvider} is a utility class responsible for providing concrete implementations + * of the {@link DAOFactory} interface based on the specified data source type. + * + *

This class acts as an entry point to obtain DAO factories for different storage mechanisms + * such as relational databases (e.g., H2), document stores (e.g., MongoDB), or file-based systems. + * It uses the {@link DataSourceType} enumeration to determine which concrete factory to + * instantiate. + * + *

Example usage: + * + *

{@code
+ * DAOFactory factory = DAOFactoryProvider.getDataSource(DataSourceType.H2);
+ * }
+ */ +public class DAOFactoryProvider { + + private DAOFactoryProvider() {} + + /** + * Returns a concrete {@link DAOFactory} intance based on the specified data source type. + * + * @param dataSourceType The type of data source for which a factory is needed. Supported values: + * {@code H2}, {@code Mongo}, {@code FlatFile} + * @return A {@link DAOFactory} implementation corresponding to the given data source type. + * @throws IllegalArgumentException if the given data source type is not supported. + */ + public static DAOFactory getDataSource(DataSourceType dataSourceType) { + return switch (dataSourceType) { + case H2 -> new H2DataSourceFactory(); + case MONGO -> new MongoDataSourceFactory(); + case FLAT_FILE -> new FlatFileDataSourceFactory(); + default -> throw new IllegalArgumentException("Unsupported data source type"); + }; + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/DataSourceType.java b/dao-factory/src/main/java/com/iluwatar/daofactory/DataSourceType.java new file mode 100644 index 000000000000..da01d451f09e --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/DataSourceType.java @@ -0,0 +1,32 @@ +/* + * 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.daofactory; + +/** Enumerates the types of data sources supported by the application. */ +public enum DataSourceType { + H2, + MONGO, + FLAT_FILE +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileCustomerDAO.java b/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileCustomerDAO.java new file mode 100644 index 000000000000..8f1f1f144f77 --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileCustomerDAO.java @@ -0,0 +1,175 @@ +/* + * 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.daofactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * A Flat File implementation of {@link CustomerDAO}, which store the customer data in a JSON file + * at path {@code ~/Desktop/customer.json}. + */ +@Slf4j +@RequiredArgsConstructor +public class FlatFileCustomerDAO implements CustomerDAO { + private final Path filePath; + private final Gson gson; + Type customerListType = new TypeToken>>() {}.getType(); + + protected Reader createReader(Path filePath) throws IOException { + return new FileReader(filePath.toFile()); + } + + protected Writer createWriter(Path filePath) throws IOException { + return new FileWriter(filePath.toFile()); + } + + /** {@inheritDoc} */ + @Override + public void save(Customer customer) { + List> customers = new LinkedList<>(); + if (filePath.toFile().exists()) { + try (Reader reader = createReader(filePath)) { + customers = gson.fromJson(reader, customerListType); + } catch (IOException ex) { + throw new CustomException("Failed to read customer data", ex); + } + } + customers.add(customer); + try (Writer writer = createWriter(filePath)) { + gson.toJson(customers, writer); + } catch (IOException ex) { + throw new CustomException("Failed to write customer data", ex); + } + } + + /** {@inheritDoc} */ + @Override + public void update(Customer customer) { + if (!filePath.toFile().exists()) { + throw new CustomException("File not found"); + } + List> customers; + try (Reader reader = createReader(filePath)) { + customers = gson.fromJson(reader, customerListType); + } catch (IOException ex) { + throw new CustomException("Failed to read customer data", ex); + } + customers.stream() + .filter(c -> c.getId().equals(customer.getId())) + .findFirst() + .ifPresentOrElse( + c -> c.setName(customer.getName()), + () -> { + throw new CustomException("Customer not found with id: " + customer.getId()); + }); + try (Writer writer = createWriter(filePath)) { + gson.toJson(customers, writer); + } catch (IOException ex) { + throw new CustomException("Failed to write customer data", ex); + } + } + + /** {@inheritDoc} */ + @Override + public void delete(Long id) { + if (!filePath.toFile().exists()) { + throw new CustomException("File not found"); + } + List> customers; + try (Reader reader = createReader(filePath)) { + customers = gson.fromJson(reader, customerListType); + } catch (IOException ex) { + throw new CustomException("Failed to read customer data", ex); + } + Customer customerToRemove = + customers.stream() + .filter(c -> c.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new CustomException("Customer not found with id: " + id)); + customers.remove(customerToRemove); + try (Writer writer = createWriter(filePath)) { + gson.toJson(customers, writer); + } catch (IOException ex) { + throw new CustomException("Failed to write customer data", ex); + } + } + + /** {@inheritDoc} */ + @Override + public List> findAll() { + if (!filePath.toFile().exists()) { + throw new CustomException("File not found"); + } + List> customers; + try (Reader reader = createReader(filePath)) { + customers = gson.fromJson(reader, customerListType); + } catch (IOException ex) { + throw new CustomException("Failed to read customer data", ex); + } + return customers; + } + + /** {@inheritDoc} */ + @Override + public Optional> findById(Long id) { + if (!filePath.toFile().exists()) { + throw new CustomException("File not found"); + } + List> customers = null; + try (Reader reader = createReader(filePath)) { + customers = gson.fromJson(reader, customerListType); + } catch (IOException ex) { + throw new CustomException("Failed to read customer data", ex); + } + return customers.stream().filter(c -> c.getId().equals(id)).findFirst(); + } + + /** {@inheritDoc} */ + @Override + public void deleteSchema() { + if (!filePath.toFile().exists()) { + throw new CustomException("File not found"); + } + try { + Files.delete(filePath); + } catch (IOException ex) { + throw new CustomException("Failed to delete customer data"); + } + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileDataSourceFactory.java b/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileDataSourceFactory.java new file mode 100644 index 000000000000..f423376703b5 --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileDataSourceFactory.java @@ -0,0 +1,43 @@ +/* + * 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.daofactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** FlatFileDataSourceFactory concrete factory. */ +public class FlatFileDataSourceFactory extends DAOFactory { + private static final String FILE_PATH = + System.getProperty("user.home") + "/Desktop/customer.json"; + + @Override + public CustomerDAO createCustomerDAO() { + Path filePath = Paths.get(FILE_PATH); + Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); + return new FlatFileCustomerDAO(filePath, gson); + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/H2CustomerDAO.java b/dao-factory/src/main/java/com/iluwatar/daofactory/H2CustomerDAO.java new file mode 100644 index 000000000000..fe027426391c --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/H2CustomerDAO.java @@ -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.daofactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.sql.DataSource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * An implementation of {@link CustomerDAO} that uses H2 database (http://www.h2database.com/) which + * is an in-memory database and data will lost after application exits. + */ +@Slf4j +@RequiredArgsConstructor +public class H2CustomerDAO implements CustomerDAO { + private final DataSource dataSource; + private static final String INSERT_CUSTOMER = "INSERT INTO customer(id, name) VALUES (?, ?)"; + private static final String UPDATE_CUSTOMER = "UPDATE customer SET name = ? WHERE id = ?"; + private static final String DELETE_CUSTOMER = "DELETE FROM customer WHERE id = ?"; + private static final String SELECT_CUSTOMER_BY_ID = + "SELECT customer.id, customer.name FROM customer WHERE id= ?"; + private static final String SELECT_ALL_CUSTOMERS = "SELECT customer.* FROM customer"; + private static final String CREATE_SCHEMA = + "CREATE TABLE IF NOT EXISTS customer (id BIGINT PRIMARY KEY, name VARCHAR(255))"; + private static final String DROP_SCHEMA = "DROP TABLE IF EXISTS customer"; + + /** {@inheritDoc} */ + @Override + public void save(Customer customer) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement saveStatement = connection.prepareStatement(INSERT_CUSTOMER)) { + saveStatement.setLong(1, customer.getId()); + saveStatement.setString(2, customer.getName()); + saveStatement.execute(); + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + } + + /** {@inheritDoc} */ + @Override + public void update(Customer customer) { + if (Objects.isNull(customer) || Objects.isNull(customer.getId())) { + throw new CustomException("Custome null or customer id null"); + } + try (Connection connection = dataSource.getConnection(); + PreparedStatement selectStatement = connection.prepareStatement(SELECT_CUSTOMER_BY_ID); + PreparedStatement updateStatement = connection.prepareStatement(UPDATE_CUSTOMER)) { + selectStatement.setLong(1, customer.getId()); + try (ResultSet resultSet = selectStatement.executeQuery()) { + if (!resultSet.next()) { + throw new CustomException("Customer not found with id: " + customer.getId()); + } + } + updateStatement.setString(1, customer.getName()); + updateStatement.setLong(2, customer.getId()); + updateStatement.executeUpdate(); + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + } + + /** {@inheritDoc} */ + @Override + public void delete(Long id) { + if (Objects.isNull(id)) { + throw new CustomException("Customer id null"); + } + try (Connection connection = dataSource.getConnection(); + PreparedStatement selectStatement = connection.prepareStatement(SELECT_CUSTOMER_BY_ID); + PreparedStatement deleteStatement = connection.prepareStatement(DELETE_CUSTOMER)) { + selectStatement.setLong(1, id); + try (ResultSet resultSet = selectStatement.executeQuery()) { + if (!resultSet.next()) { + throw new CustomException("Customer not found with id: " + id); + } + } + deleteStatement.setLong(1, id); + deleteStatement.execute(); + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + } + + /** {@inheritDoc} */ + @Override + public List> findAll() { + List> customers = new LinkedList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement selectStatement = connection.prepareStatement(SELECT_ALL_CUSTOMERS)) { + try (ResultSet resultSet = selectStatement.executeQuery()) { + while (resultSet.next()) { + Long idCustomer = resultSet.getLong("id"); + String nameCustomer = resultSet.getString("name"); + customers.add(new Customer<>(idCustomer, nameCustomer)); + } + } + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + return customers; + } + + /** {@inheritDoc} */ + @Override + public Optional> findById(Long id) { + if (Objects.isNull(id)) { + throw new CustomException("Customer id null"); + } + Customer customer = null; + try (Connection connection = dataSource.getConnection(); + PreparedStatement selectByIdStatement = + connection.prepareStatement(SELECT_CUSTOMER_BY_ID)) { + selectByIdStatement.setLong(1, id); + try (ResultSet resultSet = selectByIdStatement.executeQuery()) { + while (resultSet.next()) { + Long idCustomer = resultSet.getLong("id"); + String nameCustomer = resultSet.getString("name"); + customer = new Customer<>(idCustomer, nameCustomer); + } + } + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + return Optional.ofNullable(customer); + } + + /** Create customer schema. */ + public void createSchema() { + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute(CREATE_SCHEMA); + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + } + + /** {@inheritDoc}} */ + @Override + public void deleteSchema() { + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); ) { + statement.execute(DROP_SCHEMA); + } catch (SQLException e) { + throw new CustomException(e.getMessage(), e); + } + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/H2DataSourceFactory.java b/dao-factory/src/main/java/com/iluwatar/daofactory/H2DataSourceFactory.java new file mode 100644 index 000000000000..dbb39dd98f3b --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/H2DataSourceFactory.java @@ -0,0 +1,48 @@ +/* + * 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.daofactory; + +import javax.sql.DataSource; +import org.h2.jdbcx.JdbcDataSource; + +/** H2DataSourceFactory concrete factory. */ +public class H2DataSourceFactory extends DAOFactory { + private static final String DB_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"; + private static final String USER = "sa"; + private static final String PASS = ""; + + @Override + public CustomerDAO createCustomerDAO() { + return new H2CustomerDAO(createDataSource()); + } + + private DataSource createDataSource() { + var dataSource = new JdbcDataSource(); + dataSource.setURL(DB_URL); + dataSource.setUser(USER); + dataSource.setPassword(PASS); + return dataSource; + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/MongoCustomerDAO.java b/dao-factory/src/main/java/com/iluwatar/daofactory/MongoCustomerDAO.java new file mode 100644 index 000000000000..1870f61e85fd --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/MongoCustomerDAO.java @@ -0,0 +1,106 @@ +/* + * 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.daofactory; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Updates; +import com.mongodb.client.result.DeleteResult; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; + +/** An implementation of {@link CustomerDAO} that uses MongoDB (https://www.mongodb.com/) */ +@Slf4j +@RequiredArgsConstructor +public class MongoCustomerDAO implements CustomerDAO { + private final MongoCollection customerCollection; + + /** {@inheritDoc} */ + @Override + public void save(Customer customer) { + Document customerDocument = new Document("_id", customer.getId()); + customerDocument.append("name", customer.getName()); + customerCollection.insertOne(customerDocument); + } + + /** {@inheritDoc} */ + @Override + public void update(Customer customer) { + Document updateQuery = new Document("_id", customer.getId()); + Bson update = Updates.set("name", customer.getName()); + customerCollection.updateOne(updateQuery, update); + } + + /** {@inheritDoc} */ + @Override + public void delete(ObjectId objectId) { + Bson deleteQuery = Filters.eq("_id", objectId); + DeleteResult deleteResult = customerCollection.deleteOne(deleteQuery); + if (deleteResult.getDeletedCount() == 0) { + throw new CustomException("Delete failed: No document found with id: " + objectId); + } + } + + /** {@inheritDoc} */ + @Override + public List> findAll() { + List> customers = new LinkedList<>(); + FindIterable customerDocuments = customerCollection.find(); + for (Document customerDocument : customerDocuments) { + Customer customer = + new Customer<>( + (ObjectId) customerDocument.get("_id"), customerDocument.getString("name")); + customers.add(customer); + } + return customers; + } + + /** {@inheritDoc} */ + @Override + public Optional> findById(ObjectId objectId) { + Bson filter = Filters.eq("_id", objectId); + Document customerDocument = customerCollection.find(filter).first(); + Customer customerResult = null; + if (customerDocument != null) { + customerResult = + new Customer<>( + (ObjectId) customerDocument.get("_id"), customerDocument.getString("name")); + } + return Optional.ofNullable(customerResult); + } + + /** {@inheritDoc} */ + @Override + public void deleteSchema() { + customerCollection.drop(); + } +} diff --git a/dao-factory/src/main/java/com/iluwatar/daofactory/MongoDataSourceFactory.java b/dao-factory/src/main/java/com/iluwatar/daofactory/MongoDataSourceFactory.java new file mode 100644 index 000000000000..5a7b1f1b1ece --- /dev/null +++ b/dao-factory/src/main/java/com/iluwatar/daofactory/MongoDataSourceFactory.java @@ -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.daofactory; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.bson.types.ObjectId; + +/** MongoDataSourceFactory concrete factory. */ +public class MongoDataSourceFactory extends DAOFactory { + private static final String CONN_STR = "mongodb://localhost:27017/"; + private static final String DB_NAME = "dao_factory"; + private static final String COLLECTION_NAME = "customer"; + + @Override + public CustomerDAO createCustomerDAO() { + try { + MongoClient mongoClient = MongoClients.create(CONN_STR); + MongoDatabase database = mongoClient.getDatabase(DB_NAME); + MongoCollection customerCollection = database.getCollection(COLLECTION_NAME); + return new MongoCustomerDAO(customerCollection); + } catch (CustomException e) { + throw new CustomException("Error: " + e); + } + } +} diff --git a/dao-factory/src/main/resources/logback.xml b/dao-factory/src/main/resources/logback.xml new file mode 100644 index 000000000000..f82341ebb2ab --- /dev/null +++ b/dao-factory/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{15}) %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/dao-factory/src/test/java/com/iluwatar/daofactory/AppTest.java b/dao-factory/src/test/java/com/iluwatar/daofactory/AppTest.java new file mode 100644 index 000000000000..12efea42bdc6 --- /dev/null +++ b/dao-factory/src/test/java/com/iluwatar/daofactory/AppTest.java @@ -0,0 +1,94 @@ +/* + * 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.daofactory; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** {@link App} */ +class AppTest { + /** Test perform CRUD in main class */ + private CustomerDAO mockLongCustomerDAO; + + private CustomerDAO mockObjectIdCustomerDAO; + + @BeforeEach + void setUp() { + mockLongCustomerDAO = mock(CustomerDAO.class); + mockObjectIdCustomerDAO = mock(CustomerDAO.class); + } + + @Test + void testPerformCreateCustomerWithLongId() { + Customer c1 = new Customer<>(1L, "Test1"); + Customer c2 = new Customer<>(2L, "Test2"); + + when(mockLongCustomerDAO.findAll()).thenReturn(List.of(c1, c2)); + + App.performCreateCustomer(mockLongCustomerDAO, List.of(c1, c2)); + + verify(mockLongCustomerDAO).save(c1); + verify(mockLongCustomerDAO).save(c2); + verify(mockLongCustomerDAO).findAll(); + } + + @Test + void testPerformUpdateCustomerWithObjectId() { + ObjectId id = new ObjectId(); + Customer updatedCustomer = new Customer<>(id, "Updated"); + + when(mockObjectIdCustomerDAO.findAll()).thenReturn(List.of(updatedCustomer)); + + App.performUpdateCustomer(mockObjectIdCustomerDAO, updatedCustomer); + + verify(mockObjectIdCustomerDAO).update(updatedCustomer); + verify(mockObjectIdCustomerDAO).findAll(); + } + + @Test + void testPerformDeleteCustomerWithLongId() { + Long id = 100L; + Customer remainingCustomer = new Customer<>(1L, "Remaining"); + + when(mockLongCustomerDAO.findAll()).thenReturn(List.of(remainingCustomer)); + + App.performDeleteCustomer(mockLongCustomerDAO, id); + + verify(mockLongCustomerDAO).delete(id); + verify(mockLongCustomerDAO).findAll(); + } + + @Test + void testDeleteSchema() { + App.deleteSchema(mockLongCustomerDAO); + verify(mockLongCustomerDAO).deleteSchema(); + } +} diff --git a/dao-factory/src/test/java/com/iluwatar/daofactory/DAOFactoryTest.java b/dao-factory/src/test/java/com/iluwatar/daofactory/DAOFactoryTest.java new file mode 100644 index 000000000000..f8aaf199762d --- /dev/null +++ b/dao-factory/src/test/java/com/iluwatar/daofactory/DAOFactoryTest.java @@ -0,0 +1,54 @@ +/* + * 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.daofactory; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.junit.jupiter.api.Test; + +/** {@link DAOFactory} */ +class DAOFactoryTest { + + @Test + void verifyH2CustomerDAOCreation() { + var daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.H2); + var customerDAO = daoFactory.createCustomerDAO(); + assertInstanceOf(H2CustomerDAO.class, customerDAO); + } + + @Test + void verifyMongoCustomerDAOCreation() { + var daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.MONGO); + var customerDAO = daoFactory.createCustomerDAO(); + assertInstanceOf(MongoCustomerDAO.class, customerDAO); + } + + @Test + void verifyFlatFileCustomerDAOCreation() { + var daoFactory = DAOFactoryProvider.getDataSource(DataSourceType.FLAT_FILE); + var customerDAO = daoFactory.createCustomerDAO(); + assertInstanceOf(FlatFileCustomerDAO.class, customerDAO); + } +} diff --git a/dao-factory/src/test/java/com/iluwatar/daofactory/FlatFileCustomerDAOTest.java b/dao-factory/src/test/java/com/iluwatar/daofactory/FlatFileCustomerDAOTest.java new file mode 100644 index 000000000000..470964f4217a --- /dev/null +++ b/dao-factory/src/test/java/com/iluwatar/daofactory/FlatFileCustomerDAOTest.java @@ -0,0 +1,500 @@ +/* + * 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.daofactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +/** {@link FlatFileCustomerDAO} */ +class FlatFileCustomerDAOTest { + private Path filePath; + private File file; + private Gson gson; + + private final Type customerListType = new TypeToken>>() {}.getType(); + private final Customer existingCustomer = new Customer<>(1L, "Thanh"); + private FlatFileCustomerDAO flatFileCustomerDAO; + private FileReader fileReader; + private FileWriter fileWriter; + + @BeforeEach + void setUp() { + filePath = mock(Path.class); + file = mock(File.class); + gson = mock(Gson.class); + fileReader = mock(FileReader.class); + fileWriter = mock(FileWriter.class); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + return fileWriter; + } + }; + when(filePath.toFile()).thenReturn(file); + } + + /** Class test with scenario Save Customer */ + @Nested + class Save { + @Test + void giveFilePathNotExist_whenSaveCustomer_thenCreateNewFileWithCustomer() { + when(file.exists()).thenReturn(false); + flatFileCustomerDAO.save(existingCustomer); + + verify(gson) + .toJson( + argThat( + (List> list) -> + list.size() == 1 && list.getFirst().equals(existingCustomer)), + eq(fileWriter)); + } + + @Test + void givenEmptyFileExist_whenSaveCustomer_thenAddCustomer() { + when(file.exists()).thenReturn(true); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(new LinkedList<>()); + flatFileCustomerDAO.save(existingCustomer); + + verify(gson).fromJson(fileReader, customerListType); + verify(gson) + .toJson( + argThat( + (List> list) -> + list.size() == 1 && list.getFirst().equals(existingCustomer)), + eq(fileWriter)); + } + + @Test + void givenFileWithCustomerExist_whenSaveCustomer_thenShouldAppendCustomer() { + List> customers = new LinkedList<>(); + customers.add(new Customer<>(2L, "Duc")); + customers.add(new Customer<>(3L, "Nguyen")); + when(file.exists()).thenReturn(true); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(customers); + + flatFileCustomerDAO.save(existingCustomer); + + verify(gson).fromJson(fileReader, customerListType); + verify(gson).toJson(argThat((List> list) -> list.size() == 3), eq(fileWriter)); + } + + @Test + void whenReadFails_thenThrowException() { + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + throw new IOException("Failed to read file"); + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + when(file.exists()).thenReturn(true); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.save(existingCustomer)); + } + + @Test + void whenWriteFails_thenThrowException() { + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(new LinkedList<>()); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + throw new IOException("Failed to write file"); + } + }; + when(file.exists()).thenReturn(true); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.save(existingCustomer)); + } + } + + /** Class test with scenario Update Customer */ + @Nested + class Update { + @Test + void givenFilePathNotExist_whenUpdateCustomer_thenThrowException() { + when(file.exists()).thenReturn(false); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.update(existingCustomer)); + } + + @Test + void whenReadFails_thenThrowException() { + when(file.exists()).thenReturn(true); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + throw new IOException("Failed to read file"); + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.update(existingCustomer)); + } + + @Test + void whenWriteFails_thenThrowException() { + when(file.exists()).thenReturn(true); + when(gson.fromJson(any(Reader.class), eq(customerListType))) + .thenReturn( + new LinkedList<>() { + { + add(new Customer<>(1L, "Quang")); + } + }); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + throw new IOException("Failed to write file"); + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.update(existingCustomer)); + } + + @Test + void givenValidCustomer_whenUpdateCustomer_thenUpdateSucceed() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + return fileWriter; + } + }; + flatFileCustomerDAO.update(existingCustomer); + verify(gson) + .toJson( + argThat( + (List> customers) -> + customers.size() == 1 + && customers.stream() + .anyMatch(c -> c.getId().equals(1L) && c.getName().equals("Thanh"))), + eq(fileWriter)); + } + + @Test + void givenIdCustomerNotExist_whenUpdateCustomer_thenThrowException() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(2L, "Quang")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.update(existingCustomer)); + } + } + + /** Class test with scenario Delete Customer */ + @Nested + class Delete { + @Test + void givenFilePathNotExist_whenDeleteCustomer_thenThrowException() { + when(file.exists()).thenReturn(false); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.delete(1L)); + } + + @Test + void whenReadFails_thenThrowException() { + when(file.exists()).thenReturn(true); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + throw new IOException("Failed to read file"); + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.delete(1L)); + } + + @Test + void whenWriteFails_thenThrowException() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) throws IOException { + throw new IOException("Failed to write file"); + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.delete(1L)); + } + + @Test + void givenValidId_whenDeleteCustomer_thenDeleteSucceed() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + existingListCustomer.add(new Customer<>(2L, "Thanh")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + + flatFileCustomerDAO.delete(1L); + assertEquals(1, existingListCustomer.size()); + verify(gson) + .toJson( + argThat( + (List> customers) -> + customers.stream() + .noneMatch(c -> c.getId().equals(1L) && c.getName().equals("Quang"))), + eq(fileWriter)); + } + + @Test + void givenIdNotExist_whenDeleteCustomer_thenThrowException() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + existingListCustomer.add(new Customer<>(2L, "Thanh")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) { + return fileReader; + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.delete(3L)); + } + } + + /** Class test with scenario Find All Customer */ + @Nested + class FindAll { + @Test + void givenFileNotExist_thenThrowException() { + when(file.exists()).thenReturn(false); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.findAll()); + } + + @Test + void whenReadFails_thenThrowException() { + when(file.exists()).thenReturn(true); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + throw new IOException("Failed to read file"); + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.findAll()); + } + + @Test + void givenEmptyCustomer_thenReturnEmptyList() { + when(file.exists()).thenReturn(true); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(new LinkedList<>()); + List> customers = flatFileCustomerDAO.findAll(); + assertEquals(0, customers.size()); + verify(gson).fromJson(fileReader, customerListType); + } + + @Test + void givenCustomerExist_thenReturnCustomerList() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + existingListCustomer.add(new Customer<>(2L, "Thanh")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + List> customers = flatFileCustomerDAO.findAll(); + assertEquals(2, customers.size()); + } + } + + /** Class test with scenario Find By Id Customer */ + @Nested + class FindById { + + @Test + void givenFilePathNotExist_whenFindById_thenThrowException() { + when(file.exists()).thenReturn(false); + assertThrows(CustomException.class, () -> flatFileCustomerDAO.findById(1L)); + } + + @Test + void whenReadFails_thenThrowException() { + when(file.exists()).thenReturn(true); + flatFileCustomerDAO = + new FlatFileCustomerDAO(filePath, gson) { + @Override + protected Reader createReader(Path filePath) throws IOException { + throw new IOException("Failed to read file"); + } + + @Override + protected Writer createWriter(Path filePath) { + return fileWriter; + } + }; + assertThrows(CustomException.class, () -> flatFileCustomerDAO.findById(1L)); + } + + @Test + void givenIdCustomerExist_whenFindById_thenReturnCustomer() { + when(file.exists()).thenReturn(true); + List> existingListCustomer = new LinkedList<>(); + existingListCustomer.add(new Customer<>(1L, "Quang")); + existingListCustomer.add(new Customer<>(2L, "Thanh")); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(existingListCustomer); + Optional> customer = flatFileCustomerDAO.findById(1L); + assertTrue(customer.isPresent()); + assertEquals("Quang", customer.get().getName()); + } + + @Test + void givenIdCustomerNotExist_whenFindById_thenReturnEmpty() { + when(file.exists()).thenReturn(true); + when(gson.fromJson(any(Reader.class), eq(customerListType))).thenReturn(new LinkedList<>()); + Optional> customers = flatFileCustomerDAO.findById(1L); + assertTrue(customers.isEmpty()); + } + } + + /** Clas test with scenario Delete schema */ + @Nested + class DeleteSchema { + @Test + void givenFilePathExist_thenDeleteFile() { + when(file.exists()).thenReturn(true); + + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + flatFileCustomerDAO.deleteSchema(); + mockedFiles.verify(() -> Files.delete(filePath), times(1)); + } + } + + @Test + void givenFilePathNotExist_thenThrowException() { + when(file.exists()).thenReturn(false); + + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + assertThrows(CustomException.class, () -> flatFileCustomerDAO.deleteSchema()); + mockedFiles.verify(() -> Files.delete(filePath), times(0)); + } + } + } +} diff --git a/dao-factory/src/test/java/com/iluwatar/daofactory/H2CustomerDAOTest.java b/dao-factory/src/test/java/com/iluwatar/daofactory/H2CustomerDAOTest.java new file mode 100644 index 000000000000..ce7def36e5bc --- /dev/null +++ b/dao-factory/src/test/java/com/iluwatar/daofactory/H2CustomerDAOTest.java @@ -0,0 +1,300 @@ +/* + * 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.daofactory; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import javax.sql.DataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** Tests {@link H2CustomerDAO} */ +class H2CustomerDAOTest { + private static final String DB_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + private static final String USER = "sa"; + private static final String PASS = ""; + private static final String CREATE_SCHEMA = + "CREATE TABLE IF NOT EXISTS customer (id BIGINT PRIMARY KEY, name VARCHAR(255))"; + private static final String DROP_SCHEMA = "DROP TABLE IF EXISTS customer"; + private final Customer existingCustomer = new Customer<>(1L, "Nguyen"); + private H2CustomerDAO h2CustomerDAO; + + @BeforeEach + void createSchema() throws SQLException { + try (var connection = DriverManager.getConnection(DB_URL, USER, PASS); + var statement = connection.createStatement()) { + statement.execute(CREATE_SCHEMA); + } + } + + @AfterEach + void deleteSchema() throws SQLException { + try (var connection = DriverManager.getConnection(DB_URL, USER, PASS); + var statement = connection.createStatement()) { + statement.execute(DROP_SCHEMA); + } + } + + /** Class test for scenario connect with datasource succeed */ + @Nested + class ConnectionSucceed { + + @BeforeEach + void setUp() { + var dataSource = new JdbcDataSource(); + dataSource.setURL(DB_URL); + dataSource.setUser(USER); + dataSource.setPassword(PASS); + h2CustomerDAO = new H2CustomerDAO(dataSource); + assertDoesNotThrow(() -> h2CustomerDAO.save(existingCustomer)); + var customer = h2CustomerDAO.findById(existingCustomer.getId()); + assertTrue(customer.isPresent()); + assertEquals(customer.get().getName(), existingCustomer.getName()); + assertEquals(customer.get().getId(), existingCustomer.getId()); + } + + @Nested + class SaveCustomer { + @Test + void givenValidCustomer_whenSaveCustomer_thenAddSucceed() { + var customer = new Customer<>(2L, "Duc"); + assertDoesNotThrow(() -> h2CustomerDAO.save(customer)); + var customerInDb = h2CustomerDAO.findById(customer.getId()); + assertTrue(customerInDb.isPresent()); + assertEquals(customerInDb.get().getName(), customer.getName()); + assertEquals(customerInDb.get().getId(), customer.getId()); + List> customers = h2CustomerDAO.findAll(); + assertEquals(2, customers.size()); + } + + @Test + void givenIdCustomerDuplicated_whenSaveCustomer_thenThrowException() { + var customer = new Customer<>(existingCustomer.getId(), "Duc"); + assertThrows(CustomException.class, () -> h2CustomerDAO.save(customer)); + List> customers = h2CustomerDAO.findAll(); + assertEquals(1, customers.size()); + } + } + + @Nested + class UpdateCustomer { + @Test + void givenValidCustomer_whenUpdateCustomer_thenUpdateSucceed() { + var customerUpdate = new Customer<>(existingCustomer.getId(), "Duc"); + assertDoesNotThrow(() -> h2CustomerDAO.update(customerUpdate)); + var customerInDb = h2CustomerDAO.findById(customerUpdate.getId()); + assertTrue(customerInDb.isPresent()); + assertEquals(customerInDb.get().getName(), customerUpdate.getName()); + } + + @Test + void givenIdCustomerNotExist_whenUpdateCustomer_thenThrowException() { + var customerUpdate = new Customer<>(100L, "Duc"); + var customerInDb = h2CustomerDAO.findById(customerUpdate.getId()); + assertTrue(customerInDb.isEmpty()); + assertThrows(CustomException.class, () -> h2CustomerDAO.update(customerUpdate)); + } + + @Test + void givenNull_whenUpdateCustomer_thenThrowException() { + assertThrows(CustomException.class, () -> h2CustomerDAO.update(null)); + List> customers = h2CustomerDAO.findAll(); + assertEquals(1, customers.size()); + } + } + + @Nested + class DeleteCustomer { + @Test + void givenValidId_whenDeleteCustomer_thenDeleteSucceed() { + assertDoesNotThrow(() -> h2CustomerDAO.delete(existingCustomer.getId())); + var customerInDb = h2CustomerDAO.findById(existingCustomer.getId()); + assertTrue(customerInDb.isEmpty()); + List> customers = h2CustomerDAO.findAll(); + assertEquals(0, customers.size()); + } + + @Test + void givenIdCustomerNotExist_whenDeleteCustomer_thenThrowException() { + var customerInDb = h2CustomerDAO.findById(100L); + assertTrue(customerInDb.isEmpty()); + assertThrows(CustomException.class, () -> h2CustomerDAO.delete(100L)); + List> customers = h2CustomerDAO.findAll(); + assertEquals(1, customers.size()); + assertEquals(existingCustomer.getName(), customers.get(0).getName()); + assertEquals(existingCustomer.getId(), customers.get(0).getId()); + } + + @Test + void givenNull_whenDeleteCustomer_thenThrowException() { + assertThrows(CustomException.class, () -> h2CustomerDAO.delete(null)); + List> customers = h2CustomerDAO.findAll(); + assertEquals(1, customers.size()); + assertEquals(existingCustomer.getName(), customers.get(0).getName()); + } + } + + @Nested + class FindAllCustomers { + @Test + void givenNonCustomerInDb_whenFindAllCustomer_thenReturnEmptyList() { + assertDoesNotThrow(() -> h2CustomerDAO.delete(existingCustomer.getId())); + List> customers = h2CustomerDAO.findAll(); + assertEquals(0, customers.size()); + } + + @Test + void givenCustomerExistInDb_whenFindAllCustomer_thenReturnCustomers() { + List> customers = h2CustomerDAO.findAll(); + assertEquals(1, customers.size()); + assertEquals(existingCustomer.getName(), customers.get(0).getName()); + assertEquals(existingCustomer.getId(), customers.get(0).getId()); + } + } + + @Nested + class FindCustomerById { + @Test + void givenValidId_whenFindById_thenReturnCustomer() { + var customerInDb = h2CustomerDAO.findById(existingCustomer.getId()); + assertTrue(customerInDb.isPresent()); + assertEquals(existingCustomer.getName(), customerInDb.get().getName()); + assertEquals(existingCustomer.getId(), customerInDb.get().getId()); + } + + @Test + void givenIdCustomerNotExist_whenFindById_thenReturnEmpty() { + var customerNotExist = h2CustomerDAO.findById(100L); + assertTrue(customerNotExist.isEmpty()); + } + + @Test + void givenNull_whenFindById_thenThrowException() { + assertThrows(CustomException.class, () -> h2CustomerDAO.findById(null)); + } + } + + @Nested + class CreateSchema { + @Test + void whenCreateSchema_thenNotThrowException() { + assertDoesNotThrow(() -> h2CustomerDAO.createSchema()); + } + } + + @Nested + class DeleteSchema { + @Test + void whenDeleteSchema_thenNotThrowException() { + assertDoesNotThrow(() -> h2CustomerDAO.deleteSchema()); + } + } + } + + /** Class test with scenario connect with data source failed */ + @Nested + class ConnectionFailed { + private static final String EXCEPTION_CAUSE = "Connection not available"; + + @BeforeEach + void setUp() throws SQLException { + h2CustomerDAO = new H2CustomerDAO(mockedDataSource()); + } + + private DataSource mockedDataSource() throws SQLException { + var mockedDataSource = mock(DataSource.class); + var mockedConnection = mock(Connection.class); + var exception = new SQLException(EXCEPTION_CAUSE); + doThrow(exception).when(mockedConnection).prepareStatement(Mockito.anyString()); + doThrow(exception).when(mockedConnection).createStatement(); + doReturn(mockedConnection).when(mockedDataSource).getConnection(); + return mockedDataSource; + } + + @Test + void givenValidCustomer_whenSaveCustomer_thenThrowException() { + var customer = new Customer<>(2L, "Duc"); + CustomException exception = + assertThrows(CustomException.class, () -> h2CustomerDAO.save(customer)); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void givenValidCustomer_whenUpdateCustomer_thenThrowException() { + var customerUpdate = new Customer<>(existingCustomer.getId(), "Duc"); + CustomException exception = + assertThrows(CustomException.class, () -> h2CustomerDAO.update(customerUpdate)); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void givenValidId_whenDeleteCustomer_thenThrowException() { + Long idCustomer = existingCustomer.getId(); + CustomException exception = + assertThrows(CustomException.class, () -> h2CustomerDAO.delete(idCustomer)); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void whenFindAll_thenThrowException() { + CustomException exception = assertThrows(CustomException.class, h2CustomerDAO::findAll); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void whenFindById_thenThrowException() { + Long idCustomer = existingCustomer.getId(); + CustomException exception = + assertThrows(CustomException.class, () -> h2CustomerDAO.findById(idCustomer)); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void whenCreateSchema_thenThrowException() { + CustomException exception = assertThrows(CustomException.class, h2CustomerDAO::createSchema); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + + @Test + void whenDeleteSchema_thenThrowException() { + CustomException exception = assertThrows(CustomException.class, h2CustomerDAO::deleteSchema); + assertEquals(EXCEPTION_CAUSE, exception.getMessage()); + } + } +} diff --git a/dao-factory/src/test/java/com/iluwatar/daofactory/MongoCustomerDAOTest.java b/dao-factory/src/test/java/com/iluwatar/daofactory/MongoCustomerDAOTest.java new file mode 100644 index 000000000000..c56e72c30389 --- /dev/null +++ b/dao-factory/src/test/java/com/iluwatar/daofactory/MongoCustomerDAOTest.java @@ -0,0 +1,163 @@ +/* + * 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.daofactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.model.Filters; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.UpdateResult; +import java.util.List; +import java.util.Optional; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; + +/** Tests {@link MongoCustomerDAO} */ +class MongoCustomerDAOTest { + MongoCollection customerCollection = mock(MongoCollection.class); + MongoCustomerDAO mongoCustomerDAO = new MongoCustomerDAO(customerCollection); + + @Test + void givenValidCustomer_whenSaveCustomer_thenSaveSucceed() { + Customer customer = new Customer<>(new ObjectId(), "John"); + mongoCustomerDAO.save(customer); + verify(customerCollection) + .insertOne( + argThat( + document -> + document.get("_id").equals(customer.getId()) + && document.get("name").equals(customer.getName()))); + } + + @Test + void givenValidCustomer_whenUpdateCustomer_thenUpdateSucceed() { + ObjectId customerId = new ObjectId(); + Customer customerUpdated = new Customer<>(customerId, "John"); + when(customerCollection.updateOne(any(Bson.class), any(Bson.class))) + .thenReturn(UpdateResult.acknowledged(1L, 1L, null)); + mongoCustomerDAO.update(customerUpdated); + verify(customerCollection) + .updateOne( + argThat( + (Bson filter) -> { + Document filterDoc = (Document) filter; + return filterDoc.getObjectId("_id").equals(customerId); + }), + argThat( + (Bson update) -> { + BsonDocument bsonDoc = update.toBsonDocument(); + BsonDocument setDoc = bsonDoc.getDocument("$set"); + return setDoc.getString("name").getValue().equals(customerUpdated.getName()); + })); + } + + @Test + void givenValidObjectId_whenDeleteCustomer_thenDeleteSucceed() { + ObjectId customerId = new ObjectId(); + when(customerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1)); + mongoCustomerDAO.delete(customerId); + verify(customerCollection) + .deleteOne( + argThat( + (Bson filter) -> { + BsonDocument filterDoc = filter.toBsonDocument(); + return filterDoc.getObjectId("_id").getValue().equals(customerId); + })); + } + + @Test + void givenIdNotExist_whenDeleteCustomer_thenThrowException() { + ObjectId customerId = new ObjectId(); + when(customerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0)); + assertThrows(CustomException.class, () -> mongoCustomerDAO.delete(customerId)); + verify(customerCollection) + .deleteOne( + argThat( + (Bson filter) -> { + BsonDocument filterDoc = filter.toBsonDocument(); + return filterDoc.getObjectId("_id").getValue().equals(customerId); + })); + } + + @Test + void findAll_thenReturnAllCustomers() { + FindIterable findIterable = mock(FindIterable.class); + MongoCursor cursor = mock(MongoCursor.class); + Document customerDoc1 = new Document("_id", new ObjectId()).append("name", "Duc"); + Document customerDoc2 = new Document("_id", new ObjectId()).append("name", "Thanh"); + when(customerCollection.find()).thenReturn(findIterable); + when(findIterable.iterator()).thenReturn(cursor); + when(cursor.hasNext()).thenReturn(true, true, false); + when(cursor.next()).thenReturn(customerDoc1, customerDoc2); + List> customerList = mongoCustomerDAO.findAll(); + assertEquals(2, customerList.size()); + verify(customerCollection).find(); + } + + @Test + void givenValidId_whenFindById_thenReturnCustomer() { + FindIterable findIterable = mock(FindIterable.class); + ObjectId customerId = new ObjectId(); + String customerName = "Duc"; + Document customerDoc = new Document("_id", customerId).append("name", customerName); + when(customerCollection.find(Filters.eq("_id", customerId))).thenReturn(findIterable); + when(findIterable.first()).thenReturn(customerDoc); + + Optional> customer = mongoCustomerDAO.findById(customerId); + assertTrue(customer.isPresent()); + assertEquals(customerId, customer.get().getId()); + assertEquals(customerName, customer.get().getName()); + } + + @Test + void givenNotExistingId_whenFindById_thenReturnEmpty() { + FindIterable findIterable = mock(FindIterable.class); + ObjectId customerId = new ObjectId(); + when(customerCollection.find(Filters.eq("_id", customerId))).thenReturn(findIterable); + when(findIterable.first()).thenReturn(null); + Optional> customer = mongoCustomerDAO.findById(customerId); + assertTrue(customer.isEmpty()); + verify(customerCollection).find(Filters.eq("_id", customerId)); + } + + @Test + void whenDeleteSchema_thenDeleteCollection() { + mongoCustomerDAO.deleteSchema(); + verify(customerCollection).drop(); + } +} diff --git a/data-access-object/README.md b/data-access-object/README.md index bd020252d253..7e84299e23e1 100644 --- a/data-access-object/README.md +++ b/data-access-object/README.md @@ -199,10 +199,6 @@ The program output: 10:02:09.898 [main] INFO com.iluwatar.dao.App -- customerDao.getAllCustomers(): java.util.stream.ReferencePipeline$Head@f2f2cc1 ``` -## Detailed Explanation of Data Access Object Pattern with Real-World Examples - -![Data Access Object](./etc/dao.png "Data Access Object") - ## When to Use the Data Access Object Pattern in Java Use the Data Access Object in any of the following situations: diff --git a/data-locality/README.md b/data-locality/README.md index e9f556b8ad5c..a59103b3f89b 100644 --- a/data-locality/README.md +++ b/data-locality/README.md @@ -128,10 +128,6 @@ The console output: In this way, the data-locality module demonstrates the Data Locality pattern. By updating all components of the same type together, it increases the likelihood that the data needed for the update is already in the cache, thereby improving performance. -## Detailed Explanation of Data Locality Pattern with Real-World Examples - -![Data Locality](./etc/data-locality.urm.png "Data Locality pattern class diagram") - ## When to Use the Data Locality Pattern in Java This pattern is applicable in scenarios where large datasets are processed and performance is critical. It's particularly useful in: diff --git a/dependency-injection/README.md b/dependency-injection/README.md index c3fc2c15a977..c9a2848bdfde 100644 --- a/dependency-injection/README.md +++ b/dependency-injection/README.md @@ -118,10 +118,6 @@ The program output: 11:54:05.308 [main] INFO com.iluwatar.dependency.injection.Tobacco -- GuiceWizard smoking RivendellTobacco ``` -## Detailed Explanation of Dependency Injection Pattern with Real-World Examples - -![Dependency Injection](./etc/dependency-injection.png "Dependency Injection") - ## When to Use the Dependency Injection Pattern in Java * When aiming to reduce the coupling between classes and increase the modularity of the application. diff --git a/dynamic-proxy/pom.xml b/dynamic-proxy/pom.xml index 236723c52682..decbb24fd02d 100644 --- a/dynamic-proxy/pom.xml +++ b/dynamic-proxy/pom.xml @@ -46,7 +46,7 @@ com.fasterxml.jackson.core jackson-core - 2.18.2 + 2.19.0 com.fasterxml.jackson.core @@ -56,7 +56,7 @@ org.springframework spring-web - 7.0.0-M3 + 7.0.0-M4 org.junit.jupiter diff --git a/event-sourcing/pom.xml b/event-sourcing/pom.xml index 4cfd05d7adac..5660054d8195 100644 --- a/event-sourcing/pom.xml +++ b/event-sourcing/pom.xml @@ -50,7 +50,7 @@ com.fasterxml.jackson.core jackson-core - 2.18.2 + 2.19.0 com.fasterxml.jackson.core diff --git a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/App.java b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/App.java index 28b039cc7738..88ff5c55fa56 100644 --- a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/App.java +++ b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/App.java @@ -1,3 +1,27 @@ +/* + * 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.leaderfollowers; import java.security.SecureRandom; diff --git a/localization/fa/abstract-document/README.md b/localization/fa/abstract-document/README.md new file mode 100644 index 000000000000..7097ffc8b4ea --- /dev/null +++ b/localization/fa/abstract-document/README.md @@ -0,0 +1,243 @@ +--- +title: "الگوی Abstract Document در جاوا: ساده‌سازی مدیریت داده با انعطاف‌پذیری" +shortTitle: Abstract Document +description: "الگوی طراحی Abstract Document در جاوا را بررسی کنید. با هدف، توضیح، کاربرد، مزایا و نمونه‌های دنیای واقعی برای پیاده‌سازی ساختارهای داده‌ای پویا و انعطاف‌پذیر آشنا شوید." +category: Structural +language: fa +tag: + - Abstraction + - Decoupling + - Dynamic typing + - Encapsulation + - Extensibility + - Polymorphism +--- + +## هدف الگوی طراحی Abstract Document + +الگوی طراحی Abstract Document در جاوا یک الگوی طراحی ساختاری مهم است که راهی یکپارچه برای مدیریت ساختارهای داده‌ای سلسله‌مراتبی و درخت‌ی فراهم می‌کند، با تعریف یک واسط مشترک برای انواع مختلف اسناد. این الگو ساختار اصلی سند را از فرمت‌های خاص داده جدا می‌کند، که باعث به‌روزرسانی پویا و نگهداری ساده‌تر می‌شود. + +## توضیح دقیق الگوی Abstract Document با نمونه‌های دنیای واقعی + +الگوی طراحی Abstract Document در جاوا امکان مدیریت پویا ویژگی‌های پویا(غیر استاتیک) را فراهم می‌کند. این الگو از مفهوم traits استفاده می‌کند تا ایمنی نوع‌داده (type safety) را فراهم کرده و ویژگی‌های کلاس‌های مختلف را به مجموعه‌ای از واسط‌ها تفکیک کند. + +مثال دنیای واقعی + +> فرض کنید یک سیستم کتابخانه از الگوی Abstract Document در جاوا استفاده می‌کند، جایی که کتاب‌ها می‌توانند فرمت‌ها و ویژگی‌های متنوعی داشته باشند: کتاب‌های فیزیکی، کتاب‌های الکترونیکی، و کتاب‌های صوتی. هر فرمت ویژگی‌های خاص خود را دارد، مانند تعداد صفحات برای کتاب‌های فیزیکی، حجم فایل برای کتاب‌های الکترونیکی، و مدت‌زمان برای کتاب‌های صوتی. الگوی Abstract Document به سیستم کتابخانه اجازه می‌دهد تا این فرمت‌های متنوع را به‌صورت انعطاف‌پذیر مدیریت کند. با استفاده از این الگو، سیستم می‌تواند ویژگی‌ها را به‌صورت پویا ذخیره و بازیابی کند، بدون نیاز به ساختار سفت و سخت برای هر نوع کتاب، و این کار افزودن فرمت‌ها یا ویژگی‌های جدید را در آینده بدون تغییرات عمده در کد آسان می‌سازد. + +به زبان ساده + +> الگوی Abstract Document اجازه می‌دهد ویژگی‌هایی به اشیاء متصل شوند بدون اینکه خود آن اشیاء از آن اطلاع داشته باشند. + +ویکی‌پدیا می‌گوید + +> یک الگوی طراحی ساختاری شی‌ء‌گرا برای سازماندهی اشیاء در کلید-مقدارهایی با تایپ آزاد و ارائه داده‌ها از طریق نمای تایپ است. هدف این الگو دستیابی به انعطاف‌پذیری بالا بین اجزا در یک زبان strongly typed است که در آن بتوان ویژگی‌های جدیدی را به‌صورت پویا به ساختار درختی اشیاء اضافه کرد، بدون از دست دادن پشتیبانی از type safety. این الگو از traits برای جداسازی ویژگی‌های مختلف یک کلاس در اینترفیس‌های متفاوت استفاده می‌کند. + +نمودار کلاس + +![Abstract Document class diagram](./etc/abstract-document.png "Abstract Document class diagram") + +## مثال برنامه‌نویسی از الگوی Abstract Document در جاوا + +فرض کنید یک خودرو داریم که از قطعات مختلفی تشکیل شده است. اما نمی‌دانیم آیا این خودرو خاص واقعاً همه قطعات را دارد یا فقط برخی از آن‌ها. خودروهای ما پویا و بسیار انعطاف‌پذیر هستند. + +بیایید ابتدا کلاس‌های پایه `Document` و `AbstractDocument` را تعریف کنیم. این کلاس‌ها اساساً یک شیء را قادر می‌سازند تا یک نقشه از ویژگی‌ها و هر تعداد شیء فرزند را نگه دارد. + +```java +public interface Document { + + Void put(String key, Object value); + + Object get(String key); + + Stream children(String key, Function, T> constructor); +} + +public abstract class AbstractDocument implements Document { + + private final Map properties; + + protected AbstractDocument(Map properties) { + Objects.requireNonNull(properties, "properties map is required"); + this.properties = properties; + } + + @Override + public Void put(String key, Object value) { + properties.put(key, value); + return null; + } + + @Override + public Object get(String key) { + return properties.get(key); + } + + @Override + public Stream children(String key, Function, T> constructor) { + return Stream.ofNullable(get(key)) + .filter(Objects::nonNull) + .map(el -> (List>) el) + .findAny() + .stream() + .flatMap(Collection::stream) + .map(constructor); + } + + // Other properties and methods... +} +``` +در ادامه، یک enum به نام Property و مجموعه‌ای از واسط‌ها برای type، price، model و parts تعریف می‌کنیم. این کار به ما اجازه می‌دهد یک واسط با ظاهر استاتیک برای کلاس Car ایجاد کنیم. +```java +public enum Property { + + PARTS, TYPE, PRICE, MODEL +} + +public interface HasType extends Document { + + default Optional getType() { + return Optional.ofNullable((String) get(Property.TYPE.toString())); + } +} + +public interface HasPrice extends Document { + + default Optional getPrice() { + return Optional.ofNullable((Number) get(Property.PRICE.toString())); + } +} + +public interface HasModel extends Document { + + default Optional getModel() { + return Optional.ofNullable((String) get(Property.MODEL.toString())); + } +} + +public interface HasParts extends Document { + + default Stream getParts() { + return children(Property.PARTS.toString(), Part::new); + } +} + +public class Part extends AbstractDocument implements HasType, HasModel, HasPrice { + + public Part(Map properties) { + super(properties); + } +} +``` +اکنون آماده‌ایم تا کلاس Car را معرفی کنیم. +```java +public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts { + + public Car(Map properties) { + super(properties); + } +} +``` +و در نهایت، نحوه ساخت و استفاده از Car را در یک مثال کامل می‌بینید. +```java + public static void main(String[] args) { + LOGGER.info("Constructing parts and car"); + + var wheelProperties = Map.of( + Property.TYPE.toString(), "wheel", + Property.MODEL.toString(), "15C", + Property.PRICE.toString(), 100L); + + var doorProperties = Map.of( + Property.TYPE.toString(), "door", + Property.MODEL.toString(), "Lambo", + Property.PRICE.toString(), 300L); + + var carProperties = Map.of( + Property.MODEL.toString(), "300SL", + Property.PRICE.toString(), 10000L, + Property.PARTS.toString(), List.of(wheelProperties, doorProperties)); + + var car = new Car(carProperties); + + LOGGER.info("Here is our car:"); + LOGGER.info("-> model: {}", car.getModel().orElseThrow()); + LOGGER.info("-> price: {}", car.getPrice().orElseThrow()); + LOGGER.info("-> parts: "); + car.getParts().forEach(p -> LOGGER.info("\t{}/{}/{}", + p.getType().orElse(null), + p.getModel().orElse(null), + p.getPrice().orElse(null)) + ); +} +``` +خروجی برنامه: +``` +07:21:57.391 [main] INFO com.iluwatar.abstractdocument.App -- Constructing parts and car +07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- Here is our car: +07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- -> model: 300SL +07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> price: 10000 +07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> parts: +07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App -- wheel/15C/100 +07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App -- door/Lambo/300 +``` + + ### چه زمانی از الگوی Abstract Document در جاوا استفاده کنیم؟ + +الگوی طراحی Abstract Document به‌ویژه در سناریوهایی مفید است که نیاز به مدیریت انواع مختلفی از اسناد در جاوا وجود دارد که برخی ویژگی‌ها یا رفتارهای مشترک دارند، ولی ویژگی‌ها یا رفتارهای خاص خود را نیز دارند. در ادامه چند سناریوی مناسب برای این الگو آورده شده است: + +* سیستم‌های مدیریت محتوا (CMS): ممکن است انواع مختلفی از محتوا مانند مقاله، تصویر، ویدئو و... وجود داشته باشد. هر نوع محتوا ویژگی‌های مشترکی مثل تاریخ ایجاد، نویسنده و تگ‌ها دارد، ولی همچنین ویژگی‌های خاصی مثل ابعاد تصویر یا مدت‌زمان ویدئو. + +* سیستم‌های فایل: اگر یک سیستم فایل طراحی می‌کنید که باید انواع مختلف فایل مانند اسناد، تصاویر، فایل‌های صوتی و دایرکتوری‌ها را مدیریت کند، این الگو می‌تواند راهی یکپارچه برای دسترسی به ویژگی‌هایی مانند اندازه فایل یا تاریخ ایجاد، فراهم کند و در عین حال ویژگی‌های خاص هر نوع فایل را هم مدیریت کند. + +* سیستم‌های تجارت الکترونیک: یک پلتفرم فروش آنلاین ممکن است محصولات مختلفی داشته باشد مثل محصولات فیزیکی، فایل‌های دیجیتال، و اشتراک‌ها. این محصولات ویژگی‌هایی مثل نام، قیمت و توضیح را به اشتراک می‌گذارند، ولی ویژگی‌های خاصی هم دارند مانند وزن حمل برای محصولات فیزیکی یا لینک دانلود برای دیجیتال‌ها. + +* سیستم‌های سوابق پزشکی: در مراقبت سلامت، پرونده بیماران ممکن است داده‌های مختلفی مثل مشخصات فردی، سوابق پزشکی، نتایج آزمایش‌ها و نسخه‌ها را شامل شود. این الگو می‌تواند ویژگی‌های مشترک مثل شماره بیمار یا تاریخ تولد را مدیریت کند و هم‌زمان ویژگی‌های خاصی مثل نتایج آزمایش یا داروهای تجویزی را هم پوشش دهد. + +* مدیریت پیکربندی: هنگام کار با تنظیمات پیکربندی نرم‌افزار، ممکن است انواع مختلفی از عناصر پیکربندی وجود داشته باشد، هر یک با ویژگی‌های خاص خود. این الگو می‌تواند برای مدیریت این عناصر مفید باشد. + +* پلتفرم‌های آموزشی: سیستم‌های آموزشی ممکن است انواع مختلفی از منابع یادگیری داشته باشند مثل محتوای متنی، ویدیوها، آزمون‌ها و تمرین‌ها. ویژگی‌های مشترکی مثل عنوان، نویسنده و تاریخ انتشار وجود دارد، ولی ویژگی‌های خاصی مانند مدت ویدیو یا مهلت تحویل تمرین نیز ممکن است وجود داشته باشد. + +* ابزارهای مدیریت پروژه: در برنامه‌های مدیریت پروژه، ممکن است انواع مختلفی از وظایف مانند آیتم‌های to-do، milestoneها و issueها داشته باشید. این الگو می‌تواند برای مدیریت ویژگی‌های عمومی مانند نام وظیفه و مسئول آن استفاده شود و در عین حال ویژگی‌های خاص مانند تاریخ milestone یا اولویت issue را نیز پوشش دهد. + +* اسناد ساختار ویژگی‌های متنوع و در حال تحول دارند. + +* افزودن ویژگی‌های جدید به‌صورت پویا یک نیاز رایج است. + +* جداسازی دسترسی به داده از فرمت‌های خاص حیاتی است. + +* نگهداری‌پذیری و انعطاف‌پذیری برای کد اهمیت دارد. + +ایده اصلی پشت الگوی Abstract Document فراهم کردن روشی انعطاف‌پذیر و قابل توسعه برای مدیریت انواع مختلف اسناد یا موجودیت‌ها با ویژگی‌های مشترک و خاص است. با تعریف یک واسط مشترک و پیاده‌سازی آن در انواع مختلف اسناد، می‌توان به شیوه‌ای منظم و یکپارچه برای مدیریت ساختارهای پیچیده داده دست یافت. +### مزایا و معایب الگوی Abstract Document +
+مزایا: + +* انعطاف‌پذیری: پشتیبانی از ساختارهای متنوع اسناد و ویژگی‌ها. + +* قابلیت توسعه: افزودن ویژگی‌های جدید بدون شکستن کد موجود. + +* نگهداری‌پذیری: ارتقاء کد تمیز و قابل تطبیق به‌واسطه جداسازی وظایف. + +* قابلیت استفاده مجدد: نمای دید تایپ‌شده باعث استفاده مجدد از کد برای دسترسی به نوع خاصی از ویژگی می‌شود. + +معایب: + +* پیچیدگی: نیاز به تعریف واسط‌ها و نماها، که باعث اضافه شدن سربار پیاده‌سازی می‌شود. + +* کارایی: ممکن است سربار کمی نسبت به دسترسی مستقیم به داده داشته باشد. +
+ +منابع و اعتبارها + +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) + +* [Java Design Patterns: A Hands-On Experience with Real-World Examples](https://amzn.to/3yhh525) + +* [Pattern-Oriented Software Architecture Volume 4: A Pattern Language for Distributed Computing (v. 4)] (https://amzn.to/49zRP4R) + +* [Patterns of Enterprise Application Architecture] (https://amzn.to/3WfKBPR) + +* [Abstract Document Pattern (Wikipedia)] (https://en.wikipedia.org/wiki/Abstract_Document_Pattern) + +* [Dealing with Properties (Martin Fowler)] (http://martinfowler.com/apsupp/properties.pdf) diff --git a/localization/fa/abstract-document/etc/abstract-document.png b/localization/fa/abstract-document/etc/abstract-document.png new file mode 100644 index 000000000000..6bc0b29a4e77 Binary files /dev/null and b/localization/fa/abstract-document/etc/abstract-document.png differ diff --git a/localization/fa/abstract-factory/README.md b/localization/fa/abstract-factory/README.md new file mode 100644 index 000000000000..85dce3437ba4 --- /dev/null +++ b/localization/fa/abstract-factory/README.md @@ -0,0 +1,223 @@ +--- +title: "الگوی طراحی Abstract Factory در جاوا: مهارت در ایجاد شیء با ظرافت" +shortTitle: Abstract Factory +description: "با مثال‌های دنیای واقعی، دیاگرام‌های کلاس و آموزش‌ها، الگوی Abstract Factory را در جاوا بیاموزید. منظور، کاربرد، مزایا و نمونه‌های واقعی آن را درک کنید و دانش طراحی الگوهایتان را افزایش دهید." +category: Creational +language: fa +tag: + - Abstraction + - Decoupling + - Gang of Four + - Instantiation + - Polymorphism +--- + +## همچنین به این عنوان شناخته می‌شود + +* کیت + +## هدف از الگوی طراحی Abstract Factory + +الگوی Abstract Factory در جاوا یک واسط برای ایجاد خانواده‌هایی از اشیای مرتبط یا وابسته فراهم می‌کند بدون آنکه کلاس‌های مشخص آن‌ها را تعیین کند، و این کار موجب افزایش مدولاریتی و انعطاف‌پذیری در طراحی نرم‌افزار می‌شود. + +## توضیح دقیق الگوی Abstract Factory با مثال‌های دنیای واقعی + +مثال دنیای واقعی + +> تصور کنید یک شرکت مبلمان وجود دارد که از الگوی Abstract Factory در جاوا برای تولید سبک‌های مختلف مبلمان استفاده می‌کند: مدرن، ویکتوریایی و روستایی. هر سبک شامل محصولاتی مانند صندلی‌ها، میزها و کاناپه‌ها است. برای اطمینان از یکنواختی در هر سبک، شرکت از یک الگوی Abstract Factory استفاده می‌کند. +> +> در این سناریو، Abstract Factory یک واسط برای ایجاد خانواده‌هایی از اشیای مبلمان مرتبط (صندلی‌ها، میزها، کاناپه‌ها) است. هر Factory مشخص (کارخانه‌ی مبلمان مدرن، کارخانه‌ی مبلمان ویکتوریایی، کارخانه‌ی مبلمان روستایی) این واسط را پیاده‌سازی می‌کند و مجموعه‌ای از محصولات مطابق با سبک خاص ایجاد می‌کند. به این ترتیب، مشتریان می‌توانند یک مجموعه کامل از مبلمان مدرن یا ویکتوریایی ایجاد کنند بدون اینکه نگران جزئیات ساخت آن‌ها باشند. این باعث حفظ یکنواختی سبک می‌شود و امکان تغییر آسان سبک مبلمان را فراهم می‌کند. + +به زبان ساده + +> کارخانه‌ای از کارخانه‌ها؛ یک Factory یا کارخانه که مجموعه‌ای از کارخانه‌های مرتبط یا وابسته را بدون مشخص کردن کلاس‌های concrete آن‌ها گروه‌بندی می‌کند. + +ویکی‌پدیا می‌گوید + +> الگوی Abstract Factory راهی برای کپسوله کردن مجموعه‌ای از کارخانه‌های منحصر به فرد با یک تم مشترک بدون تعیین کلاس‌های concrete آن‌ها فراهم می‌کند. + +دیاگرام کلاس + +![Abstract Factory class diagram](./etc/abstract-factory.urm.png "Abstract Factory class diagram") + +## مثال برنامه‌نویسی از Abstract Factory در جاوا + +برای ایجاد یک پادشاهی با استفاده از الگوی Abstract Factory در جاوا، ما به اشیایی با یک تم مشترک نیاز داریم. یک پادشاهی اِلف (Elf) به یک پادشاه اِلف، یک قلعه‌ی اِلف، و یک ارتش اِلف نیاز دارد، در حالی که یک پادشاهی اورک (Orc) به یک پادشاه اورک، یک قلعه‌ی اورک، و یک ارتش اورک نیاز دارد. بین اشیای موجود در پادشاهی وابستگی وجود دارد. + +ترجمه‌ی مثال پادشاهی بالا. ابتدا ما برخی واسط‌ها و پیاده‌سازی‌هایی برای اشیای موجود در پادشاهی داریم: + +```java +public interface Castle { + String getDescription(); +} + +public interface King { + String getDescription(); +} + +public interface Army { + String getDescription(); +} + +// Elven implementations -> +public class ElfCastle implements Castle { + static final String DESCRIPTION = "This is the elven castle!"; + + @Override + public String getDescription() { + return DESCRIPTION; + } +} + +public class ElfKing implements King { + static final String DESCRIPTION = "This is the elven king!"; + + @Override + public String getDescription() { + return DESCRIPTION; + } +} + +public class ElfArmy implements Army { + static final String DESCRIPTION = "This is the elven Army!"; + + @Override + public String getDescription() { + return DESCRIPTION; + } +} + +// Orcish implementations similarly -> ... +``` + +سپس واسط و پیاده‌سازی‌های کارخانه‌ی پادشاهی را داریم: + +```java +public interface KingdomFactory { + Castle createCastle(); + King createKing(); + Army createArmy(); +} + +public class ElfKingdomFactory implements KingdomFactory { + + @Override + public Castle createCastle() { + return new ElfCastle(); + } + + @Override + public King createKing() { + return new ElfKing(); + } + + @Override + public Army createArmy() { + return new ElfArmy(); + } +} + +// Orcish implementations similarly -> ... +``` + +اکنون می‌توانیم یک کارخانه برای کارخانه‌های مختلف پادشاهی طراحی کنیم. در این مثال، ما `FactoryMaker` را ایجاد کردیم که مسئول برگرداندن یک نمونه از `ElfKingdomFactory` یا `OrcKingdomFactory` است. مشتری می تواند از `FactoryMaker` برای ایجاد کارخانه concrete مورد نظر استفاده کند که به نوبه خود اشیاء concrete مختلف (مشتق شده از ارتش، پادشاه، قلعه) را تولید می‌کند. در این مثال، ما همچنین از یک enum برای پارامتری کردن نوع کارخانه پادشاهی که مشتری درخواست خواهد کرد استفاده کردیم. + +```java +public static class FactoryMaker { + + public enum KingdomType { + ELF, ORC + } + + public static KingdomFactory makeFactory(KingdomType type) { + return switch (type) { + case ELF -> new ElfKingdomFactory(); + case ORC -> new OrcKingdomFactory(); + }; + } +} +``` + +نمونه‌ای از تابع اصلی برنامه: + +```java +LOGGER.info("elf kingdom"); +createKingdom(Kingdom.FactoryMaker.KingdomType.ELF); +LOGGER.info(kingdom.getArmy().getDescription()); +LOGGER.info(kingdom.getCastle().getDescription()); +LOGGER.info(kingdom.getKing().getDescription()); + +LOGGER.info("orc kingdom"); +createKingdom(Kingdom.FactoryMaker.KingdomType.ORC); +LOGGER.info(kingdom.getArmy().getDescription()); +LOGGER.info(kingdom.getCastle().getDescription()); +LOGGER.info(kingdom.getKing().getDescription()); +``` + +خروجی برنامه: + +``` +07:35:46.340 [main] INFO com.iluwatar.abstractfactory.App -- elf kingdom +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven army! +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven castle! +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven king! +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- orc kingdom +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc army! +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc castle! +07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc king! +``` + +## چه زمانی باید از الگوی Abstract Factory در جاوا استفاده کرد؟ + +* زمانی که سیستم باید مستقل از نحوه‌ی ایجاد، ترکیب و نمایش محصولاتش باشد. +* زمانی که نیاز به پیکربندی سیستم با یکی از چند خانواده محصول دارید. +* زمانی که باید خانواده‌ای از اشیای مرتبط با هم استفاده شوند، برای اطمینان از یکنواختی. +* زمانی که می‌خواهید کتابخانه‌ای از محصولات را فراهم کنید و فقط واسط‌های آن‌ها را نمایان کنید، نه پیاده‌سازی‌ها را. +* زمانی که طول عمر وابستگی‌ها کوتاه‌تر از مصرف‌کننده باشد. +* زمانی که نیاز به ساخت وابستگی‌ها با مقادیر یا پارامترهای زمان اجرا باشد. +* زمانی که باید در زمان اجرا انتخاب کنید که کدام خانواده از محصول را استفاده کنید. +* زمانی که افزودن محصولات یا خانواده های جدید نباید نیاز به تغییر در کد موجود داشته باشد. + +## آموزش‌های الگوی Abstract Factory در جاوا + +* [Abstract Factory Design Pattern in Java (DigitalOcean)](https://www.digitalocean.com/community/tutorials/abstract-factory-design-pattern-in-java) +* [Abstract Factory (Refactoring Guru)](https://refactoring.guru/design-patterns/abstract-factory) + +## مزایا و معایب الگوی Abstract Factory + +مزایا: + +> * انعطاف‌پذیری: به راحتی می‌توان خانواده‌های محصول را تعویض کرد بدون تغییر کد. + +> * جداسازی (Decoupling): کد مشتری فقط با واسط‌های انتزاعی کار می‌کند که باعث قابلیت حمل و نگهداری می‌شود. + +> * قابلیت استفاده مجدد: کارخانه‌های انتزاعی و محصولات امکان استفاده مجدد از مؤلفه‌ها را فراهم می‌کنند. + +> * قابلیت نگهداری: تغییرات در خانواده‌های محصول محلی شده و به‌روزرسانی را ساده‌تر می‌کند. + +معایب: + +> * پیچیدگی: تعریف واسط‌های انتزاعی و کارخانه‌های مشخص سربار اولیه ایجاد می‌کند. + +> * غیرمستقیم بودن: کد مشتری از طریق کارخانه‌ها با محصولات کار می‌کند که ممکن است شفافیت را کاهش دهد. + +## نمونه‌های واقعی استفاده از الگوی Abstract Factory در جاوا + +* کلاس‌های `LookAndFeel` در Java Swing برای ارائه گزینه های مختلف look-and-feel +* پیاده‌سازی‌های مختلف در Java AWT برای ایجاد اجزای مختلف GUI +* [javax.xml.parsers.DocumentBuilderFactory](http://docs.oracle.com/javase/8/docs/api/javax/xml/parsers/DocumentBuilderFactory.html) +* [javax.xml.transform.TransformerFactory](http://docs.oracle.com/javase/8/docs/api/javax/xml/transform/TransformerFactory.html#newInstance--) +* [javax.xml.xpath.XPathFactory](http://docs.oracle.com/javase/8/docs/api/javax/xml/xpath/XPathFactory.html#newInstance--) + +## الگوهای طراحی مرتبط با جاوا + +* الگوی [Factory Method](https://java-design-patterns.com/patterns/factory-method/): الگوی کارخانه‌ی انتزاعی از روش‌های کارخانه‌ای برای ایجاد محصولات استفاده می‌کند. +* الگوی [Singleton](https://java-design-patterns.com/patterns/singleton/): کلاس‌های کارخانه‌ی انتزاعی اغلب به صورت Singleton پیاده‌سازی می‌شوند. +* الگوی [Factory Kit](https://java-design-patterns.com/patterns/factory-kit/): مشابه کارخانه‌ی انتزاعی اما بر پیکربندی و مدیریت مجموعه‌ای از اشیای مرتبط تمرکز دارد. + +## منابع و ارجاعات + +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) +* [Design Patterns in Java](https://amzn.to/3Syw0vC) +* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq) +* [Java Design Patterns: A Hands-On Experience with Real-World Examples](https://amzn.to/3HWNf4U) diff --git a/localization/fa/abstract-factory/etc/abstract-factory.urm.png b/localization/fa/abstract-factory/etc/abstract-factory.urm.png new file mode 100644 index 000000000000..836858a2c652 Binary files /dev/null and b/localization/fa/abstract-factory/etc/abstract-factory.urm.png differ diff --git a/localization/fa/active-object/README.md b/localization/fa/active-object/README.md new file mode 100644 index 000000000000..7c105b072aaa --- /dev/null +++ b/localization/fa/active-object/README.md @@ -0,0 +1,220 @@ +--- +title: "الگوی Active Object در جاوا: دستیابی به پردازش ناهمگام کارآمد" +shortTitle: Active Object +description: "با الگوی طراحی Active Object در جاوا آشنا شوید. این راهنما رفتار ناهمگام، هم‌زمانی (concurrency) و مثال‌های کاربردی برای بهبود عملکرد برنامه‌های جاوای شما را پوشش می‌دهد." +category: Concurrency +language: fa +tag: + - Asynchronous + - Decoupling + - Messaging + - Synchronization + - Thread management +--- + +## هدف الگوی طراحی Active Object + +الگوی Active Object روشی مطمئن برای پردازش ناهمگام در جاوا فراهم می‌کند که به پاسخ‌گو بودن برنامه‌ها و مدیریت مؤثر threadها کمک می‌کند. این الگو با محصور کردن وظایف در شیءهایی که هر کدام thread و صف پیام مخصوص خود را دارند، به این هدف می‌رسد. این جداسازی باعث می‌شود thread اصلی پاسخ‌گو باقی بماند و مشکلاتی مانند دست‌کاری مستقیم threadها یا دسترسی به وضعیت مشترک (shared state) به وجود نیاید. + +## توضیح کامل الگوی Active Object با مثال‌های دنیای واقعی + +مثال دنیای واقعی + +> تصور کنید در یک رستوران شلوغ، مشتریان سفارش خود را به گارسون‌ها می‌سپارند. به‌جای آنکه گارسون‌ها خودشان به آشپزخانه بروند و غذا را آماده کنند، سفارش‌ها را روی کاغذهایی می‌نویسند و به یک هماهنگ‌کننده می‌دهند. این هماهنگ‌کننده گروهی از سرآشپزها را مدیریت می‌کند که غذاها را به صورت ناهمگام آماده می‌کنند. هرگاه آشپزی آزاد شود، سفارش بعدی را از صف برمی‌دارد، غذا را آماده می‌کند و پس از آن گارسون را برای سرو غذا مطلع می‌سازد. +> +> در این قیاس، گارسون‌ها نماینده threadهای کلاینت هستند، هماهنگ‌کننده نقش زمان‌بند (scheduler) را ایفا می‌کند، و آشپزها نمایان‌گر اجرای متدها در threadهای جداگانه هستند. این ساختار باعث می‌شود گارسون‌ها بتوانند بدون مسدود شدن توسط فرایند آماده‌سازی غذا، سفارش‌های بیشتری دریافت کنند—درست مانند اینکه الگوی Active Object، فراخوانی متد را از اجرای آن جدا می‌کند تا هم‌زمانی (concurrency) را افزایش دهد. + +به زبان ساده + +> الگوی Active Object، اجرای متد را از فراخوانی آن جدا می‌کند تا در برنامه‌های چندریسمانی (multithreaded)، هم‌زمانی و پاسخ‌گویی بهتری فراهم شود. + +طبق تعریف ویکی‌پدیا + +> الگوی طراحی Active Object اجرای متد را از فراخوانی آن جدا می‌کند، برای شیءهایی که هرکدام thread کنترل مخصوص به خود را دارند. هدف، معرفی هم‌زمانی با استفاده از فراخوانی متد به‌صورت ناهمگام و یک زمان‌بند برای مدیریت درخواست‌ها است. +> +> این الگو شامل شش جزء کلیدی است: +> +> * یک proxy، که رابطی برای کلاینت‌ها با متدهای عمومی فراهم می‌کند. +> * یک interface که درخواست متد برای شیء فعال (active object) را تعریف می‌کند. +> * فهرستی از درخواست‌های معلق از سوی کلاینت‌ها. +> * یک زمان‌بند (scheduler) که تصمیم می‌گیرد کدام درخواست بعدی اجرا شود. +> * پیاده‌سازی متد شیء فعال. +> * یک callback یا متغیر برای اینکه کلاینت نتیجه را دریافت کند. + +نمودار توالی + +![Active Object sequence diagram](./etc/active-object-sequence-diagram.png) + +## مثال برنامه‌نویسی از Active Object در جاوا + +این بخش نحوه عملکرد الگوی Active Object در جاوا را توضیح می‌دهد و کاربرد آن در مدیریت وظایف ناهمگام و کنترل هم‌زمانی را نشان می‌دهد. + +اورک‌ها به دلیل ذات وحشی و غیرقابل مهارشان شناخته می‌شوند. به‌نظر می‌رسد هرکدام thread کنترل مخصوص خود را دارند. برای پیاده‌سازی یک موجود که دارای سازوکار thread مستقل خود باشد و فقط API را در اختیار قرار دهد نه اجرای داخلی را، می‌توان از الگوی Active Object استفاده کرد. + +```java +public abstract class ActiveCreature { + private final Logger logger = LoggerFactory.getLogger(ActiveCreature.class.getName()); + + private BlockingQueue requests; + + private String name; + + private Thread thread; + + public ActiveCreature(String name) { + this.name = name; + this.requests = new LinkedBlockingQueue(); + thread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + requests.take().run(); + } catch (InterruptedException e) { + logger.error(e.getMessage()); + } + } + } + } + ); + thread.start(); + } + + public void eat() throws InterruptedException { + requests.put(new Runnable() { + @Override + public void run() { + logger.info("{} is eating!", name()); + logger.info("{} has finished eating!", name()); + } + } + ); + } + + public void roam() throws InterruptedException { + requests.put(new Runnable() { + @Override + public void run() { + logger.info("{} has started to roam the wastelands.", name()); + } + } + ); + } + + public String name() { + return this.name; + } +} +``` + +می‌توان دید هر کلاسی که از ActiveCreature ارث‌بری کند، دارای thread کنترل مختص به خود برای فراخوانی و اجرای متدها خواهد بود. + +برای مثال، کلاس Orc: + +```java +public class Orc extends ActiveCreature { + + public Orc(String name) { + super(name); + } +} +``` +اکنون می‌توان چند موجود مانند orc ایجاد کرد، به آن‌ها دستور داد که بخورند و پرسه بزنند، و آن‌ها این دستورات را در thread مختص به خود اجرا می‌کنند: + +```java +public class App implements Runnable { + + private static final Logger logger = LoggerFactory.getLogger(App.class.getName()); + + private static final int NUM_CREATURES = 3; + + public static void main(String[] args) { + var app = new App(); + app.run(); + } + + @Override + public void run() { + List creatures = new ArrayList<>(); + try { + for (int i = 0; i < NUM_CREATURES; i++) { + creatures.add(new Orc(Orc.class.getSimpleName() + i)); + creatures.get(i).eat(); + creatures.get(i).roam(); + } + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.error(e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + for (int i = 0; i < NUM_CREATURES; i++) { + creatures.get(i).kill(0); + } + } + } +} +``` + +خروجی برنامه: + +``` +09:00:02.501 [Thread-0] INFO com.iluwatar.activeobject.ActiveCreature -- Orc0 is eating! +09:00:02.501 [Thread-2] INFO com.iluwatar.activeobject.ActiveCreature -- Orc2 is eating! +09:00:02.501 [Thread-1] INFO com.iluwatar.activeobject.ActiveCreature -- Orc1 is eating! +09:00:02.504 [Thread-0] INFO com.iluwatar.activeobject.ActiveCreature -- Orc0 has finished eating! +09:00:02.504 [Thread-1] INFO com.iluwatar.activeobject.ActiveCreature -- Orc1 has finished eating! +09:00:02.504 [Thread-0] INFO com.iluwatar.activeobject.ActiveCreature -- Orc0 has started to roam in the wastelands. +09:00:02.504 [Thread-2] INFO com.iluwatar.activeobject.ActiveCreature -- Orc2 has finished eating! +09:00:02.504 [Thread-1] INFO com.iluwatar.activeobject.ActiveCreature -- Orc1 has started to roam in the wastelands. +09:00:02.504 [Thread-2] INFO com.iluwatar.activeobject.ActiveCreature -- Orc2 has started to roam in the wastelands. +``` + +چه زمانی از الگوی Active Object در جاوا استفاده کنیم؟ + +از الگوی Active Object در جاوا استفاده کنید زمانی که: +> * نیاز دارید وظایف ناهمگام را بدون مسدود کردن thread اصلی مدیریت کنید تا عملکرد و پاسخ‌گویی بهتری داشته باشید. +> * نیاز به تعامل ناهمگام با منابع خارجی دارید. +> * می‌خواهید پاسخ‌گویی برنامه را افزایش دهید. +> * نیاز به مدیریت وظایف هم‌زمان به‌صورت ماژولار و قابل نگهداری دارید. + +آموزش‌های Java برای الگوی Active Object +> [Android and Java Concurrency: The Active Object Pattern (Douglas Schmidt)]((https://www.youtube.com/watch?v=Cd8t2u5Qmvc)) + +کاربردهای دنیای واقعی الگوی Active Object در جاوا + +> سیستم‌های معاملات بلادرنگ که درخواست‌ها به‌صورت ناهمگام پردازش می‌شوند. +> که در آن وظایف طولانی در پس‌زمینه اجرا می‌شوند بدون آنکه رابط کاربری را متوقف کنند. +> رابط‌های کاربری گرافیکی (GUI) +> برنامه‌نویسی بازی‌ها برای مدیریت به‌روزرسانی‌های هم‌زمان وضعیت بازی یا محاسبات هوش مصنوعی. + +مزایا و ملاحظات الگوی Active Object + +با مزایا و معایب استفاده از الگوی Active Object در جاوا آشنا شوید؛ از جمله بهبود ایمنی threadها و ملاحظات سربار احتمالی (overhead). + +> مزایا: +> +> * پاسخ‌گویی بهتر thread اصلی. +> * محصورسازی مسائل مربوط به هم‌زمانی درون شیءها. +> * بهبود سازمان‌دهی کد و قابلیت نگهداری. +> * فراهم‌سازی ایمنی در برابر شرایط بحرانی (thread safety) و جلوگیری از مشکلات وضعیت مشترک. + +> معایب: +> +> * سربار اضافی به دلیل ارسال پیام و مدیریت threadها. +> * برای تمام سناریوهای هم‌زمانی مناسب نیست. + +الگوهای طراحی مرتبط در جاوا + +> * [Command](https://java-design-patterns.com/patterns/command/): درخواست را به‌عنوان یک شیء کپسوله می‌کند، مشابه روشی که Active Object فراخوانی متد را کپسوله می‌کند. +> * [Promise](https://java-design-patterns.com/patterns/promise/): راهی برای دریافت نتیجه یک فراخوانی متد ناهمگام فراهم می‌کند؛ اغلب همراه با Active Object استفاده می‌شود. +> * [Proxy](https://java-design-patterns.com/patterns/proxy/): الگوی Active Object می‌تواند از proxy برای مدیریت فراخوانی‌های متد به‌صورت ناهمگام استفاده کند. + +منابع و مراجع + +> * [Design Patterns: Elements of Reusable Object Software](https://amzn.to/3HYqrBE) +> * [Concurrent Programming in Java: Design Principles and Patterns](https://amzn.to/498SRVq) +> * [Java Concurrency in Practice](https://amzn.to/4aRMruW) +> * [Learning Concurrent Programming in Scala](https://amzn.to/3UE07nV) +> * [Pattern Languages of Program Design 3](https://amzn.to/3OI1j61) +> * [Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects](https://amzn.to/3UgC24V) + diff --git a/localization/fa/active-object/etc/active-object-sequence-diagram.png b/localization/fa/active-object/etc/active-object-sequence-diagram.png new file mode 100644 index 000000000000..b725d9b07b6d Binary files /dev/null and b/localization/fa/active-object/etc/active-object-sequence-diagram.png differ diff --git a/localization/fa/active-object/etc/active-object.urm.png b/localization/fa/active-object/etc/active-object.urm.png new file mode 100644 index 000000000000..c14f66144ee2 Binary files /dev/null and b/localization/fa/active-object/etc/active-object.urm.png differ diff --git a/localization/fa/active-object/etc/active-object.urm.puml b/localization/fa/active-object/etc/active-object.urm.puml new file mode 100644 index 000000000000..3fc3c8e1e921 --- /dev/null +++ b/localization/fa/active-object/etc/active-object.urm.puml @@ -0,0 +1,25 @@ +@startuml +package com.iluwatar.activeobject { + abstract class ActiveCreature { + - logger : Logger + - name : String + - requests : BlockingQueue + - thread : Thread + + ActiveCreature(name : String) + + eat() + + name() : String + + roam() + } + class App { + - creatures : Integer + - logger : Logger + + App() + + main(args : String[]) {static} + + run() + } + class Orc { + + Orc(name : String) + } +} +Orc --|> ActiveCreature +@enduml \ No newline at end of file diff --git a/localization/fa/factory/README.md b/localization/fa/factory/README.md new file mode 100644 index 000000000000..db41813464e3 --- /dev/null +++ b/localization/fa/factory/README.md @@ -0,0 +1,155 @@ +--- +title: "الگوی factory در جاوا: ساده‌سازی ایجاد اشیاء" +shortTitle: factory +description: "الگوی طراحی factory در جاوا را با مثال‌ها و توضیحات دقیق بیاموزید. یاد بگیرید چگونه با استفاده از الگوی factory کدی انعطاف‌پذیر و مقیاس‌پذیر ایجاد کنید. مناسب برای توسعه‌دهندگانی که به دنبال بهبود مهارت‌های طراحی شیءگرا هستند." +category: structural +language: fa +tag: + - Abstraction + - Encapsulation + - Gang of Four + - Instantiation + - Polymorphism +--- + +## هدف از الگوی طراحی factory + +الگوی طراحی factory در جاوا یک الگوی ساختاری است که یک رابط برای ایجاد یک شیء تعریف می‌کند اما به زیرکلاس‌ها اجازه می‌دهد نوع اشیائی را که ایجاد خواهند شد تغییر دهند. این الگو انعطاف‌پذیری و مقیاس‌پذیری را در کد شما ترویج می‌دهد. + +## توضیح دقیق الگوی factory با مثال‌های دنیای واقعی + +### مثال دنیای واقعی + +> تصور کنید در یک نانوایی انواع مختلف کیک‌ها با استفاده از الگوی طراحی factory ساخته می‌شوند. `CakeFactory` فرآیند ایجاد را مدیریت می‌کند و امکان افزودن آسان انواع جدید کیک‌ها را بدون تغییر در فرآیند اصلی فراهم می‌کند. `CakeFactory` می‌تواند انواع مختلفی از کیک‌ها مانند کیک شکلاتی، کیک وانیلی و کیک توت‌فرنگی تولید کند. به جای اینکه کارکنان نانوایی به صورت دستی مواد اولیه را انتخاب کنند و دستورالعمل‌های خاصی را برای هر نوع کیک دنبال کنند، از `CakeFactory` برای مدیریت فرآیند استفاده می‌کنند. مشتری فقط نوع کیک را درخواست می‌کند و `CakeFactory` مواد اولیه و دستورالعمل مناسب را تعیین کرده و نوع خاصی از کیک را ایجاد می‌کند. این تنظیم به نانوایی اجازه می‌دهد تا انواع جدید کیک‌ها را به راحتی اضافه کند بدون اینکه فرآیند اصلی تغییر کند، که این امر انعطاف‌پذیری و مقیاس‌پذیری را ترویج می‌دهد. + +### تعریف ویکی‌پدیا + +> الگوی factory یک شیء برای ایجاد اشیاء دیگر است – به طور رسمی، factory یک تابع یا متدی است که اشیاء با نمونه‌ها یا کلاس‌های مختلف را بازمی‌گرداند. + +### نمودار توالی + +![نمودار توالی factory](./etc/factory-sequence-diagram.png) + +## مثال برنامه‌نویسی از الگوی factory در جاوا + +تصور کنید یک کیمیاگر قصد دارد سکه‌هایی تولید کند. کیمیاگر باید بتواند هم سکه‌های طلا و هم سکه‌های مسی ایجاد کند و تغییر بین آن‌ها باید بدون تغییر در کد موجود امکان‌پذیر باشد. الگوی factory این امکان را فراهم می‌کند با ارائه یک متد ایجاد استاتیک که می‌توان آن را با پارامترهای مرتبط فراخوانی کرد. + +در جاوا، می‌توانید الگوی factory را با تعریف یک رابط `Coin` و پیاده‌سازی‌های آن `GoldCoin` و `CopperCoin` پیاده‌سازی کنید. کلاس `CoinFactory` یک متد استاتیک `getCoin` ارائه می‌دهد تا اشیاء سکه را بر اساس نوع ایجاد کند. + +```java +public interface Coin { + String getDescription(); +} +``` + +```java +public class GoldCoin implements Coin { + + static final String DESCRIPTION = "This is a gold coin."; + + @Override + public String getDescription() { + return DESCRIPTION; + } +} +``` + +```java +public class CopperCoin implements Coin { + + static final String DESCRIPTION = "This is a copper coin."; + + @Override + public String getDescription() { + return DESCRIPTION; + } +} +``` + +کد زیر انواع سکه‌هایی که پشتیبانی می‌شوند (`GoldCoin` و `CopperCoin`) را نشان می‌دهد. + +```java +@RequiredArgsConstructor +@Getter +public enum CoinType { + + COPPER(CopperCoin::new), + GOLD(GoldCoin::new); + + private final Supplier constructor; +} +``` + +سپس متد استاتیک `getCoin` برای ایجاد اشیاء سکه در کلاس factory `CoinFactory` کپسوله شده است. + +```java +public class CoinFactory { + + public static Coin getCoin(CoinType type) { + return type.getConstructor().get(); + } +} +``` + +اکنون، در کد کلاینت، می‌توانیم انواع مختلفی از سکه‌ها را با استفاده از کلاس factory تولید کنیم. + +```java +public static void main(String[] args) { + LOGGER.info("The alchemist begins his work."); + var coin1 = CoinFactory.getCoin(CoinType.COPPER); + var coin2 = CoinFactory.getCoin(CoinType.GOLD); + LOGGER.info(coin1.getDescription()); + LOGGER.info(coin2.getDescription()); +} +``` + +خروجی برنامه: + +``` +06:19:53.530 [main] INFO com.iluwatar.factory.App -- The alchemist begins his work. +06:19:53.533 [main] INFO com.iluwatar.factory.App -- This is a copper coin. +06:19:53.533 [main] INFO com.iluwatar.factory.App -- This is a gold coin. +``` + +## زمان استفاده از الگوی factory در جاوا + +* از الگوی طراحی factory در جاوا زمانی استفاده کنید که کلاس از قبل نوع دقیق و وابستگی‌های اشیائی که نیاز به ایجاد آن دارد را نمی‌داند. +* زمانی که یک متد یکی از چندین کلاس ممکن که یک کلاس والد مشترک دارند را بازمی‌گرداند و می‌خواهد منطق انتخاب شیء را کپسوله کند. +* این الگو معمولاً هنگام طراحی فریم‌ورک‌ها یا کتابخانه‌ها برای ارائه بهترین انعطاف‌پذیری و جداسازی از انواع کلاس‌های خاص استفاده می‌شود. + +## کاربردهای دنیای واقعی الگوی factory در جاوا + +> * [java.util.Calendar#getInstance()](https://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html#getInstance--) +> * [java.util.ResourceBundle#getBundle()](https://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-) +> * [java.text.NumberFormat#getInstance()](https://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html#getInstance--) +> * [java.nio.charset.Charset#forName()](https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html#forName-java.lang.String-) +> * این مورد [java.net.URLStreamHandlerFactory#createURLStreamHandler(String)](https://docs.oracle.com/javase/8/docs/api/java/net/URLStreamHandlerFactory.html) اشیاء singleton مختلف را بر اساس یک پروتکل بازمی‌گرداند +> * [java.util.EnumSet#of()](https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html#of(E)) +> * [javax.xml.bind.JAXBContext#createMarshaller()](https://docs.oracle.com/javase/8/docs/api/javax/xml/bind/JAXBContext.html#createMarshaller--) و متدهای مشابه دیگر. +> +> * کتابخانه‌ی JavaFX از الگوهای factory برای ایجاد کنترل‌های مختلف رابط کاربری متناسب با نیازهای محیط کاربر استفاده می‌کند. + +## مزایا و معایب الگوی factory + +### مزایا: + +> * پیاده‌سازی الگوی factory در برنامه جاوای شما، وابستگی بین پیاده‌سازی و کلاس‌هایی که استفاده می‌کند را کاهش می‌دهد. +> * از [اصل Open/Closed](https://java-design-patterns.com/principles/#open-closed-principle) پشتیبانی می‌کند، زیرا سیستم می‌تواند انواع جدیدی را بدون تغییر کد موجود معرفی کند. + +### معایب: + +> * کد می‌تواند به دلیل معرفی چندین کلاس اضافی پیچیده‌تر شود. +> * استفاده بیش از حد می‌تواند کد را کمتر خوانا کند اگر پیچیدگی ایجاد اشیاء کم یا غیرضروری باشد. + +## الگوهای طراحی مرتبط با جاوا + +> * الگوی [Abstract Factory](https://java-design-patterns.com/patterns/abstract-factory/): می‌توان آن را نوعی factory در نظر گرفت که با گروهی از محصولات کار می‌کند. +> * الگوی [Singleton](https://java-design-patterns.com/patterns/singleton/): اغلب همراه با factory استفاده می‌شود تا اطمینان حاصل شود که یک کلاس تنها یک نمونه دارد. +> * الگوی [Builder](https://java-design-patterns.com/patterns/builder/): ساخت یک شیء پیچیده را از نمایش آن جدا می‌کند، مشابه نحوه‌ای که factoryها مدیریت نمونه‌سازی را انجام می‌دهند. +> * الگوی [Factory Kit](https://java-design-patterns.com/patterns/factory-kit/): یک factory از محتوای غیرقابل تغییر با رابط‌های builder و factory جداگانه است. + +## منابع و اعتبارات + +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0Rk5y) +* [Effective Java](https://amzn.to/4cGk2Jz) +* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/3UpTLrG) diff --git a/localization/fa/factory/etc/factory-sequence-diagram.png b/localization/fa/factory/etc/factory-sequence-diagram.png new file mode 100644 index 000000000000..260bea92f247 Binary files /dev/null and b/localization/fa/factory/etc/factory-sequence-diagram.png differ diff --git a/microservices-self-registration/README.md b/microservices-self-registration/README.md new file mode 100644 index 000000000000..bfc837817c57 --- /dev/null +++ b/microservices-self-registration/README.md @@ -0,0 +1,236 @@ +--- +title: "Microservices Self-Registration Pattern in Java with Spring Boot and Eureka" +shortTitle: Microservices Pattern - Self-Registration +description: "Dynamically register and discover Java microservices using Spring Boot and Eureka for resilient, scalable communication." +category: Service Discovery +language: en +tag: + - Microservices + - Self-Registration + - Service Discovery + - Eureka + - Spring Boot + - Spring Cloud + - Java + - Dynamic Configuration + - Resilience +--- + +## Intent of Microservices Self-Registration Pattern + +The intent of the Self-Registration pattern is to enable microservices to automatically announce their presence and location to a central registry (like Eureka) upon startup, simplifying service discovery and allowing other services to find and communicate with them without manual configuration or hardcoded addresses. This promotes dynamic and resilient microservices architectures. + +## What's in the Project + +This project demonstrates the Microservices Self-Registration pattern using Java, Spring Boot (version 3.4.4), and Eureka for service discovery. It consists of three main components: a Eureka Server and two simple microservices, a Greeting Service and a Context Service, which discover and communicate with each other. + +### Project Structure +* **`eureka-server`:** The central service registry where microservices register themselves. +* **`greeting-service`:** A simple microservice that provides a greeting. +* **`context-service`:** A microservice that consumes the greeting from the Greeting Service and adds context. + + The **Eureka Server** acts as the discovery service. Microservices register themselves with the Eureka Server, providing their network location. + + package com.example.eurekaserver; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + + @SpringBootApplication + @EnableEurekaServer + public class EurekaServerApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } + } + + The **Greeting Service** is a simple microservice that exposes an endpoint to retrieve a greeting. + + package com.example.greetingservice; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + + @SpringBootApplication + @EnableDiscoveryClient + public class GreetingServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(GreetingServiceApplication.class, args); + } + } + + Greeting Controller + + package com.example.greetingservice.controller; + + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class GreetingController { + + @GetMapping("/greeting") + public String getGreeting() { + return "Hello"; + } + } + +The **Context Service** consumes the greeting from the Greeting Service using OpenFeign and adds contextual information. + + package com.example.contextservice; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + import org.springframework.cloud.openfeign.EnableFeignClients; + + @SpringBootApplication + @EnableDiscoveryClient + @EnableFeignClients + public class ContextServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ContextServiceApplication.class, args); + } + } + + Feign Client : Spring Cloud OpenFeign is a declarative HTTP client that makes it easier to consume RESTful web services in your Spring Cloud applications. Instead of writing the boilerplate code for making HTTP requests, you simply declare interface with annotations that describe the web service you want to consume. + + package com.example.contextservice.client; + + import org.springframework.cloud.openfeign.FeignClient; + import org.springframework.web.bind.annotation.GetMapping; + + @FeignClient(name = "greeting-service") + public interface GreetingServiceClient { + + @GetMapping("/greeting") + String getGreeting(); + } + + Context Controller + + package com.example.contextservice.controller; + + import com.example.contextservice.client.GreetingServiceClient; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.beans.factory.annotation.Value; + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class ContextController { + + @Autowired + private GreetingServiceClient greetingServiceClient; + + @Value("${user.region}") + private String userRegion; + + @GetMapping("/context") + public String getContext() { + String greeting = greetingServiceClient.getGreeting(); + return "The Greeting Service says: " + greeting + " from " + userRegion + "!"; + } + } + + 1. Both the Greeting Service and the Context Service register themselves with the Eureka Server upon startup using the _@EnableDiscoveryClient_ annotation. + 2. The Context Service, annotated with _@EnableFeignClients_, uses the GreetingServiceClient interface with _@FeignClient(name = "greeting-service")_ to declare its intent to communicate with the service named "greeting-service" in Eureka. + 3. When the /context endpoint of the Context Service is accessed, it calls the _getGreeting()_ method of the GreetingServiceClient. + 4. OpenFeign, leveraging the service discovery information from Eureka, resolves the network location of an available instance of the Greeting Service and makes an HTTP GET request to its /greeting endpoint. + 5. The Greeting Service responds with "Hello", and the Context Service then adds the configured user.region to the response. + + This project utilizes Spring Boot Actuator, which is included as a dependency, to provide health check endpoints for each microservice. These endpoints (e.g., /actuator/health) can be used by Eureka Server to monitor the health of the registered instances. + +## Steps to use for this Project + +Prerequisites: + - Java Development Kit (JDK): Make sure you have a compatible JDK installed (ideally Java 17 or later, as Spring Boot 3.x requires it). + - Maven or Gradle: You'll need either Maven (if you chose Maven during Spring Initializr setup) or Gradle (if you chose Gradle) installed on your system. + - An IDE (Optional but Recommended): IntelliJ IDEA, Eclipse, or Spring Tool Suite (STS) can make it easier to work with the project. + - Web Browser: You'll need a web browser to access the Eureka dashboard and the microservice endpoints. + +Step : + - You'll need to build each microservice individually. Navigate to the root directory of each project in your terminal or command prompt and run the appropriate build command: + _cd eurekaserver + mvn clean install + cd ../greetingservice + mvn clean install + cd ../contextservice + mvn clean install_ +Step : + - Navigate to the root directory of your eurekaserver project in your terminal or command prompt + _mvn spring-boot:run_ + - Wait for the Eureka Server application to start. You should see logs in the console indicating that it has started on port 8761 (as configured). + - Open your web browser and go to http://localhost:8761/. You should see the Eureka Server dashboard. Initially, the list of registered instances will be empty. +Step : + - Run the Greeting Service + - Open a new terminal or command prompt. + - Navigate to the root directory of your greetingservice project. + - Run the Spring Boot application: _mvn spring-boot:run_ + - Wait for the Greeting Service to start. You should see logs indicating that it has registered with the Eureka Server. + - Go back to your Eureka Server dashboard in the browser (http://localhost:8761/). You should now see GREETINGSERVICE listed under the "Instances currently registered with Eureka". Its status should be "UP". +Step : + - Run the Context Service + - Open a new terminal or command prompt. + - Navigate to the root directory of your contextservice project. + - Run the Spring Boot application: _mvn spring-boot:run_ + - Wait for the Context Service to start. You should see logs indicating that it has registered with the Eureka Server. + - Go back to your Eureka Server dashboard in the browser (http://localhost:8761/). You should now see CONTEXTSERVICE listed under the "Instances currently registered with Eureka". Its status should be "UP". +STEP : + - Test the Greeting Service Directly: Open your web browser and go to http://localhost:8081/greeting. You should see the output: Hello. + - Test the Context Service (which calls the Greeting Service): Open your web browser and go to http://localhost:8082/context. You should see the output: The Greeting Service says: Hello from Chennai, Tamil Nadu, India!. This confirms that the Context Service successfully discovered and called the Greeting Service through Eureka. + +Optional: Check Health Endpoints + +You can also verify the health status of each service using Spring Boot Actuator: + - Greeting Service Health: http://localhost:8081/actuator/health (should return {"status":"UP"}) + - Context Service Health: http://localhost:8082/actuator/health (should return {"status":"UP"}) + - Eureka Server Health: http://localhost:8761/actuator/health (should return {"status":"UP"}) + +## When to use Microservices Self-Registration Pattern + + - **Dynamic Environments:** When your microservices are frequently deployed, scaled up or down, or their network locations (IP addresses and ports) change often. This is common in cloud-based or containerized environments (like Docker and Kubernetes). + - **Large Number of Services:** As the number of microservices in your system grows, manually managing their configurations and dependencies becomes complex and error-prone. Self-registration automates this process. + - **Need for Automatic Service** Discovery: When services need to find and communicate with each other without hardcoding network locations. This allows for greater flexibility and reduces coupling. + - **Implementing Load Balancing:** Service registries like Eureka often integrate with load balancers, enabling them to automatically distribute traffic across available instances of a service that have registered themselves. + - **Improving System Resilience:** If a service instance fails, the registry will eventually be updated (through heartbeats or health checks), and other services can discover and communicate with the remaining healthy instances. + - **DevOps Automation:** This pattern aligns well with DevOps practices, allowing for more automated deployment and management of microservices. + +## Real-World Applications of Self-Registration pattern + + - E-Commerce platforms have numerous independent services for product catalogs, order processing, payments, shipping, etc. Self-registration allows these services to dynamically discover and communicate with each other as the system scales during peak loads or as new features are deployed. + - Streaming services rely on many microservices for user authentication, content delivery networks (CDNs), recommendation engines, billing systems, etc. Self-registration helps these services adapt to varying user demands and infrastructure changes. + - Social media These platforms use microservices for managing user profiles, timelines, messaging, advertising, and more. Self-registration enables these services to scale independently and handle the massive traffic they experience. + +## Advantages + + - Microservices can dynamically locate and communicate with each other without needing to know their specific network addresses beforehand. This is crucial in dynamic environments where IP addresses and ports can change frequently. + - Reduces the need for manual configuration of service locations in each microservice. Services don't need to be updated every time another service's location changes. + - Scaling microservices up or down becomes easier. New instances automatically register themselves with the service registry, making them immediately discoverable by other services without manual intervention. + - If a service instance fails, it will eventually stop sending heartbeats to the registry and will be removed. Consumers can then discover and connect to other healthy instances, improving the system's overall resilience. + - Services are less tightly coupled as they don't have direct dependencies on the physical locations of other services. This makes deployments and updates more flexible. + - Service registries often integrate with load balancers. When a new service instance registers, the load balancer can automatically include it in the pool of available instances, distributing traffic effectively. + - Microservices can be deployed across different environments (development, testing, production) without significant changes to their discovery mechanism, as long as they are configured to connect to the appropriate service registry for that environment. + +## Trade-offs + + - Introducing a service registry adds another component to your system that needs to be set up, managed, and monitored. This increases the overall complexity of the infrastructure. + - The service registry itself becomes a critical component. If the service registry becomes unavailable, it can disrupt communication between microservices. High availability for the service registry is therefore essential. + - Microservices need to communicate with the service registry for registration, sending heartbeats, and querying for other services. This can lead to increased network traffic. + - There might be a slight delay between when a microservice instance starts and when it becomes fully registered and discoverable in the service registry. This needs to be considered, especially during scaling events. + - You need to consider how your microservices will behave if they fail to register with the service registry upon startup. Robust error handling and retry mechanisms are often necessary. + - Microservices need to include and configure client libraries (like the Eureka Discovery Client) to interact with the service registry. This adds a dependency to your application code. + - In distributed service registries, ensuring consistency of the registry data across all nodes can be a challenge. Different registries might have different consistency models (e.g., eventual consistency). + +## References + + - Microservices Patterns: https://microservices.io/ + - Eureka Documentation: https://github.com/Netflix/eureka | https://spring.io/projects/spring-cloud-netflix + - Spring Boot Documentation: https://spring.io/projects/spring-boot + - Spring Cloud OpenFeignDocumentation: https://spring.io/projects/spring-cloud-openfeign + - Spring Boot Actuator Documentation: https://www.baeldung.com/spring-boot-actuators \ No newline at end of file diff --git a/microservices-self-registration/application.log.2025-04-09.0.gz b/microservices-self-registration/application.log.2025-04-09.0.gz new file mode 100644 index 000000000000..d51965d73d7a Binary files /dev/null and b/microservices-self-registration/application.log.2025-04-09.0.gz differ diff --git a/microservices-self-registration/contextservice/.gitattributes b/microservices-self-registration/contextservice/.gitattributes new file mode 100644 index 000000000000..3b41682ac579 --- /dev/null +++ b/microservices-self-registration/contextservice/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/microservices-self-registration/contextservice/.gitignore b/microservices-self-registration/contextservice/.gitignore new file mode 100644 index 000000000000..549e00a2a96f --- /dev/null +++ b/microservices-self-registration/contextservice/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/microservices-self-registration/contextservice/application.log.2025-04-05.0.gz b/microservices-self-registration/contextservice/application.log.2025-04-05.0.gz new file mode 100644 index 000000000000..6ceb03b833a7 Binary files /dev/null and b/microservices-self-registration/contextservice/application.log.2025-04-05.0.gz differ diff --git a/microservices-self-registration/contextservice/application.log.2025-04-07.0.gz b/microservices-self-registration/contextservice/application.log.2025-04-07.0.gz new file mode 100644 index 000000000000..eb2a63ce194e Binary files /dev/null and b/microservices-self-registration/contextservice/application.log.2025-04-07.0.gz differ diff --git a/microservices-self-registration/contextservice/application.log.2025-04-09.0.gz b/microservices-self-registration/contextservice/application.log.2025-04-09.0.gz new file mode 100644 index 000000000000..bd773dc8ba59 Binary files /dev/null and b/microservices-self-registration/contextservice/application.log.2025-04-09.0.gz differ diff --git a/microservices-self-registration/contextservice/pom.xml b/microservices-self-registration/contextservice/pom.xml new file mode 100644 index 000000000000..ea6d105bd06f --- /dev/null +++ b/microservices-self-registration/contextservice/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + com.learning + contextservice + 0.0.1-SNAPSHOT + contextservice + contextservice + + + 2024.0.1 + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + 1.18.38 + provided + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/ContextserviceApplication.java b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/ContextserviceApplication.java new file mode 100644 index 000000000000..eb22d094ffce --- /dev/null +++ b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/ContextserviceApplication.java @@ -0,0 +1,17 @@ +package com.learning.contextservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +public class ContextserviceApplication { + + public static void main(String[] args) { + SpringApplication.run(ContextserviceApplication.class, args); + } + +} diff --git a/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/MyCustomHealthCheck.java b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/MyCustomHealthCheck.java new file mode 100644 index 000000000000..0226fc50e803 --- /dev/null +++ b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/MyCustomHealthCheck.java @@ -0,0 +1,42 @@ +package com.learning.contextservice; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + + +@Component("myCustomHealthCheck") +public class MyCustomHealthCheck implements HealthIndicator { + + private static final Logger log = LoggerFactory.getLogger(MyCustomHealthCheck.class); + + private volatile boolean isHealthy = true; + + @Scheduled(fixedRate = 5000) // Run every 5 seconds + public void updateHealthStatus() { + // Perform checks here to determine the current health + // For example, check database connectivity, external service availability, etc. + isHealthy = performHealthCheck(); + log.info("Update health status : {}", isHealthy); + } + + boolean performHealthCheck() { + boolean current = System.currentTimeMillis() % 10000 < 5000; // Simulate fluctuating health + log.debug("Performing health check, current status: {}", current); + return current; // Simulate fluctuating health + } + + @Override + public Health health() { + if (isHealthy) { + log.info("Health check successful, service is UP"); + return Health.up().withDetail("message", "Service is running and scheduled checks are OK").build(); + } else { + log.warn("Health check failed, service is DOWN"); + return Health.down().withDetail("error", "Scheduled health checks failed").build(); + } + } +} diff --git a/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/client/GreetingServiceClient.java b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/client/GreetingServiceClient.java new file mode 100644 index 000000000000..367a3bdd496c --- /dev/null +++ b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/client/GreetingServiceClient.java @@ -0,0 +1,11 @@ +package com.learning.contextservice.client; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient(name = "greetingservice") +public interface GreetingServiceClient { + + @GetMapping("/greeting") + String getGreeting(); +} diff --git a/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/controller/ContextController.java b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/controller/ContextController.java new file mode 100644 index 000000000000..5ad8969e0871 --- /dev/null +++ b/microservices-self-registration/contextservice/src/main/java/com/learning/contextservice/controller/ContextController.java @@ -0,0 +1,26 @@ +package com.learning.contextservice.controller; + +import com.learning.contextservice.client.GreetingServiceClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ContextController { + + private final GreetingServiceClient greetingServiceClient; + private final String userRegion; + + @Autowired + public ContextController(GreetingServiceClient greetingServiceClient, @Value("${user.region}") String userRegion) { + this.greetingServiceClient = greetingServiceClient; + this.userRegion = userRegion; + } + + @GetMapping("/context") + public String getContext() { + String greeting = greetingServiceClient.getGreeting(); + return "The Greeting Service says: "+greeting+" from "+userRegion; + } +} diff --git a/microservices-self-registration/contextservice/src/main/resources/application.yml b/microservices-self-registration/contextservice/src/main/resources/application.yml new file mode 100644 index 000000000000..dfef73bbbeec --- /dev/null +++ b/microservices-self-registration/contextservice/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8082 + +spring: + application: + name: contextservice + +eureka: + client: + service-url.defaultZone: http://localhost:8761/eureka + +user: + region: Chennai, Tamil Nadu, India + +management: + endpoint: + health: + show-details: always + web: + exposure: + include: health + +logging: + file: + name: application.log diff --git a/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextControllerTest.java b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextControllerTest.java new file mode 100644 index 000000000000..f11da867cda0 --- /dev/null +++ b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextControllerTest.java @@ -0,0 +1,49 @@ +package com.learning.contextservice; + +import com.learning.contextservice.client.GreetingServiceClient; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest(classes = ContextserviceApplication.class) +@AutoConfigureMockMvc +@Import(TestConfig.class) +class ContextControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private GreetingServiceClient greetingServiceClient; + + @Value("${user.region}") + private String userRegion; + + @Test + void shouldReturnContextGreeting() throws Exception{ + Mockito.when(greetingServiceClient.getGreeting()).thenReturn("Mocked Hello"); + + mockMvc.perform(MockMvcRequestBuilders.get("/context") + .accept(MediaType.TEXT_PLAIN)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().string("The Greeting Service says: Mocked Hello from Chennai, Tamil Nadu, India")); + } + + @Test + void shouldReturnContextServiceHealthStatusUp() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/actuator/health")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("\"status\":\"UP\""))); + } +} diff --git a/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextserviceApplicationTests.java b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextserviceApplicationTests.java new file mode 100644 index 000000000000..a5d5c869c664 --- /dev/null +++ b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/ContextserviceApplicationTests.java @@ -0,0 +1,17 @@ +package com.learning.contextservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ContextserviceApplicationTests { + + @Test + void contextLoads() { + // This is a basic integration test that checks if the Spring Application Context loads successfully. + // If the context loads without any exceptions, the test is considered passing. + // It is often left empty as the act of loading the context is the primary verification. + // You can add specific assertions here if you want to verify the presence or state of certain beans. + } + +} diff --git a/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/TestConfig.java b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/TestConfig.java new file mode 100644 index 000000000000..f378f46f59df --- /dev/null +++ b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/TestConfig.java @@ -0,0 +1,17 @@ +package com.learning.contextservice; + +import com.learning.contextservice.client.GreetingServiceClient; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestConfig { + + @Bean + public GreetingServiceClient greetingServiceClient() { + GreetingServiceClient mockClient = Mockito.mock(GreetingServiceClient.class); + Mockito.when(mockClient.getGreeting()).thenReturn("Mocked Hello"); + return mockClient; + } +} diff --git a/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/myCustomHealthCheckTest.java b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/myCustomHealthCheckTest.java new file mode 100644 index 000000000000..129209469827 --- /dev/null +++ b/microservices-self-registration/contextservice/src/test/java/com/learning/contextservice/myCustomHealthCheckTest.java @@ -0,0 +1,33 @@ +package com.learning.contextservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.boot.actuate.health.Status; +import static org.junit.jupiter.api.Assertions.*; + +class MyCustomHealthCheckTest { + + @Test + void testHealthUp() { + MyCustomHealthCheck healthCheck = new MyCustomHealthCheck(); + // Simulate a healthy state + ReflectionTestUtils.setField(healthCheck, "isHealthy", true); + Health health = healthCheck.health(); + assertEquals(Status.UP, health.getStatus()); + assertTrue(health.getDetails().containsKey("message")); + assertEquals("Service is running and scheduled checks are OK", health.getDetails().get("message")); + } + + @Test + void testHealthDown() { + MyCustomHealthCheck healthCheck = new MyCustomHealthCheck(); + // Simulate an unhealthy state + ReflectionTestUtils.setField(healthCheck, "isHealthy", false); + Health health = healthCheck.health(); + assertEquals(Status.DOWN, health.getStatus()); + assertTrue(health.getDetails().containsKey("error")); + assertEquals("Scheduled health checks failed", health.getDetails().get("error")); + } + +} \ No newline at end of file diff --git a/microservices-self-registration/eurekaserver/.gitattributes b/microservices-self-registration/eurekaserver/.gitattributes new file mode 100644 index 000000000000..3b41682ac579 --- /dev/null +++ b/microservices-self-registration/eurekaserver/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/microservices-self-registration/eurekaserver/.gitignore b/microservices-self-registration/eurekaserver/.gitignore new file mode 100644 index 000000000000..549e00a2a96f --- /dev/null +++ b/microservices-self-registration/eurekaserver/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/microservices-self-registration/eurekaserver/pom.xml b/microservices-self-registration/eurekaserver/pom.xml new file mode 100644 index 000000000000..b1a4b26cf4f4 --- /dev/null +++ b/microservices-self-registration/eurekaserver/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + com.learning + eurekaserver + 0.0.1-SNAPSHOT + eurekaserver + eurekaserver + + + 2024.0.1 + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/microservices-self-registration/eurekaserver/src/main/java/com/learning/eurekaserver/EurekaserverApplication.java b/microservices-self-registration/eurekaserver/src/main/java/com/learning/eurekaserver/EurekaserverApplication.java new file mode 100644 index 000000000000..80b3d904ff4c --- /dev/null +++ b/microservices-self-registration/eurekaserver/src/main/java/com/learning/eurekaserver/EurekaserverApplication.java @@ -0,0 +1,15 @@ +package com.learning.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaserverApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaserverApplication.class, args); + } + +} diff --git a/microservices-self-registration/eurekaserver/src/main/resources/application.yml b/microservices-self-registration/eurekaserver/src/main/resources/application.yml new file mode 100644 index 000000000000..51f8a815d251 --- /dev/null +++ b/microservices-self-registration/eurekaserver/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8761 + +eureka: + client: + register-with-eureka: false + fetch-registry: false + server: + enable-self-preservation: true + diff --git a/microservices-self-registration/eurekaserver/src/test/java/com/learning/eurekaserver/EurekaserverApplicationTests.java b/microservices-self-registration/eurekaserver/src/test/java/com/learning/eurekaserver/EurekaserverApplicationTests.java new file mode 100644 index 000000000000..b5150fefa940 --- /dev/null +++ b/microservices-self-registration/eurekaserver/src/test/java/com/learning/eurekaserver/EurekaserverApplicationTests.java @@ -0,0 +1,17 @@ +package com.learning.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaserverApplicationTests { + + @Test + void contextLoads() { + // This is a basic integration test that checks if the Spring Application Context loads successfully. + // If the context loads without any exceptions, the test is considered passing. + // It is often left empty as the act of loading the context is the primary verification. + // You can add specific assertions here if you want to verify the presence or state of certain beans. + } + +} diff --git a/microservices-self-registration/greetingservice/.gitattributes b/microservices-self-registration/greetingservice/.gitattributes new file mode 100644 index 000000000000..3b41682ac579 --- /dev/null +++ b/microservices-self-registration/greetingservice/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/microservices-self-registration/greetingservice/.gitignore b/microservices-self-registration/greetingservice/.gitignore new file mode 100644 index 000000000000..549e00a2a96f --- /dev/null +++ b/microservices-self-registration/greetingservice/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/microservices-self-registration/greetingservice/application.log.2025-04-05.0.gz b/microservices-self-registration/greetingservice/application.log.2025-04-05.0.gz new file mode 100644 index 000000000000..93d6a2e62ac1 Binary files /dev/null and b/microservices-self-registration/greetingservice/application.log.2025-04-05.0.gz differ diff --git a/microservices-self-registration/greetingservice/application.log.2025-04-07.0.gz b/microservices-self-registration/greetingservice/application.log.2025-04-07.0.gz new file mode 100644 index 000000000000..40c96f702853 Binary files /dev/null and b/microservices-self-registration/greetingservice/application.log.2025-04-07.0.gz differ diff --git a/microservices-self-registration/greetingservice/application.log.2025-04-09.0.gz b/microservices-self-registration/greetingservice/application.log.2025-04-09.0.gz new file mode 100644 index 000000000000..59f2cbc3c8df Binary files /dev/null and b/microservices-self-registration/greetingservice/application.log.2025-04-09.0.gz differ diff --git a/microservices-self-registration/greetingservice/application.log.2025-04-11.0.gz b/microservices-self-registration/greetingservice/application.log.2025-04-11.0.gz new file mode 100644 index 000000000000..62d73c3020c1 Binary files /dev/null and b/microservices-self-registration/greetingservice/application.log.2025-04-11.0.gz differ diff --git a/microservices-self-registration/greetingservice/pom.xml b/microservices-self-registration/greetingservice/pom.xml new file mode 100644 index 000000000000..45988a145866 --- /dev/null +++ b/microservices-self-registration/greetingservice/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + com.learning + greetingservice + 0.0.1-SNAPSHOT + greetingservice + greetingservice + + + 2024.0.1 + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + 1.18.38 + provided + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/GreetingserviceApplication.java b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/GreetingserviceApplication.java new file mode 100644 index 000000000000..7a549a084284 --- /dev/null +++ b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/GreetingserviceApplication.java @@ -0,0 +1,17 @@ +package com.learning.greetingservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@EnableDiscoveryClient +@ComponentScan("com.learning.greetingservice.controller") +public class GreetingserviceApplication { + + public static void main(String[] args) { + SpringApplication.run(GreetingserviceApplication.class, args); + } + +} diff --git a/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/MyCustomHealthCheck.java b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/MyCustomHealthCheck.java new file mode 100644 index 000000000000..218a4ad002d4 --- /dev/null +++ b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/MyCustomHealthCheck.java @@ -0,0 +1,41 @@ +package com.learning.greetingservice; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component("myCustomHealthCheck") +public class MyCustomHealthCheck implements HealthIndicator { + + private static final Logger log = LoggerFactory.getLogger(MyCustomHealthCheck.class); + + private volatile boolean isHealthy = true; + + @Scheduled(fixedRate = 5000) // Run every 5 seconds + public void updateHealthStatus() { + // Perform checks here to determine the current health + // For example, check database connectivity, external service availability, etc. + isHealthy = performHealthCheck(); + log.info("Update health status : {}", isHealthy); + } + + boolean performHealthCheck() { + boolean current = System.currentTimeMillis() % 10000 < 5000; // Simulate fluctuating health + log.debug("Performing health check, current status: {}", current); + return current; // Simulate fluctuating health + } + + @Override + public Health health() { + if (isHealthy) { + log.info("Health check successful, service is UP"); + return Health.up().withDetail("message", "Service is running and scheduled checks are OK").build(); + } else { + log.warn("Health check failed, service is DOWN"); + return Health.down().withDetail("error", "Scheduled health checks failed").build(); + } + } +} diff --git a/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/controller/GreetingsController.java b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/controller/GreetingsController.java new file mode 100644 index 000000000000..ea385beb1abe --- /dev/null +++ b/microservices-self-registration/greetingservice/src/main/java/com/learning/greetingservice/controller/GreetingsController.java @@ -0,0 +1,13 @@ +package com.learning.greetingservice.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingsController { + + @GetMapping("/greeting") + public String getGreeting() { + return "Hello"; + } +} diff --git a/microservices-self-registration/greetingservice/src/main/resources/application.yml b/microservices-self-registration/greetingservice/src/main/resources/application.yml new file mode 100644 index 000000000000..adcfac884c2b --- /dev/null +++ b/microservices-self-registration/greetingservice/src/main/resources/application.yml @@ -0,0 +1,22 @@ +server: + port: 8081 + +spring: + application: + name: greetingservice +eureka: + client: + service-url.defaultZone: http://localhost:8761/eureka + +management: + endpoint: + health: + show-details: always + web: + exposure: + include: health + +logging: + file: + name: application.log + diff --git a/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/GreetingserviceApplicationTests.java b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/GreetingserviceApplicationTests.java new file mode 100644 index 000000000000..945898278aa9 --- /dev/null +++ b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/GreetingserviceApplicationTests.java @@ -0,0 +1,17 @@ +package com.learning.greetingservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GreetingserviceApplicationTests { + + @Test + void contextLoads() { + // This is a basic integration test that checks if the Spring Application Context loads successfully. + // If the context loads without any exceptions, the test is considered passing. + // It is often left empty as the act of loading the context is the primary verification. + // You can add specific assertions here if you want to verify the presence or state of certain beans. + } + +} diff --git a/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/MyCustomHealthCheckTest.java b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/MyCustomHealthCheckTest.java new file mode 100644 index 000000000000..8ba8b2a30f93 --- /dev/null +++ b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/MyCustomHealthCheckTest.java @@ -0,0 +1,22 @@ +package com.learning.greetingservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.boot.actuate.health.Status; +import static org.junit.jupiter.api.Assertions.*; + +class MyCustomHealthCheckTest { + + @Test + void testHealthUp() { + MyCustomHealthCheck healthCheck = new MyCustomHealthCheck(); + // Simulate a healthy state + ReflectionTestUtils.setField(healthCheck, "isHealthy", true); + Health health = healthCheck.health(); + assertEquals(Status.UP, health.getStatus()); + assertTrue(health.getDetails().containsKey("message")); + assertEquals("Service is running and scheduled checks are OK", health.getDetails().get("message")); + } + +} \ No newline at end of file diff --git a/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/controller/GreetingControllerTest.java b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/controller/GreetingControllerTest.java new file mode 100644 index 000000000000..5ae98c8b6aeb --- /dev/null +++ b/microservices-self-registration/greetingservice/src/test/java/com/learning/greetingservice/controller/GreetingControllerTest.java @@ -0,0 +1,36 @@ +package com.learning.greetingservice.controller; + +import com.learning.greetingservice.GreetingserviceApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest(classes = GreetingserviceApplication.class) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class GreetingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldReturnGreeting() throws Exception{ + mockMvc.perform(MockMvcRequestBuilders.get("/greeting") + .accept(MediaType.TEXT_PLAIN)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().string("Hello")); + } + + @Test + void shouldReturnHealthStatusUp() throws Exception{ + mockMvc.perform(MockMvcRequestBuilders.get("/actuator/health")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().string(org.hamcrest.Matchers.containsString("\"status\":\"UP\""))); + } +} diff --git a/microservices-self-registration/pom.xml b/microservices-self-registration/pom.xml new file mode 100644 index 000000000000..4b708cb0eb12 --- /dev/null +++ b/microservices-self-registration/pom.xml @@ -0,0 +1,63 @@ + + + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + 4.0.0 + microservices-self-registration + pom + + eurekaserver + greetingservice + contextservice + + + + 21 + ${java.version} + ${java.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 46429c75af7c..82d15bdf1d74 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 0.8.13 1.4 - 4.7.0 + 4.17.0 2.11.0 6.0.0 1.1.0 @@ -59,11 +59,11 @@ 3.5.2 - 4.6 + 5.0.0 3.14.0 - 5.0.0.4389 + 5.1.0.4751 https://sonarcloud.io iluwatar iluwatar_java-design-patterns @@ -106,6 +106,7 @@ converter curiously-recurring-template-pattern currying + dao-factory data-access-object data-bus data-locality @@ -166,6 +167,7 @@ microservices-distributed-tracing microservices-idempotent-consumer microservices-log-aggregation + microservices-self-registration model-view-controller model-view-intent model-view-presenter @@ -230,6 +232,7 @@ table-module template-method templateview + thread-pool-executor throttling tolerant-reader trampoline @@ -240,9 +243,12 @@ update-method value-object version-number + view-helper virtual-proxy visitor - backpressure + backpressure + actor-model + rate-limiting-pattern
@@ -316,12 +322,6 @@ ${junit.version} test - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - org.junit.jupiter junit-jupiter-migrationsupport diff --git a/producer-consumer/README.md b/producer-consumer/README.md index bb62c799a08f..575be804cd45 100644 --- a/producer-consumer/README.md +++ b/producer-consumer/README.md @@ -181,10 +181,6 @@ Program output: 08:10:17.483 [pool-1-thread-5] INFO com.iluwatar.producer.consumer.Consumer -- Consumer [Consumer_2] consume item [10] produced by [Producer_1] ``` -## Detailed Explanation of Producer-Consumer Pattern with Real-World Examples - -![Producer-Consumer](./etc/producer-consumer.png "Producer-Consumer") - ## When to Use the Producer-Consumer Pattern in Java * When you need to manage a buffer or queue where producers add data and consumers take data, often in a multithreaded environment. diff --git a/prototype/README.md b/prototype/README.md index 48f10868a86c..b97df0707ffc 100644 --- a/prototype/README.md +++ b/prototype/README.md @@ -156,10 +156,6 @@ Here's the console output from running the example. 08:36:19.014 [main] INFO com.iluwatar.prototype.App -- Orcish wolf attacks with laser ``` -## Detailed Explanation of Prototype Pattern with Real-World Examples - -![alt text](./etc/prototype.urm.png "Prototype pattern class diagram") - ## When to Use the Prototype Pattern in Java * When the classes to instantiate are specified at run-time, for example, by dynamic loading. diff --git a/publish-subscribe/src/main/java/com/iluwatar/publish/subscribe/model/Topic.java b/publish-subscribe/src/main/java/com/iluwatar/publish/subscribe/model/Topic.java index 4c421afe735a..2011f096a5eb 100644 --- a/publish-subscribe/src/main/java/com/iluwatar/publish/subscribe/model/Topic.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publish/subscribe/model/Topic.java @@ -51,7 +51,7 @@ public void addSubscriber(Subscriber subscriber) { } /** - * Remove a subscriber to the list of subscribers. + * Remove a subscriber from the list of subscribers. * * @param subscriber subscriber to remove */ diff --git a/rate-limiting-pattern/README.md b/rate-limiting-pattern/README.md new file mode 100644 index 000000000000..de7742a5d13f --- /dev/null +++ b/rate-limiting-pattern/README.md @@ -0,0 +1,328 @@ +--- +title: "Rate Limiting Pattern in Java: Controlling System Overload Gracefully" +shortTitle: Rate Limiting +description: "Explore multiple rate limiting strategies in Java—Token Bucket, Fixed Window, and Adaptive Limiting. Learn with diagrams, programmatic examples, and real-world simulation." +category: Behavioral +language: en +tag: + - Resilience + - System Overload Protection + - API Throttling + - Concurrency + - Cloud Patterns +--- + +## Also known as + +- Throttling +- Request Limiting +- API Rate Limiting + +--- + +## Intent of Rate Limiting Design Pattern + +To regulate the number of requests sent to a service in a specific time window, avoiding resource exhaustion and ensuring system stability. This is especially useful in distributed and cloud-native architectures. + +--- + +## Detailed Explanation of Rate Limiting with Real-World Examples + +### Real-world example + +Imagine you're entering a concert hall that only allows 50 people per minute. If too many fans arrive at once, the gate staff slows down entry, allowing only a few at a time. This prevents overcrowding and ensures safety. Similarly, the rate limiter controls how many requests are processed to avoid overloading a server. + +### In plain words + +Regulate the number of requests a system handles within a time frame to protect availability and performance. + + +### AWS says + +> "API Gateway limits the steady-state rate and burst rate of requests that it allows for each method in your REST APIs. When request rates exceed these limits, API Gateway begins to throttle requests." + +— [API Gateway quotas and important notes - AWS Documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) + +--- + +## Architecture Diagram + +![UML Class Diagram](etc/UMLClassDiagram.png) + +This UML shows the key components: +- `RateLimiter` interface +- `TokenBucketRateLimiter`, `FixedWindowRateLimiter`, `AdaptiveRateLimiter` +- Supporting exception classes +- `FindCustomerRequest` as a rate-limited operation + +--- + +## Flowcharts + +### 1. Token Bucket Strategy + +![Token Bucket Rate Limiter](etc/TokenBucketRateLimiter.png) + +### 2. Fixed Window Strategy + +![Fixed Window Rate Limiter](etc/FixedWindowRateLimiter.png) + +### 3. Adaptive Rate Limiting Strategy + +![Adaptive Rate Limiter](etc/AdaptiveRateLimiter.png) + +--- + +### Programmatic Example of Rate Limiter Pattern in Java + +The **Rate Limiter** design pattern helps protect systems from overload by restricting the number of operations that can be performed in a given time window. It is especially useful when accessing shared resources, APIs, or services that are sensitive to spikes in traffic. + +This implementation demonstrates three strategies for rate limiting: + +- **Token Bucket Rate Limiter** +- **Fixed Window Rate Limiter** +- **Adaptive Rate Limiter** + +Let’s walk through the key components. + +--- + +#### 1. Token Bucket Rate Limiter + +The token bucket allows short bursts followed by a steady rate. Tokens are added periodically and requests are only allowed if a token is available. + +```java +public class TokenBucketRateLimiter implements RateLimiter { + private final int capacity; + private final int refillRate; + private final ConcurrentHashMap buckets = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public TokenBucketRateLimiter(int capacity, int refillRate) { + this.capacity = capacity; + this.refillRate = refillRate; + scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS); + } + + @Override + public void check(String serviceName, String operationName) throws RateLimitException { + String key = serviceName + ":" + operationName; + TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(capacity)); + if (!bucket.tryConsume()) { + throw new ThrottlingException(serviceName, operationName, 1000); + } + } + + private void refillBuckets() { + buckets.forEach((k, b) -> b.refill(refillRate)); + } + + private static class TokenBucket { + private final int capacity; + private final AtomicInteger tokens; + + TokenBucket(int capacity) { + this.capacity = capacity; + this.tokens = new AtomicInteger(capacity); + } + + boolean tryConsume() { + while (true) { + int current = tokens.get(); + if (current <= 0) return false; + if (tokens.compareAndSet(current, current - 1)) return true; + } + } + + void refill(int amount) { + tokens.getAndUpdate(current -> Math.min(current + amount, capacity)); + } + } +} +``` + +--- + +#### 2. Fixed Window Rate Limiter + +This strategy uses a simple counter within a fixed time window. + +```java +public class FixedWindowRateLimiter implements RateLimiter { + private final int limit; + private final long windowMillis; + private final ConcurrentHashMap counters = new ConcurrentHashMap<>(); + + public FixedWindowRateLimiter(int limit, long windowSeconds) { + this.limit = limit; + this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds); + } + + @Override + public synchronized void check(String serviceName, String operationName) throws RateLimitException { + String key = serviceName + ":" + operationName; + WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter()); + + if (!counter.tryIncrement()) { + throw new RateLimitException("Rate limit exceeded for " + key, windowMillis); + } + } + + private class WindowCounter { + private AtomicInteger count = new AtomicInteger(0); + private volatile long windowStart = System.currentTimeMillis(); + + synchronized boolean tryIncrement() { + long now = System.currentTimeMillis(); + if (now - windowStart > windowMillis) { + count.set(0); + windowStart = now; + } + return count.incrementAndGet() <= limit; + } + } +} +``` + +--- + +#### 3. Adaptive Rate Limiter + +This version adjusts the rate based on system health, reducing the rate when throttling occurs and recovering periodically. + +```java +public class AdaptiveRateLimiter implements RateLimiter { + private final int initialLimit; + private final int maxLimit; + private final AtomicInteger currentLimit; + private final ConcurrentHashMap limiters = new ConcurrentHashMap<>(); + private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1); + + public AdaptiveRateLimiter(int initialLimit, int maxLimit) { + this.initialLimit = initialLimit; + this.maxLimit = maxLimit; + this.currentLimit = new AtomicInteger(initialLimit); + healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS); + } + + @Override + public void check(String serviceName, String operationName) throws RateLimitException { + String key = serviceName + ":" + operationName; + int current = currentLimit.get(); + RateLimiter limiter = limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current)); + + try { + limiter.check(serviceName, operationName); + } catch (RateLimitException e) { + currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2)); + throw e; + } + } + + private void adjustLimits() { + currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2))); + } +} +``` + +--- + +#### 4. Simulated Demo Using All Limiters + +```java +public final class App { + public static void main(String[] args) { + TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1); + FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1); + AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6); + + ExecutorService executor = Executors.newFixedThreadPool(3); + for (int i = 1; i <= 3; i++) { + executor.submit(createClientTask(i, tb, fw, ar)); + } + } + + private static Runnable createClientTask(int clientId, RateLimiter tb, RateLimiter fw, RateLimiter ar) { + return () -> { + String[] services = {"s3", "dynamodb", "lambda"}; + String[] operations = {"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"}; + Random random = new Random(); + + while (true) { + String service = services[random.nextInt(services.length)]; + String operation = operations[random.nextInt(operations.length)]; + try { + switch (service) { + case "s3" -> tb.check(service, operation); + case "dynamodb" -> fw.check(service, operation); + case "lambda" -> ar.check(service, operation); + } + System.out.printf("Client %d: %s.%s - ALLOWED%n", clientId, service, operation); + } catch (RateLimitException e) { + System.out.printf("Client %d: %s.%s - THROTTLED%n", clientId, service, operation); + } + + try { + Thread.sleep(30 + random.nextInt(50)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + } +} +``` + +--- + +This example highlights how the Rate Limiter pattern supports various throttling techniques and how they respond under simulated traffic pressure, making it invaluable for building scalable, resilient systems. + +## When to Use Rate Limiting + +- APIs receiving unpredictable traffic +- Shared cloud resources (e.g., DB, compute) +- Services requiring fair client usage +- Preventing DoS or abuse + +--- + +## Real-World Applications + +- **AWS API Gateway** +- **Google Cloud Functions** +- **Netflix Zuul API Gateway** +- **Stripe API Throttling** + +--- + +## Benefits and Trade-offs + +### Benefits + +- Protects backend from overload +- Fair distribution of resources +- Better user experience under load + +### Trade-offs + +- May delay valid requests +- Requires tuning of limits +- Could create bottlenecks if misused + +--- + +## Related Java Design Patterns + +- [Circuit Breaker](https://java-design-patterns.com/patterns/circuit-breaker/) +- [Retry](https://java-design-patterns.com/patterns/retry/) +- [Throttling Queue](https://java-design-patterns.com/patterns/throttling/) + +--- + +## References and Credits + +- [Microsoft Cloud Design Patterns](https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling) +- [AWS API Gateway Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) +- *Designing Data-Intensive Applications* by Martin Kleppmann +- [Resilience4j](https://resilience4j.readme.io/) +- Java Design Patterns Project: [java-design-patterns](https://github.com/iluwatar/java-design-patterns) diff --git a/rate-limiting-pattern/etc/AdaptiveRateLimiter.png b/rate-limiting-pattern/etc/AdaptiveRateLimiter.png new file mode 100644 index 000000000000..2d849ee8d057 Binary files /dev/null and b/rate-limiting-pattern/etc/AdaptiveRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/FixedWindowRateLimiter.png b/rate-limiting-pattern/etc/FixedWindowRateLimiter.png new file mode 100644 index 000000000000..a81f61d7fa95 Binary files /dev/null and b/rate-limiting-pattern/etc/FixedWindowRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/TokenBucketRateLimiter.png b/rate-limiting-pattern/etc/TokenBucketRateLimiter.png new file mode 100644 index 000000000000..d41701781e27 Binary files /dev/null and b/rate-limiting-pattern/etc/TokenBucketRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/UMLClassDiagram.png b/rate-limiting-pattern/etc/UMLClassDiagram.png new file mode 100644 index 000000000000..9292880244e0 Binary files /dev/null and b/rate-limiting-pattern/etc/UMLClassDiagram.png differ diff --git a/rate-limiting-pattern/pom.xml b/rate-limiting-pattern/pom.xml new file mode 100644 index 000000000000..9d5d2624ee8b --- /dev/null +++ b/rate-limiting-pattern/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + rate-limiter + + + 22 + 22 + UTF-8 + 5.11.1 + 1.11.1 + + + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + org.mockito + mockito-core + 5.12.0 + test + + + + org.slf4j + slf4j-api + 2.0.9 + + + + ch.qos.logback + logback-classic + 1.4.11 + + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.2 + + + + check + apply + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + + + + + diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java new file mode 100644 index 000000000000..1b18a8c941b9 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java @@ -0,0 +1,50 @@ +package com.iluwatar.rate.limiting.pattern; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** Adaptive rate limiter that adjusts limits based on system health. */ +public class AdaptiveRateLimiter implements RateLimiter { + private final int initialLimit; + private final int maxLimit; + private final AtomicInteger currentLimit; + private final ConcurrentHashMap limiters = new ConcurrentHashMap<>(); + private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1); + + public AdaptiveRateLimiter(int initialLimit, int maxLimit) { + this.initialLimit = initialLimit; + this.maxLimit = maxLimit; + this.currentLimit = new AtomicInteger(initialLimit); + // Periodically increase limit to recover if system appears healthy + healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS); + } + + @Override + public void check(String serviceName, String operationName) throws RateLimitException { + String key = serviceName + ":" + operationName; + int current = currentLimit.get(); + + // Reuse or create TokenBucket for this key using currentLimit + RateLimiter limiter = + limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current)); + + try { + limiter.check(serviceName, operationName); + System.out.printf( + "[Adaptive] Allowed %s.%s - CurrentLimit: %d%n", serviceName, operationName, current); + } catch (RateLimitException e) { + // On throttling, reduce system limit to reduce load + currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2)); + System.out.printf( + "[Adaptive] Throttled %s.%s - Decreasing limit to %d%n", + serviceName, operationName, currentLimit.get()); + throw e; + } + } + + // Periodic recovery mechanism to raise limits when the system is under control + private void adjustLimits() { + int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2))); + System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated); + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java new file mode 100644 index 000000000000..e76ed5254345 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java @@ -0,0 +1,178 @@ +package com.iluwatar.rate.limiting.pattern; + +import java.security.SecureRandom; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Rate Limiter pattern is a key defensive strategy used to prevent system overload and + * ensure fair usage of shared services. This demo showcases how different rate limiting techniques + * can regulate traffic in distributed systems. + * + *

Specifically, this simulation implements three rate limiter strategies: + * + *

    + *
  • Token Bucket – Allows short bursts followed by steady request rates. + *
  • Fixed Window – Enforces a strict limit per discrete time window (e.g., 3 + * requests/sec). + *
  • Adaptive – Dynamically scales limits based on system health, simulating elastic + * backoff. + *
+ * + *

Each simulated service (e.g., S3, DynamoDB, Lambda) is governed by one of these limiters. + * Multiple concurrent client threads issue randomized requests to these services over a fixed + * duration. Each request is either: + * + *

    + *
  • ALLOWED – Permitted under the current rate limit + *
  • THROTTLED – Rejected due to quota exhaustion + *
  • FAILED – Dropped due to transient service failure + *
+ * + *

Statistics are printed every few seconds, and the simulation exits gracefully after a fixed + * runtime, offering a clear view into how each limiter behaves under pressure. + * + *

Relation to AWS API Gateway:
+ * This implementation mirrors the throttling behavior described in the + * AWS API Gateway Request Throttling documentation, where limits are applied per second and + * over longer durations (burst and rate limits). The TokenBucketRateLimiter mimics + * burst capacity, the FixedWindowRateLimiter models steady rate enforcement, and the + * AdaptiveRateLimiter reflects elasticity in real-world systems like AWS Lambda under + * variable load. + */ +public final class App { + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + private static final int RUN_DURATION_SECONDS = 10; + private static final int SHUTDOWN_TIMEOUT_SECONDS = 5; + + static final AtomicInteger successfulRequests = new AtomicInteger(); + static final AtomicInteger throttledRequests = new AtomicInteger(); + static final AtomicInteger failedRequests = new AtomicInteger(); + static final AtomicBoolean running = new AtomicBoolean(true); + private static final String DIVIDER_LINE = "===================================="; + + public static void main(String[] args) { + LOGGER.info("Starting Rate Limiter Demo"); + LOGGER.info(DIVIDER_LINE); + + ExecutorService executor = Executors.newFixedThreadPool(3); + ScheduledExecutorService statsPrinter = Executors.newSingleThreadScheduledExecutor(); + + try { + TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1); + FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1); + AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6); + + statsPrinter.scheduleAtFixedRate(App::printStats, 2, 2, TimeUnit.SECONDS); + + for (int i = 1; i <= 3; i++) { + executor.submit(createClientTask(i, tb, fw, ar)); + } + + Thread.sleep(RUN_DURATION_SECONDS * 1000L); + LOGGER.info("Shutting down the demo..."); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + running.set(false); + shutdownExecutor(executor, "mainExecutor"); + shutdownExecutor(statsPrinter, "statsPrinter"); + printFinalStats(); + LOGGER.info("Demo completed."); + } + } + + private static void shutdownExecutor(ExecutorService service, String name) { + service.shutdown(); + try { + if (!service.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + service.shutdownNow(); + LOGGER.warn("Forced shutdown of {}", name); + } + } catch (InterruptedException e) { + service.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + static Runnable createClientTask( + int clientId, RateLimiter s3Limiter, RateLimiter dynamoDbLimiter, RateLimiter lambdaLimiter) { + + return () -> { + String[] services = {"s3", "dynamodb", "lambda"}; + String[] operations = { + "GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions" + }; + SecureRandom random = new SecureRandom(); // ✅ Safe & compliant for SonarCloud + + while (running.get() && !Thread.currentThread().isInterrupted()) { + try { + String service = services[random.nextInt(services.length)]; + String operation = operations[random.nextInt(operations.length)]; + + switch (service) { + case "s3" -> makeRequest(clientId, s3Limiter, service, operation); + case "dynamodb" -> makeRequest(clientId, dynamoDbLimiter, service, operation); + case "lambda" -> makeRequest(clientId, lambdaLimiter, service, operation); + default -> LOGGER.warn("Unknown service: {}", service); + } + + Thread.sleep(30L + random.nextInt(50)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + } + + static void makeRequest(int clientId, RateLimiter limiter, String service, String operation) { + try { + limiter.check(service, operation); + successfulRequests.incrementAndGet(); + LOGGER.info("Client {}: {}.{} - ALLOWED", clientId, service, operation); + } catch (ThrottlingException e) { + throttledRequests.incrementAndGet(); + LOGGER.warn( + "Client {}: {}.{} - THROTTLED (Retry in {}ms)", + clientId, + service, + operation, + e.getRetryAfterMillis()); + } catch (ServiceUnavailableException e) { + failedRequests.incrementAndGet(); + LOGGER.warn("Client {}: {}.{} - SERVICE UNAVAILABLE", clientId, service, operation); + } catch (Exception e) { + failedRequests.incrementAndGet(); + LOGGER.error("Client {}: {}.{} - ERROR: {}", clientId, service, operation, e.getMessage()); + } + } + + static void printStats() { + if (!running.get()) return; + LOGGER.info("=== Current Statistics ==="); + LOGGER.info("Successful Requests: {}", successfulRequests.get()); + LOGGER.info("Throttled Requests : {}", throttledRequests.get()); + LOGGER.info("Failed Requests : {}", failedRequests.get()); + LOGGER.info(DIVIDER_LINE); + } + + static void printFinalStats() { + LOGGER.info("Final Statistics"); + LOGGER.info(DIVIDER_LINE); + LOGGER.info("Successful Requests: {}", successfulRequests.get()); + LOGGER.info("Throttled Requests : {}", throttledRequests.get()); + LOGGER.info("Failed Requests : {}", failedRequests.get()); + LOGGER.info(DIVIDER_LINE); + } + + static void resetCountersForTesting() { + successfulRequests.set(0); + throttledRequests.set(0); + failedRequests.set(0); + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java new file mode 100644 index 000000000000..a61fc35f496a --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java @@ -0,0 +1,40 @@ +package com.iluwatar.rate.limiting.pattern; + +/** + * A rate-limited customer lookup operation. This class wraps the rate limiting logic and represents + * an executable business request. + */ +public class FindCustomerRequest implements RateLimitOperation { + private final String customerId; + private final RateLimiter rateLimiter; + + public FindCustomerRequest(String customerId, RateLimiter rateLimiter) { + this.customerId = customerId; + this.rateLimiter = rateLimiter; + } + + @Override + public String getServiceName() { + return "CustomerService"; + } + + @Override + public String getOperationName() { + return "FindCustomer"; + } + + @Override + public String execute() throws RateLimitException { + // Ensure the operation respects the assigned rate limiter + rateLimiter.check(getServiceName(), getOperationName()); + + // Simulate actual operation + try { + Thread.sleep(50); // Simulate processing time + return "Customer-" + customerId; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ServiceUnavailableException(getServiceName(), 1000); + } + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java new file mode 100644 index 000000000000..3a2b52888ca7 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java @@ -0,0 +1,53 @@ +package com.iluwatar.rate.limiting.pattern; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements a fixed window rate limiter. It allows up to 'limit' number of requests within a time + * window of fixed size. + */ +public class FixedWindowRateLimiter implements RateLimiter { + private final int limit; + private final long windowMillis; + private final ConcurrentHashMap counters = new ConcurrentHashMap<>(); + + public FixedWindowRateLimiter(int limit, long windowSeconds) { + this.limit = limit; + this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds); + } + + @Override + public synchronized void check(String serviceName, String operationName) + throws RateLimitException { + String key = serviceName + ":" + operationName; + WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter()); + + if (!counter.tryIncrement()) { + System.out.printf( + "[FixedWindow] Throttled %s.%s - Limit %d reached in window%n", + serviceName, operationName, limit); + throw new RateLimitException("Rate limit exceeded for " + key, windowMillis); + } else { + System.out.printf( + "[FixedWindow] Allowed %s.%s - Count within window%n", serviceName, operationName); + } + } + + /** Tracks the count of requests within the current window. */ + private class WindowCounter { + private AtomicInteger count = new AtomicInteger(0); + private volatile long windowStart = System.currentTimeMillis(); + + synchronized boolean tryIncrement() { + long now = System.currentTimeMillis(); + // Reset window if expired + if (now - windowStart > windowMillis) { + count.set(0); + windowStart = now; + } + // Enforce the request limit within window + return count.incrementAndGet() <= limit; + } + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java new file mode 100644 index 000000000000..2b3cc1f3006b --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java @@ -0,0 +1,15 @@ +package com.iluwatar.rate.limiting.pattern; + +/** Base exception for rate limiting errors. */ +public class RateLimitException extends Exception { + private final long retryAfterMillis; + + public RateLimitException(String message, long retryAfterMillis) { + super(message); + this.retryAfterMillis = retryAfterMillis; + } + + public long getRetryAfterMillis() { + return retryAfterMillis; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java new file mode 100644 index 000000000000..59191e81fc04 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java @@ -0,0 +1,10 @@ +package com.iluwatar.rate.limiting.pattern; + +/** Represents a business operation that needs rate limiting. Supports type-safe return values. */ +public interface RateLimitOperation { + String getServiceName(); + + String getOperationName(); + + T execute() throws RateLimitException; +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java new file mode 100644 index 000000000000..19495b401ddb --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java @@ -0,0 +1,13 @@ +package com.iluwatar.rate.limiting.pattern; + +/** Base interface for all rate limiter strategies. */ +public interface RateLimiter { + /** + * Checks if a request is allowed under current rate limits + * + * @param serviceName Service being called (e.g., "dynamodb") + * @param operationName Operation being performed (e.g., "Query") + * @throws RateLimitException if request exceeds limits + */ + void check(String serviceName, String operationName) throws RateLimitException; +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java new file mode 100644 index 000000000000..f9fc55f15d20 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java @@ -0,0 +1,15 @@ +package com.iluwatar.rate.limiting.pattern; + +/** Exception for when a service is temporarily unavailable. */ +public class ServiceUnavailableException extends RateLimitException { + private final String serviceName; + + public ServiceUnavailableException(String serviceName, long retryAfterMillis) { + super("Service temporarily unavailable: " + serviceName, retryAfterMillis); + this.serviceName = serviceName; + } + + public String getServiceName() { + return serviceName; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java new file mode 100644 index 000000000000..e07087dfee6c --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java @@ -0,0 +1,21 @@ +package com.iluwatar.rate.limiting.pattern; + +/** Exception thrown when AWS-style throttling occurs. */ +public class ThrottlingException extends RateLimitException { + private final String serviceName; + private final String errorCode; + + public ThrottlingException(String serviceName, String operationName, long retryAfterMillis) { + super("AWS throttling error for " + serviceName + "/" + operationName, retryAfterMillis); + this.serviceName = serviceName; + this.errorCode = "ThrottlingException"; + } + + public String getServiceName() { + return serviceName; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java new file mode 100644 index 000000000000..3001c880aad3 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java @@ -0,0 +1,64 @@ +package com.iluwatar.rate.limiting.pattern; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Token Bucket rate limiter implementation. Allows requests to proceed as long as there are tokens + * available in the bucket. Tokens are added at a fixed interval up to a defined capacity. + */ +public class TokenBucketRateLimiter implements RateLimiter { + private final int capacity; + private final int refillRate; + private final ConcurrentHashMap buckets = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public TokenBucketRateLimiter(int capacity, int refillRate) { + this.capacity = capacity; + this.refillRate = refillRate; + // Refill tokens in all buckets every second + scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS); + } + + @Override + public void check(String serviceName, String operationName) throws RateLimitException { + String key = serviceName + ":" + operationName; + TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(capacity)); + + if (!bucket.tryConsume()) { + System.out.printf( + "[TokenBucket] Throttled %s.%s - No tokens available%n", serviceName, operationName); + throw new ThrottlingException(serviceName, operationName, 1000); + } else { + System.out.printf( + "[TokenBucket] Allowed %s.%s - Tokens remaining%n", serviceName, operationName); + } + } + + private void refillBuckets() { + buckets.forEach((k, b) -> b.refill(refillRate)); + } + + /** Inner class that represents the bucket holding tokens for each service-operation. */ + private static class TokenBucket { + private final int capacity; + private final AtomicInteger tokens; + + TokenBucket(int capacity) { + this.capacity = capacity; + this.tokens = new AtomicInteger(capacity); + } + + boolean tryConsume() { + while (true) { + int current = tokens.get(); + if (current <= 0) return false; + if (tokens.compareAndSet(current, current - 1)) return true; + } + } + + void refill(int amount) { + tokens.getAndUpdate(current -> Math.min(current + amount, capacity)); + } + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java new file mode 100644 index 000000000000..042d606490d0 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java @@ -0,0 +1,56 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AdaptiveRateLimiterTest { + @Test + void shouldDecreaseLimitWhenThrottled() throws Exception { + AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20); + + // Exceed initial limit + for (int i = 0; i < 11; i++) { + try { + limiter.check("test", "op"); + } catch (RateLimitException e) { + // Expected after 10 requests + } + } + + // Verify limit was reduced + assertThrows( + RateLimitException.class, + () -> { + for (int i = 0; i < 6; i++) { // New limit should be 5 (10/2) + limiter.check("test", "op"); + } + }); + } + + @Test + void shouldGraduallyIncreaseLimitWhenHealthy() throws Exception { + AdaptiveRateLimiter limiter = + new AdaptiveRateLimiter(4, 10); // Start from 4 → expect 2 → expect increase to 4 + + // Force throttling to reduce limit + for (int i = 0; i < 5; i++) { + try { + limiter.check("test", "op"); + } catch (RateLimitException e) { + // Expected to throttle and reduce limit + } + } + + // Wait for health check to increase limit + Thread.sleep(11000); // Wait slightly more than 10 seconds + + // Allow up to 4 requests again (limit should've increased to 4) + for (int i = 0; i < 4; i++) { + limiter.check("test", "op"); + } + + // 5th should throw exception again + assertThrows(RateLimitException.class, () -> limiter.check("test", "op")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java new file mode 100644 index 000000000000..11815a75de84 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java @@ -0,0 +1,59 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link App}. */ +class AppTest { + + private RateLimiter mockLimiter; + + @BeforeEach + void setUp() { + mockLimiter = mock(RateLimiter.class); + AppTestUtils.resetCounters(); // Ensures counters are clean before every test + } + + @Test + void shouldAllowRequest() { + AppTestUtils.invokeMakeRequest(1, mockLimiter, "s3", "GetObject"); + assertEquals(1, AppTestUtils.getSuccessfulRequests().get(), "Successful count should be 1"); + assertEquals(0, AppTestUtils.getThrottledRequests().get(), "Throttled count should be 0"); + assertEquals(0, AppTestUtils.getFailedRequests().get(), "Failed count should be 0"); + } + + @Test + void shouldHandleThrottlingException() throws Exception { + doThrow(new ThrottlingException("s3", "PutObject", 1000)).when(mockLimiter).check(any(), any()); + AppTestUtils.invokeMakeRequest(2, mockLimiter, "s3", "PutObject"); + assertEquals(0, AppTestUtils.getSuccessfulRequests().get()); + assertEquals(1, AppTestUtils.getThrottledRequests().get()); + assertEquals(0, AppTestUtils.getFailedRequests().get()); + } + + @Test + void shouldHandleServiceUnavailableException() throws Exception { + doThrow(new ServiceUnavailableException("lambda", 500)).when(mockLimiter).check(any(), any()); + AppTestUtils.invokeMakeRequest(3, mockLimiter, "lambda", "Invoke"); + assertEquals(0, AppTestUtils.getSuccessfulRequests().get()); + assertEquals(0, AppTestUtils.getThrottledRequests().get()); + assertEquals(1, AppTestUtils.getFailedRequests().get()); + } + + @Test + void shouldHandleGenericException() throws Exception { + doThrow(new RuntimeException("Unexpected")).when(mockLimiter).check(any(), any()); + AppTestUtils.invokeMakeRequest(4, mockLimiter, "dynamodb", "Query"); + assertEquals(0, AppTestUtils.getSuccessfulRequests().get()); + assertEquals(0, AppTestUtils.getThrottledRequests().get()); + assertEquals(1, AppTestUtils.getFailedRequests().get()); + } + + @Test + void shouldRunMainMethodWithoutException() { + assertDoesNotThrow(() -> App.main(new String[] {})); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java new file mode 100644 index 000000000000..9d652ec96c63 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java @@ -0,0 +1,27 @@ +package com.iluwatar.rate.limiting.pattern; + +import java.util.concurrent.atomic.AtomicInteger; + +public class AppTestUtils { + + public static void invokeMakeRequest( + int clientId, RateLimiter limiter, String service, String operation) { + App.makeRequest(clientId, limiter, service, operation); + } + + public static void resetCounters() { + App.resetCountersForTesting(); + } + + public static AtomicInteger getSuccessfulRequests() { + return App.successfulRequests; + } + + public static AtomicInteger getThrottledRequests() { + return App.throttledRequests; + } + + public static AtomicInteger getFailedRequests() { + return App.failedRequests; + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java new file mode 100644 index 000000000000..35a1294a8ad3 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java @@ -0,0 +1,69 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class ConcurrencyTests { + @Test + void tokenBucketShouldHandleConcurrentRequests() throws Exception { + int threadCount = 10; + int requestLimit = 5; + RateLimiter limiter = new TokenBucketRateLimiter(requestLimit, requestLimit); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + AtomicInteger successCount = new AtomicInteger(); + AtomicInteger failureCount = new AtomicInteger(); + + for (int i = 0; i < threadCount; i++) { + executor.submit( + () -> { + try { + limiter.check("test", "op"); + successCount.incrementAndGet(); + } catch (RateLimitException e) { + failureCount.incrementAndGet(); + } + latch.countDown(); + }); + } + + latch.await(); + assertEquals(requestLimit, successCount.get()); + assertEquals(threadCount - requestLimit, failureCount.get()); + } + + @Test + void adaptiveLimiterShouldAdjustUnderLoad() throws Exception { + AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20); + ExecutorService executor = Executors.newFixedThreadPool(20); + + // Flood with requests to trigger throttling + for (int i = 0; i < 30; i++) { + executor.submit( + () -> { + try { + limiter.check("test", "op"); + } catch (RateLimitException ignored) { + } + }); + } + + Thread.sleep(15000); // Wait for adjustment + + // Verify new limit is in effect + int allowed = 0; + for (int i = 0; i < 20; i++) { + try { + limiter.check("test", "op"); + allowed++; + } catch (RateLimitException ignored) { + } + } + + assertTrue(allowed > 5 && allowed < 15); // Should be between initial and max + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java new file mode 100644 index 000000000000..a7b037fbe73f --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java @@ -0,0 +1,28 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ExceptionTests { + @Test + void rateLimitExceptionShouldContainRetryInfo() { + RateLimitException exception = new RateLimitException("Test", 1000); + assertEquals(1000, exception.getRetryAfterMillis()); + assertEquals("Test", exception.getMessage()); + } + + @Test + void throttlingExceptionShouldContainServiceInfo() { + ThrottlingException exception = new ThrottlingException("dynamodb", "Query", 500); + assertEquals("dynamodb", exception.getServiceName()); + assertEquals("ThrottlingException", exception.getErrorCode()); + } + + @Test + void serviceUnavailableExceptionShouldContainRetryInfo() { + ServiceUnavailableException exception = new ServiceUnavailableException("s3", 2000); + assertEquals("s3", exception.getServiceName()); + assertEquals(2000, exception.getRetryAfterMillis()); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java new file mode 100644 index 000000000000..d0c3197289cd --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java @@ -0,0 +1,62 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class FindCustomerRequestTest implements RateLimitOperationTest { + + @Override + public RateLimitOperation createOperation(RateLimiter limiter) { + return new FindCustomerRequest("123", limiter); + } + + @Test + void shouldExecuteWhenUnderRateLimit() throws Exception { + RateLimiter limiter = new TokenBucketRateLimiter(10, 10); + RateLimitOperation request = createOperation(limiter); + + String result = request.execute(); + assertEquals("Customer-123", result); + } + + @Test + void shouldThrowWhenRateLimitExceeded() { + RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled + RateLimitOperation request = createOperation(limiter); + + assertThrows(RateLimitException.class, request::execute); + } + + @Test + void shouldReturnCorrectServiceAndOperationNames() { + RateLimiter limiter = new TokenBucketRateLimiter(10, 10); + FindCustomerRequest request = new FindCustomerRequest("123", limiter); + + assertEquals("CustomerService", request.getServiceName()); + assertEquals("FindCustomer", request.getOperationName()); + } + + // Reuse helper logic from the interface for coverage + @Test + void shouldExecuteUsingDefaultHelper() throws Exception { + RateLimiter limiter = new TokenBucketRateLimiter(5, 5); + shouldExecuteWhenUnderLimit(createOperation(limiter)); + } + + @Test + void shouldThrowServiceUnavailableOnInterruptedException() { + RateLimiter noOpLimiter = (service, operation) -> {}; // no throttling + + FindCustomerRequest request = + new FindCustomerRequest("999", noOpLimiter) { + @Override + public String execute() throws RateLimitException { + Thread.currentThread().interrupt(); // Simulate thread interruption + return super.execute(); // Should throw ServiceUnavailableException + } + }; + + assertThrows(ServiceUnavailableException.class, request::execute); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java new file mode 100644 index 000000000000..656185b7e68e --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java @@ -0,0 +1,40 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class FixedWindowRateLimiterTest extends RateLimiterTest { + @Override + protected RateLimiter createRateLimiter(int limit, long windowMillis) { + return new FixedWindowRateLimiter(limit, windowMillis / 1000); + } + + @Test + void shouldResetCounterAfterWindow() throws Exception { + FixedWindowRateLimiter limiter = + new FixedWindowRateLimiter(1, 1); // 1 request per 1 second window + + // First request should pass + limiter.check("test", "op"); + + // Second request in same window should be throttled + assertThrows(RateLimitException.class, () -> limiter.check("test", "op")); + + // Wait a bit more than 1 second to ensure window resets + TimeUnit.MILLISECONDS.sleep(1100); + + // After window reset, this should pass again + limiter.check("test", "op"); + } + + @Test + void shouldNotAllowMoreThanLimitInWindow() throws Exception { + FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(3, 1); + for (int i = 0; i < 3; i++) { + limiter.check("test", "op"); + } + assertThrows(RateLimitException.class, () -> limiter.check("test", "op")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java new file mode 100644 index 000000000000..d4922bdfc073 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java @@ -0,0 +1,22 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +interface RateLimitOperationTest { + + RateLimitOperation createOperation(RateLimiter limiter); + + @Test + default void shouldThrowWhenRateLimited() { + RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled + RateLimitOperation operation = createOperation(limiter); + assertThrows(RateLimitException.class, operation::execute); + } + + // ✅ No @Test here, just a helper method + default void shouldExecuteWhenUnderLimit(RateLimitOperation operation) throws Exception { + assertNotNull(operation.execute()); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java new file mode 100644 index 000000000000..7f1e6b4a2806 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java @@ -0,0 +1,25 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public abstract class RateLimiterTest { + protected abstract RateLimiter createRateLimiter(int limit, long windowMillis); + + @Test + void shouldAllowRequestsWithinLimit() throws Exception { + RateLimiter limiter = createRateLimiter(5, 1000); + for (int i = 0; i < 5; i++) { + limiter.check("test", "op"); + } + } + + @Test + void shouldThrowWhenLimitExceeded() throws Exception { + RateLimiter limiter = createRateLimiter(2, 1000); + limiter.check("test", "op"); + limiter.check("test", "op"); + assertThrows(RateLimitException.class, () -> limiter.check("test", "op")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java new file mode 100644 index 000000000000..5696299512fb --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java @@ -0,0 +1,39 @@ +package com.iluwatar.rate.limiting.pattern; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class TokenBucketRateLimiterTest extends RateLimiterTest { + @Override + protected RateLimiter createRateLimiter(int limit, long windowMillis) { + return new TokenBucketRateLimiter(limit, (int) (limit * 1000 / windowMillis)); + } + + @Test + void shouldAllowBurstRequests() throws Exception { + TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5); + for (int i = 0; i < 10; i++) { + limiter.check("test", "op"); + } + } + + @Test + void shouldRefillTokensAfterTime() throws Exception { + TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1); + limiter.check("test", "op"); + assertThrows(RateLimitException.class, () -> limiter.check("test", "op")); + + TimeUnit.SECONDS.sleep(1); + limiter.check("test", "op"); + } + + @Test + void shouldHandleMultipleServicesSeparately() throws Exception { + TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1); + limiter.check("service1", "op"); + limiter.check("service2", "op"); + assertThrows(RateLimitException.class, () -> limiter.check("service1", "op")); + } +} diff --git a/thread-pool-executor/README.md b/thread-pool-executor/README.md new file mode 100644 index 000000000000..fa1d5a2c8062 --- /dev/null +++ b/thread-pool-executor/README.md @@ -0,0 +1,200 @@ +--- +title: "Thread-Pool Executor Pattern in Java: Efficient Concurrent Task Management" +shortTitle: Thread-Pool Executor +description: "Learn the Thread-Pool Executor pattern in Java with practical examples, class +diagrams, and implementation details. Understand how to manage concurrent tasks efficiently, +improving resource utilization and application performance." +category: Concurrency +language: en +tag: + +- Performance +- Resource Management +- Concurrency +- Multithreading +- Scalability + +--- + +## Intent of Thread-Pool Executor Design Pattern + +The Thread-Pool Executor pattern maintains a pool of worker threads to execute tasks concurrently, +optimizing resource usage by reusing existing threads instead of creating new ones for each task. + +## Detailed Explanation of Thread-Pool Executor Pattern with Real-World Examples + +### Real-world example + +> Imagine a busy airport security checkpoint where instead of opening a new lane for each traveler, +> a fixed number of security lanes (threads) are open to process all passengers. Each security +> officer (thread) processes one passenger (task) at a time, and when finished, immediately calls the +> next passenger in line. During peak travel times, passengers wait in a queue, but the system is much +> more efficient than trying to open a new security lane for each individual traveler. The airport can +> handle fluctuating passenger traffic throughout the day with consistent staffing, optimizing both +> resource utilization and passenger throughput. + +### In plain words + +> Thread-Pool Executor keeps a set of reusable threads that process multiple tasks throughout their +> lifecycle, rather than creating a new thread for each task. + +### Wikipedia says + +> A thread pool is a software design pattern for achieving concurrency of execution in a computer +> program. Often also called a replicated workers or worker-crew model, a thread pool maintains +> multiple threads waiting for tasks to be allocated for concurrent execution by the supervising +> program. + +### Class diagram + +![Thread-pool-executor Class diagram](./etc/thread-pool-executor.urm.png) + +## Programmatic Example of Thread-Pool Executor Pattern in Java + +Imagine a hotel front desk. + +The number of employees (thread pool) is limited, but guests (tasks) keep arriving endlessly. + +The Thread-Pool Executor pattern efficiently handles a large number of requests by reusing a small +set of threads. + +```java +@Slf4j +public class HotelFrontDesk { + public static void main(String[] args) throws InterruptedException, ExecutionException { + // Hire 3 front desk employees (threads) + ExecutorService frontDesk = Executors.newFixedThreadPool(3); + + LOGGER.info("Hotel front desk operation started!"); + + // 7 regular guests checking in (Runnable) + for (int i = 1; i <= 7; i++) { + String guestName = "Guest-" + i; + frontDesk.submit(() -> { + String employeeName = Thread.currentThread().getName(); + LOGGER.info("{} is checking in {}...", employeeName, guestName); + try { + Thread.sleep(2000); // Simulate check-in time + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + LOGGER.info("{} has been successfully checked in!", guestName); + }); + } + + // 3 VIP guests checking in (Callable with result) + Callable vipGuest1 = createVipGuest("VIP-Guest-1"); + Callable vipGuest2 = createVipGuest("VIP-Guest-2"); + Callable vipGuest3 = createVipGuest("VIP-Guest-3"); + + Future vipResult1 = frontDesk.submit(vipGuest1); + Future vipResult2 = frontDesk.submit(vipGuest2); + Future vipResult3 = frontDesk.submit(vipGuest3); + + // Shutdown after submitting all tasks + frontDesk.shutdown(); + + if (frontDesk.awaitTermination(1, TimeUnit.HOURS)) { + // Print VIP guests' check-in results + LOGGER.info("VIP Check-in Results:"); + LOGGER.info(vipResult1.get()); + LOGGER.info(vipResult2.get()); + LOGGER.info(vipResult3.get()); + LOGGER.info("All guests have been successfully checked in. Front desk is now closed."); + } else { + LOGGER.info("Check-in timeout. Forcefully shutting down the front desk."); + } + } + + private static Callable createVipGuest(String vipGuestName) { + return () -> { + String employeeName = Thread.currentThread().getName(); + LOGGER.info("{} is checking in VIP guest {}...", employeeName, vipGuestName); + Thread.sleep(1000); // VIPs are faster to check in + return vipGuestName + " has been successfully checked in!"; + }; + } +} +``` + +Here's the console output: + +```markdown +Hotel front desk operation started! +pool-1-thread-3 is checking in Guest-3... +pool-1-thread-2 is checking in Guest-2... +pool-1-thread-1 is checking in Guest-1... +Guest-2 has been successfully checked in! +Guest-1 has been successfully checked in! +Guest-3 has been successfully checked in! +pool-1-thread-2 is checking in Guest-5... +pool-1-thread-3 is checking in Guest-4... +pool-1-thread-1 is checking in Guest-6... +Guest-5 has been successfully checked in! +pool-1-thread-2 is checking in Guest-7... +Guest-4 has been successfully checked in! +pool-1-thread-3 is checking in VIP guest VIP-Guest-1... +Guest-6 has been successfully checked in! +pool-1-thread-1 is checking in VIP guest VIP-Guest-2... +pool-1-thread-3 is checking in VIP guest VIP-Guest-3... +Guest-7 has been successfully checked in! +VIP Check-in Results: +VIP-Guest-1 has been successfully checked in! +VIP-Guest-2 has been successfully checked in! +VIP-Guest-3 has been successfully checked in! +All guests have been successfully checked in. Front desk is now closed. +``` + +**Note:** Since this example demonstrates asynchronous thread execution, **the actual output may vary between runs**. The order of execution and timing can differ due to thread scheduling, system load, and other factors that affect concurrent processing. The core behavior of the thread pool (limiting concurrent tasks to the number of threads and reusing threads) will remain consistent, but the exact sequence of log messages may change with each execution. + +## When to Use the Thread-Pool Executor Pattern in Java + +* When you need to limit the number of threads running simultaneously to avoid resource exhaustion +* For applications that process a large number of short-lived independent tasks +* To improve performance by reducing thread creation/destruction overhead +* When implementing server applications that handle multiple client requests concurrently +* To execute recurring tasks at fixed rates or with fixed delays + +## Thread-Pool Executor Pattern Java Tutorial + +* [Thread-Pool Executor Pattern Tutorial (Baeldung)](https://www.baeldung.com/thread-pool-java-and-guava) + +## Real-World Applications of Thread-Pool Executor Pattern in Java + +* Application servers like Tomcat and Jetty use thread pools to handle HTTP requests +* Database connection pools in JDBC implementations +* Background job processing frameworks like Spring Batch +* Task scheduling systems like Quartz Scheduler +* Java EE's Managed Executor Service for enterprise applications + +## Benefits and Trade-offs of Thread-Pool Executor Pattern + +### Benefits + +* Improves performance by reusing existing threads instead of creating new ones +* Provides better resource management by limiting the number of active threads +* Simplifies thread lifecycle management and cleanup +* Facilitates easy implementation of task prioritization and scheduling +* Enhances application stability by preventing resource exhaustion + +### Trade-offs + +* May lead to thread starvation if improperly configured (too few threads) +* Potential for resource underutilization if improperly sized (too many threads) +* Requires careful shutdown handling to prevent task loss or resource leaks + +## Related Java Design Patterns + +* [Master-Worker Pattern](https://java-design-patterns.com/patterns/master-worker/): Tasks between a + master and multiple workers. +* [Producer-Consumer Pattern](https://java-design-patterns.com/patterns/producer-consumer/): + Separates task production and task consumption, typically using a blocking queue. +* [Object Pool Pattern](https://java-design-patterns.com/patterns/object-pool/): Reuses a set of + objects (e.g., threads) instead of creating/destroying them repeatedly. + +## References and Credits + +* [Java Documentation for ThreadPoolExecutor](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html) +* [Java Concurrency in Practice](https://jcip.net/) by Brian Goetz +* [Effective Java](https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/) by Joshua + Bloch diff --git a/thread-pool-executor/etc/thread-pool-executor.urm.png b/thread-pool-executor/etc/thread-pool-executor.urm.png new file mode 100644 index 000000000000..d426343f8171 Binary files /dev/null and b/thread-pool-executor/etc/thread-pool-executor.urm.png differ diff --git a/thread-pool-executor/etc/thread-pool-executor.urm.puml b/thread-pool-executor/etc/thread-pool-executor.urm.puml new file mode 100644 index 000000000000..ca83f40c1b40 --- /dev/null +++ b/thread-pool-executor/etc/thread-pool-executor.urm.puml @@ -0,0 +1,66 @@ +@startuml + +interface Runnable { + +run(): void +} + +interface Callable { + +call(): T +} + +interface ExecutorService { + +submit(task: Runnable): Future + +submit(task: Callable): Future + +shutdown(): void + +awaitTermination(timeout: long, unit: TimeUnit): boolean +} + +class ThreadPoolExecutor { + -corePoolSize: int + -maximumPoolSize: int + -keepAliveTime: long + -workQueue: BlockingQueue + +execute(task: Runnable): void + +submit(task: Callable): Future +} + +class ThreadPoolManager { + -executorService: ExecutorService + +ThreadPoolManager(numThreads: int) + +submitTask(task: Runnable): void + +submitCallable(task: Callable): Future + +shutdown(): void + +awaitTermination(timeout: long, unit: TimeUnit): boolean +} + +class Task { + -id: int + -name: String + -processingTime: long + +Task(id: int, name: String, processingTime: long) + +run(): void + +call(): TaskResult +} + +class TaskResult { + -taskId: int + -taskName: String + -executionTime: long + +TaskResult(taskId: int, taskName: String, executionTime: long) +} + +class App { + +main(args: String[]): void + -executeRunnableTasks(poolManager: ThreadPoolManager): void + -executeCallableTasks(poolManager: ThreadPoolManager): void +} + +ExecutorService <|-- ThreadPoolExecutor : implements +Task ..|> Runnable : implements +Task ..|> Callable : implements +Task --> TaskResult : produces +ThreadPoolManager --> ExecutorService : wraps +App --> ThreadPoolManager : uses +App --> Task : creates + +@enduml \ No newline at end of file diff --git a/thread-pool-executor/pom.xml b/thread-pool-executor/pom.xml new file mode 100644 index 000000000000..f77cd92c67aa --- /dev/null +++ b/thread-pool-executor/pom.xml @@ -0,0 +1,83 @@ + + + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + thread-pool-executor + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.threadpoolexecutor.App + + + + + + + + + diff --git a/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/App.java b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/App.java new file mode 100644 index 000000000000..0c1292b89c3a --- /dev/null +++ b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/App.java @@ -0,0 +1,90 @@ +/* + * 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.threadpoolexecutor; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +/** + * The Thread-Pool Executor pattern demonstrates how a pool of worker threads can be used to execute + * tasks concurrently. This pattern is particularly useful in scenarios where you need to execute a + * large number of independent tasks and want to limit the number of threads used. + * + *

In this example, a hotel front desk with a fixed number of employees processes guest + * check-ins. Each employee is represented by a thread, and each check-in is a task. + * + *

Key benefits demonstrated: + * + *

    + *
  • Resource management - Limiting the number of concurrent threads + *
  • Efficiency - Reusing threads instead of creating new ones for each task + *
  • Responsiveness - Handling many requests with limited resources + *
+ */ +@Slf4j +public class App { + + /** + * Program main entry point. + * + * @param args program runtime arguments + */ + public static void main(String[] args) throws InterruptedException, ExecutionException { + + FrontDeskService frontDesk = new FrontDeskService(5); + LOGGER.info("Hotel front desk operation started!"); + + LOGGER.info("Processing 30 regular guest check-ins..."); + for (int i = 1; i <= 30; i++) { + frontDesk.submitGuestCheckIn(new GuestCheckInTask("Guest-" + i)); + Thread.sleep(100); + } + + LOGGER.info("Processing 3 VIP guest check-ins..."); + List> vipResults = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + Future result = + frontDesk.submitVipGuestCheckIn(new VipGuestCheckInTask("VIP-Guest-" + i)); + vipResults.add(result); + } + + frontDesk.shutdown(); + + if (frontDesk.awaitTermination(1, TimeUnit.HOURS)) { + LOGGER.info("VIP Check-in Results:"); + for (Future result : vipResults) { + LOGGER.info(result.get()); + } + LOGGER.info("All guests have been successfully checked in. Front desk is now closed."); + } else { + LOGGER.warn("Check-in timeout. Forcefully shutting down the front desk."); + } + } +} diff --git a/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/FrontDeskService.java b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/FrontDeskService.java new file mode 100644 index 000000000000..b80236ee5ecf --- /dev/null +++ b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/FrontDeskService.java @@ -0,0 +1,108 @@ +/* + * 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.threadpoolexecutor; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +/** + * FrontDeskService represents the hotel's front desk with a fixed number of employees. This class + * demonstrates the Thread-Pool Executor pattern using Java's ExecutorService. + */ +@Slf4j +public class FrontDeskService { + + private final ExecutorService executorService; + private final int numberOfEmployees; + + /** + * Creates a new front desk with the specified number of employees. + * + * @param numberOfEmployees the number of employees (threads) at the front desk + */ + public FrontDeskService(int numberOfEmployees) { + this.numberOfEmployees = numberOfEmployees; + this.executorService = Executors.newFixedThreadPool(numberOfEmployees); + LOGGER.info("Front desk initialized with {} employees.", numberOfEmployees); + } + + /** + * Submits a regular guest check-in task to an available employee. + * + * @param task the check-in task to submit + * @return a Future representing pending completion of the task + */ + public Future submitGuestCheckIn(Runnable task) { + LOGGER.debug("Submitting regular guest check-in task"); + return executorService.submit(task, null); + } + + /** + * Submits a VIP guest check-in task to an available employee. + * + * @param task the VIP check-in task to submit + * @param the type of the task's result + * @return a Future representing pending completion of the task + */ + public Future submitVipGuestCheckIn(Callable task) { + LOGGER.debug("Submitting VIP guest check-in task"); + return executorService.submit(task); + } + + /** + * Closes the front desk after all currently checked-in guests are processed. No new check-ins + * will be accepted. + */ + public void shutdown() { + LOGGER.info("Front desk is closing - no new guests will be accepted."); + executorService.shutdown(); + } + + /** + * Waits for all check-in processes to complete or until timeout. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument + * @return true if all tasks completed, false if timeout elapsed + * @throws InterruptedException if interrupted while waiting + */ + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + LOGGER.info("Waiting for all check-ins to complete (max wait: {} {})", timeout, unit); + return executorService.awaitTermination(timeout, unit); + } + + /** + * Gets the number of employees at the front desk. + * + * @return the number of employees + */ + public int getNumberOfEmployees() { + return numberOfEmployees; + } +} diff --git a/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/GuestCheckInTask.java b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/GuestCheckInTask.java new file mode 100644 index 000000000000..d8a33fdfc8d2 --- /dev/null +++ b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/GuestCheckInTask.java @@ -0,0 +1,52 @@ +/* + * 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.threadpoolexecutor; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * GuestCheckInTask represents a regular guest check-in process. Implements Runnable because it + * performs an action without returning a result. + */ +@Slf4j +@AllArgsConstructor +public class GuestCheckInTask implements Runnable { + + private final String guestName; + + @Override + public void run() { + String employeeName = Thread.currentThread().getName(); + LOGGER.info("{} is checking in {}...", employeeName, guestName); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Check-in for {} was interrupted", guestName); + } + LOGGER.info("{} has been successfully checked in!", guestName); + } +} diff --git a/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTask.java b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTask.java new file mode 100644 index 000000000000..3948c114f0d6 --- /dev/null +++ b/thread-pool-executor/src/main/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTask.java @@ -0,0 +1,52 @@ +/* + * 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.threadpoolexecutor; + +import java.util.concurrent.Callable; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * VipGuestCheckInTask represents a VIP guest check-in process. Implements Callable because it + * returns a result (check-in confirmation). + */ +@Slf4j +@AllArgsConstructor +public class VipGuestCheckInTask implements Callable { + + private final String vipGuestName; + + @Override + public String call() throws Exception { + String employeeName = Thread.currentThread().getName(); + LOGGER.info("{} is checking in VIP guest {}...", employeeName, vipGuestName); + + Thread.sleep(1000); + + String result = vipGuestName + " has been successfully checked in!"; + LOGGER.info("VIP check-in completed: {}", result); + return result; + } +} diff --git a/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/AppTest.java b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/AppTest.java new file mode 100644 index 000000000000..13e3a5beec3c --- /dev/null +++ b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/AppTest.java @@ -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.threadpoolexecutor; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void appStartsWithoutException() { + assertDoesNotThrow(() -> App.main(new String[] {})); + } +} diff --git a/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/FrontDeskServiceTest.java b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/FrontDeskServiceTest.java new file mode 100644 index 000000000000..8d0396bf0541 --- /dev/null +++ b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/FrontDeskServiceTest.java @@ -0,0 +1,248 @@ +/* + * 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.threadpoolexecutor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class FrontDeskServiceTest { + + /** + * Tests that the constructor correctly sets the number of employees (threads). This verifies the + * basic initialization of the thread pool. + */ + @Test + void testConstructorSetsCorrectNumberOfEmployees() { + int expectedEmployees = 3; + + FrontDeskService frontDesk = new FrontDeskService(expectedEmployees); + + assertEquals(expectedEmployees, frontDesk.getNumberOfEmployees()); + } + + /** + * Tests that the submitGuestCheckIn method returns a non-null Future object. This verifies the + * basic task submission functionality. + */ + @Test + void testSubmitGuestCheckInReturnsNonNullFuture() { + FrontDeskService frontDesk = new FrontDeskService(1); + + Runnable task = + () -> { + // Task that completes quickly + }; + + Future future = frontDesk.submitGuestCheckIn(task); + + assertNotNull(future); + } + + /** + * Tests that the submitVipGuestCheckIn method returns a non-null Future object. This verifies + * that tasks with return values can be submitted correctly. + */ + @Test + void testSubmitVipGuestCheckInReturnsNonNullFuture() { + FrontDeskService frontDesk = new FrontDeskService(1); + Callable task = () -> "VIP Check-in complete"; + + Future future = frontDesk.submitVipGuestCheckIn(task); + + assertNotNull(future); + } + + /** + * Tests that the shutdown and awaitTermination methods work correctly. This verifies the basic + * shutdown functionality of the thread pool. + */ + @Test + void testShutdownAndAwaitTermination() throws InterruptedException { + FrontDeskService frontDesk = new FrontDeskService(2); + CountDownLatch taskLatch = new CountDownLatch(1); + + Runnable task = taskLatch::countDown; + + frontDesk.submitGuestCheckIn(task); + frontDesk.shutdown(); + boolean terminated = frontDesk.awaitTermination(1, TimeUnit.SECONDS); + + assertTrue(terminated); + assertTrue(taskLatch.await(100, TimeUnit.MILLISECONDS)); + } + + /** + * Tests the thread pool's behavior under load with multiple tasks. This verifies that the thread + * pool limits concurrent execution to the number of threads, all submitted tasks are eventually + * completed, and threads are reused for multiple tasks. + */ + @Test + void testMultipleTasksUnderLoad() throws InterruptedException { + FrontDeskService frontDesk = new FrontDeskService(2); + int taskCount = 10; + CountDownLatch tasksCompletedLatch = new CountDownLatch(taskCount); + AtomicInteger concurrentTasks = new AtomicInteger(0); + AtomicInteger maxConcurrentTasks = new AtomicInteger(0); + + for (int i = 0; i < taskCount; i++) { + frontDesk.submitGuestCheckIn( + () -> { + try { + int current = concurrentTasks.incrementAndGet(); + maxConcurrentTasks.updateAndGet(max -> Math.max(max, current)); + + Thread.sleep(100); + + concurrentTasks.decrementAndGet(); + tasksCompletedLatch.countDown(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + boolean allTasksCompleted = tasksCompletedLatch.await(2, TimeUnit.SECONDS); + + frontDesk.shutdown(); + frontDesk.awaitTermination(1, TimeUnit.SECONDS); + + assertTrue(allTasksCompleted); + assertEquals(2, maxConcurrentTasks.get()); + assertEquals(0, concurrentTasks.get()); + } + + /** + * Tests proper shutdown behavior under load. This verifies that after shutdown no new tasks are + * accepted, all previously submitted tasks are completed, and the executor terminates properly + * after all tasks complete. + */ + @Test + void testProperShutdownUnderLoad() throws InterruptedException { + FrontDeskService frontDesk = new FrontDeskService(2); + int taskCount = 5; + CountDownLatch startedTasksLatch = new CountDownLatch(2); + CountDownLatch tasksCompletionLatch = new CountDownLatch(taskCount); + + for (int i = 0; i < taskCount; i++) { + frontDesk.submitGuestCheckIn( + () -> { + try { + startedTasksLatch.countDown(); + Thread.sleep(100); + tasksCompletionLatch.countDown(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + assertTrue(startedTasksLatch.await(1, TimeUnit.SECONDS)); + + frontDesk.shutdown(); + + assertThrows( + RejectedExecutionException.class, + () -> { + frontDesk.submitGuestCheckIn(() -> {}); + }); + + boolean allTasksCompleted = tasksCompletionLatch.await(2, TimeUnit.SECONDS); + + boolean terminated = frontDesk.awaitTermination(1, TimeUnit.SECONDS); + + assertTrue(allTasksCompleted); + assertTrue(terminated); + } + + /** + * Tests concurrent execution of different task types (regular and VIP). This verifies that both + * Runnable and Callable tasks can be processed concurrently, all tasks complete successfully, and + * Callable tasks return their results correctly. + */ + @Test + void testConcurrentRegularAndVipTasks() throws Exception { + FrontDeskService frontDesk = new FrontDeskService(3); + int regularTaskCount = 4; + int vipTaskCount = 3; + CountDownLatch allTasksLatch = new CountDownLatch(regularTaskCount + vipTaskCount); + + List> regularResults = new ArrayList<>(); + for (int i = 0; i < regularTaskCount; i++) { + Future result = + frontDesk.submitGuestCheckIn( + () -> { + try { + Thread.sleep(50); + allTasksLatch.countDown(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + regularResults.add(result); + } + + List> vipResults = new ArrayList<>(); + for (int i = 0; i < vipTaskCount; i++) { + final int guestNum = i; + Future result = + frontDesk.submitVipGuestCheckIn( + () -> { + Thread.sleep(25); + allTasksLatch.countDown(); + return "VIP-" + guestNum + " checked in"; + }); + vipResults.add(result); + } + + boolean allCompleted = allTasksLatch.await(2, TimeUnit.SECONDS); + + frontDesk.shutdown(); + frontDesk.awaitTermination(1, TimeUnit.SECONDS); + + assertTrue(allCompleted); + + for (Future result : regularResults) { + assertTrue(result.isDone()); + } + + for (int i = 0; i < vipTaskCount; i++) { + Future result = vipResults.get(i); + assertTrue(result.isDone()); + assertEquals("VIP-" + i + " checked in", result.get()); + } + } +} diff --git a/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/GuestCheckInTaskTest.java b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/GuestCheckInTaskTest.java new file mode 100644 index 000000000000..27bb75efd2db --- /dev/null +++ b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/GuestCheckInTaskTest.java @@ -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.threadpoolexecutor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class GuestCheckInTaskTest { + + /** + * Tests that the task executes in the current thread when called directly. This verifies that the + * thread name inside the task matches the calling thread. + */ + @Test + void testThreadNameInTask() { + String guestName = "TestGuest"; + AtomicReference capturedThreadName = new AtomicReference<>(); + + GuestCheckInTask task = + new GuestCheckInTask(guestName) { + @Override + public void run() { + capturedThreadName.set(Thread.currentThread().getName()); + } + }; + + task.run(); + + assertEquals(Thread.currentThread().getName(), capturedThreadName.get()); + } +} diff --git a/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTaskTest.java b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTaskTest.java new file mode 100644 index 000000000000..d76d90625c95 --- /dev/null +++ b/thread-pool-executor/src/test/java/com/iluwatar/threadpoolexecutor/VipGuestCheckInTaskTest.java @@ -0,0 +1,48 @@ +/* + * 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.threadpoolexecutor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class VipGuestCheckInTaskTest { + + /** + * Tests that the call method returns the expected result string. This verifies that the VIP + * check-in task correctly formats its result message. + */ + @Test + void testCallReturnsExpectedResult() throws Exception { + String vipGuestName = "TestVipGuest"; + VipGuestCheckInTask task = new VipGuestCheckInTask(vipGuestName); + + String result = task.call(); + + assertNotNull(result); + assertEquals("TestVipGuest has been successfully checked in!", result); + } +} diff --git a/view-helper/README.md b/view-helper/README.md new file mode 100644 index 000000000000..fbc93acba6fd --- /dev/null +++ b/view-helper/README.md @@ -0,0 +1,98 @@ +--- +title: "View Helper Pattern in Java: Simplifying Presentation Logic in MVC Applications" +shortTitle: View Helper +description: "Discover the View Helper Design Pattern in Java, a powerful technique for separating view-related logic from business logic in MVC-based web applications. This pattern enhances maintainability, reusability, and testability by delegating complex UI operations to reusable helper components. Ideal for developers aiming to keep views clean and focused on presentation." +category: Architectural +language: en +tag: + - Architecture + - Presentation + - Decoupling + - Code reuse +--- + +## Intent of View Helper Design Pattern +The View Helper Design Pattern separates presentation logic from the view by delegating complex UI tasks — like formatting or conditional display — to reusable helper components. This keeps views clean, promotes reuse, and aligns with the MVC principle of separating concerns between the view and the business logic. + +## Detailed Explanation with Real‑World Analogy +Real‑world example +> Imagine you're putting together a slideshow for a business presentation. You focus on arranging the slides, choosing the layout, and telling the story. But for tasks like resizing images, formatting charts, or converting data into visual form, you use tools or templates that automate those parts. +> +> In this analogy, you are the view, and the tools/templates are the helpers. They handle the heavy lifting behind the scenes so you can concentrate on the presentation. Similarly, in the View Helper pattern, the view delegates logic-heavy tasks—such as formatting or conditionally displaying data—to helper classes, keeping the view layer clean and presentation-focused. + +### In plain words +> The View Helper pattern is about keeping your UI code clean by moving any logic—like formatting, calculations, or decision-making—into separate helper classes. Instead of stuffing all the logic into the HTML or template files, you delegate it to helpers, so the view just focuses on showing the final result. + +### Sequence diagram +![Sequence diagram for View Helper](etc/view-helper-sequence-diagram.png) + +## Programmatic Example of View Helper Pattern in Java +Raw domain object +```java +public record Product(String name, BigDecimal price, LocalDate releaseDate, boolean discounted) {} +``` + +View model object for display +```java +public record ProductViewModel(String name, String price, String releasedDate) {} +``` + +View Helper formats data for display +```java +class ProductViewHelper implements ViewHelper { + + private static final String DISCOUNT_TAG = " (ON SALE)"; + + public ProductViewModel prepare(Product product) { + var displayName = product.name() + (product.discounted() ? DISCOUNT_TAG : ""); + var priceWithCurrency = NumberFormat.getCurrencyInstance(US).format(product.price()); + var formattedDate = product.releaseDate().format(ISO_DATE); + + return new ProductViewModel(displayName, priceWithCurrency, formattedDate); + } +} +``` + +View renders the formatted data +```java +public class ConsoleProductView implements View { + + @Override + public void render(ProductViewModel productViewModel) { + LOGGER.info(productViewModel.toString()); + } +} +``` +The `App.java` class simulates how the View Helper pattern works in a real application. It starts with a raw `Product` object containing unformatted data. +Then it: +1. Initializes a helper (`ProductViewHelper`) to format the product data for display. +1. Creates a view (`ConsoleProductView`) to render the formatted data. +1. Uses a controller (`ProductController`) to coordinate the flow between raw data, helper logic, and view rendering. + +Finally, it simulates a user request by passing the product to the controller, which prepares the view model using the helper and displays it using the view. This demonstrates a clean separation between data, presentation logic, and rendering. + +## When to Use the View Helper Pattern in Java +Use the View Helper pattern when your view layer starts containing logic such as formatting data, applying conditional styles, or transforming domain objects for display. It's especially useful in MVC architectures where you want to keep views clean and focused on rendering, while delegating non-trivial presentation logic to reusable helper classes. This pattern helps improve maintainability, testability, and separation of concerns in your application's UI layer. + +## Real‑World Uses of View Helper Pattern in Java +The View Helper pattern is widely used in web frameworks that follow the MVC architecture. In Java-based web applications (e.g., JSP, Spring MVC), it's common to use helper classes or utility methods to format dates, currencies, or apply conditional logic before rendering views. Technologies like Thymeleaf or JSF often rely on custom tags or expression helpers to achieve the same effect. + +## Benefits and Trade‑offs +Benefits: +* Separation of concerns: Keeps view templates clean by moving logic into dedicated helpers. +* Reusability: Common formatting and display logic can be reused across multiple views. +* Improved maintainability: Easier to update presentation logic without touching the view. +* Testability: Helpers can be unit tested independently from the UI layer. + +Trade‑offs: +* Added complexity: Introduces extra classes, which may feel unnecessary for very simple views. +* Overuse risk: Excessive use of helpers can spread logic thinly across many files, making it harder to trace behavior. +* Tight coupling risk: If not designed carefully, helpers can become tightly coupled to specific views or data formats. + +## Related Java Design Patterns +* [Model-View-Controller (MVC)](https://java-design-patterns.com/patterns/model-view-controller/): View Helper supports the View layer in MVC by offloading logic from the view to helper classes. +* [Template Method](https://java-design-patterns.com/patterns/template-method/): Can structure the steps of rendering or data transformation, with helpers handling specific formatting tasks. +* [Data Transfer Object (DTO)](https://java-design-patterns.com/patterns/data-transfer-object/): Often used alongside View Helper when transferring raw data that needs formatting before being displayed. + +## References & Credits +* [Core J2EE Patterns: View Helper.](https://www.oracle.com/java/technologies/viewhelper.html) diff --git a/view-helper/etc/view-helper-sequence-diagram.png b/view-helper/etc/view-helper-sequence-diagram.png new file mode 100644 index 000000000000..22e53c6b8afe Binary files /dev/null and b/view-helper/etc/view-helper-sequence-diagram.png differ diff --git a/view-helper/etc/view-helper-sequence-diagram.puml b/view-helper/etc/view-helper-sequence-diagram.puml new file mode 100644 index 000000000000..d5be27e51510 --- /dev/null +++ b/view-helper/etc/view-helper-sequence-diagram.puml @@ -0,0 +1,15 @@ +@startuml +actor Client +participant Controller +participant ViewHelper +participant View +participant Product +participant ProductViewModel + +Client -> Controller : handle(product) +Controller -> ViewHelper : prepare(product) +ViewHelper -> Product : access data +ViewHelper -> ProductViewModel : return formatted view model +Controller -> View : render(viewModel) +View -> Console : display output +@enduml \ No newline at end of file diff --git a/view-helper/etc/view-helper.png b/view-helper/etc/view-helper.png new file mode 100644 index 000000000000..9550b80d7acb Binary files /dev/null and b/view-helper/etc/view-helper.png differ diff --git a/view-helper/etc/view-helper.puml b/view-helper/etc/view-helper.puml new file mode 100644 index 000000000000..cbc4a57d31ff --- /dev/null +++ b/view-helper/etc/view-helper.puml @@ -0,0 +1,45 @@ +@startuml +package com.iluwatar.viewhelper { + interface View { + +render(T model) + } + + interface ViewHelper { + +prepare(S source): T + } + + class Product { + -name: String + -price: BigDecimal + -releaseDate: LocalDate + -discounted: boolean + } + + class ProductViewModel { + -name: String + -price: String + -releasedDate: String + } + + class ProductViewHelper { + +prepare(Product): ProductViewModel + } + + class ConsoleProductView { + +render(ProductViewModel) + } + + class ProductController { + -helper: ViewHelper + -view: View + +handle(Product) + } +} +Product --> ProductViewHelper +ProductViewHelper ..|> ViewHelper +ConsoleProductView ..|> View +ProductViewHelper --> ProductViewModel +ProductController --> ProductViewHelper +ProductController --> ConsoleProductView + +@enduml \ No newline at end of file diff --git a/view-helper/pom.xml b/view-helper/pom.xml new file mode 100644 index 000000000000..e2f3afca3a08 --- /dev/null +++ b/view-helper/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + view-helper + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.value.object.App + + + + + + + + + diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/App.java b/view-helper/src/main/java/com/iluwatar/viewhelper/App.java new file mode 100644 index 000000000000..e427bc8f5e9b --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/App.java @@ -0,0 +1,52 @@ +/* + * 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.viewhelper; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** The main application class that sets up and runs the View Helper pattern demo. */ +public class App { + /** + * The entry point of the application. + * + * @param args the command line arguments + */ + public static void main(String[] args) { + // Raw Product data (no formatting, no UI tags) + var product = + new Product( + "Design patterns book", new BigDecimal("18.90"), LocalDate.of(2025, 4, 19), true); + + // Create view, viewHelper and viewHelper + var helper = new ProductViewHelper(); + var view = new ConsoleProductView(); + var controller = new ProductController(helper, view); + + // Handle “request” + controller.handle(product); + } +} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/ConsoleProductView.java b/view-helper/src/main/java/com/iluwatar/viewhelper/ConsoleProductView.java new file mode 100644 index 000000000000..7f215a7514f6 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/ConsoleProductView.java @@ -0,0 +1,36 @@ +/* + * 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.viewhelper; + +import lombok.extern.slf4j.Slf4j; + +/** Renders {@link ProductViewModel} to the console. */ +@Slf4j +public class ConsoleProductView implements View { + @Override + public void render(ProductViewModel productViewModel) { + LOGGER.info(productViewModel.toString()); + } +} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/Product.java b/view-helper/src/main/java/com/iluwatar/viewhelper/Product.java new file mode 100644 index 000000000000..a0a75cf24188 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/Product.java @@ -0,0 +1,31 @@ +/* + * 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.viewhelper; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** Definition of product. */ +public record Product(String name, BigDecimal price, LocalDate releaseDate, boolean discounted) {} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/ProductController.java b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductController.java new file mode 100644 index 000000000000..836f34a33167 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductController.java @@ -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. + */ +package com.iluwatar.viewhelper; + +/** + * Controller delegates a {@link Product} to {@link ProductViewHelper} and then to {@link + * ConsoleProductView}. + */ +public class ProductController { + + private final ViewHelper viewHelper; + private final View view; + + public ProductController( + ViewHelper viewHelper, View view) { + this.viewHelper = viewHelper; + this.view = view; + } + + /** + * Passes the product to the helper for formatting and then forwards formatted product to the + * view. + */ + public void handle(Product product) { + view.render(viewHelper.prepare(product)); + } +} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewHelper.java b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewHelper.java new file mode 100644 index 000000000000..4c739e9f14a8 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewHelper.java @@ -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.viewhelper; + +import static java.time.format.DateTimeFormatter.ISO_DATE; +import static java.util.Locale.US; + +import java.text.NumberFormat; + +/** Formats a {@link Product} into a {@link ProductViewModel}. */ +public class ProductViewHelper implements ViewHelper { + + private static final String DISCOUNT_TAG = " ON SALE"; + + @Override + public ProductViewModel prepare(Product product) { + var displayName = product.name() + (product.discounted() ? DISCOUNT_TAG : ""); + var priceWithCurrency = NumberFormat.getCurrencyInstance(US).format(product.price()); + var formattedDate = product.releaseDate().format(ISO_DATE); + + return new ProductViewModel(displayName, priceWithCurrency, formattedDate); + } +} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewModel.java b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewModel.java new file mode 100644 index 000000000000..b133fc071da1 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/ProductViewModel.java @@ -0,0 +1,28 @@ +/* + * 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.viewhelper; + +/** Class defining formatted display data of a {@link Product}. */ +public record ProductViewModel(String name, String price, String releasedDate) {} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/View.java b/view-helper/src/main/java/com/iluwatar/viewhelper/View.java new file mode 100644 index 000000000000..7bc822166885 --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/View.java @@ -0,0 +1,30 @@ +/* + * 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.viewhelper; + +public interface View { + + void render(V data); +} diff --git a/view-helper/src/main/java/com/iluwatar/viewhelper/ViewHelper.java b/view-helper/src/main/java/com/iluwatar/viewhelper/ViewHelper.java new file mode 100644 index 000000000000..68756f46585d --- /dev/null +++ b/view-helper/src/main/java/com/iluwatar/viewhelper/ViewHelper.java @@ -0,0 +1,29 @@ +/* + * 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.viewhelper; + +public interface ViewHelper { + V prepare(M source); +} diff --git a/view-helper/src/test/java/com/iluwatar/viewhelper/AppTest.java b/view-helper/src/test/java/com/iluwatar/viewhelper/AppTest.java new file mode 100644 index 000000000000..bc4a31cc43a8 --- /dev/null +++ b/view-helper/src/test/java/com/iluwatar/viewhelper/AppTest.java @@ -0,0 +1,39 @@ +/* + * 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.viewhelper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +/** Application test */ +class AppTest { + + @Test + void shouldExecuteApplicationWithoutException() { + assertDoesNotThrow(() -> App.main(new String[] {})); + } +} diff --git a/view-helper/src/test/java/com/iluwatar/viewhelper/ProductViewHelperTest.java b/view-helper/src/test/java/com/iluwatar/viewhelper/ProductViewHelperTest.java new file mode 100644 index 000000000000..49cc48556f47 --- /dev/null +++ b/view-helper/src/test/java/com/iluwatar/viewhelper/ProductViewHelperTest.java @@ -0,0 +1,60 @@ +/* + * 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.viewhelper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ProductViewHelperTest { + + private ProductViewHelper helper; + + @BeforeEach + void setUp() { + helper = new ProductViewHelper(); + } + + @Test + void shouldFormatProductWithoutDiscount() { + var product = new Product("X", new BigDecimal("10.00"), LocalDate.of(2025, 1, 1), false); + ProductViewModel viewModel = helper.prepare(product); + + assertEquals("X", viewModel.name()); + assertEquals("$10.00", viewModel.price()); + assertEquals("2025-01-01", viewModel.releasedDate()); + } + + @Test + void shouldFormatProductWithDiscount() { + var product = new Product("X", new BigDecimal("10.00"), LocalDate.of(2025, 1, 1), true); + ProductViewModel viewModel = helper.prepare(product); + + assertEquals("X ON SALE", viewModel.name()); // locale follows JVM default + } +}