From f92e6829c3664ac569cdbef86d68db11eff8b36d Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Duc Date: Mon, 9 Feb 2026 02:11:47 +0700 Subject: [PATCH] feat: Implement DAO Factory pattern (#3282) * feat: implement dao factory * feat: done implement dao factory * test: Add unit test dao-factory * doc: add document for dao-factory * fix: h2 inmemory database to pass CI * fix: unit test pipeline * add license * fix SonarQube * add pom.xml * fix: throw exception + refactor --- dao-factory/README.md | 360 +++++++++++++ dao-factory/etc/dao-factory.png | Bin 0 -> 113583 bytes dao-factory/etc/dao-factory.puml | 74 +++ dao-factory/pom.xml | 82 +++ .../java/com/iluwatar/daofactory/App.java | 124 +++++ .../iluwatar/daofactory/CustomException.java | 36 ++ .../com/iluwatar/daofactory/Customer.java | 47 ++ .../com/iluwatar/daofactory/CustomerDAO.java | 85 +++ .../com/iluwatar/daofactory/DAOFactory.java | 45 ++ .../daofactory/DAOFactoryProvider.java | 62 +++ .../iluwatar/daofactory/DataSourceType.java | 32 ++ .../daofactory/FlatFileCustomerDAO.java | 175 ++++++ .../daofactory/FlatFileDataSourceFactory.java | 43 ++ .../iluwatar/daofactory/H2CustomerDAO.java | 179 +++++++ .../daofactory/H2DataSourceFactory.java | 48 ++ .../iluwatar/daofactory/MongoCustomerDAO.java | 106 ++++ .../daofactory/MongoDataSourceFactory.java | 51 ++ dao-factory/src/main/resources/logback.xml | 12 + .../java/com/iluwatar/daofactory/AppTest.java | 94 ++++ .../iluwatar/daofactory/DAOFactoryTest.java | 54 ++ .../daofactory/FlatFileCustomerDAOTest.java | 500 ++++++++++++++++++ .../daofactory/H2CustomerDAOTest.java | 300 +++++++++++ .../daofactory/MongoCustomerDAOTest.java | 163 ++++++ pom.xml | 1 + 24 files changed, 2673 insertions(+) create mode 100644 dao-factory/README.md create mode 100644 dao-factory/etc/dao-factory.png create mode 100644 dao-factory/etc/dao-factory.puml create mode 100644 dao-factory/pom.xml create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/App.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/CustomException.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/Customer.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/CustomerDAO.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactory.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/DAOFactoryProvider.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/DataSourceType.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileCustomerDAO.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/FlatFileDataSourceFactory.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/H2CustomerDAO.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/H2DataSourceFactory.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/MongoCustomerDAO.java create mode 100644 dao-factory/src/main/java/com/iluwatar/daofactory/MongoDataSourceFactory.java create mode 100644 dao-factory/src/main/resources/logback.xml create mode 100644 dao-factory/src/test/java/com/iluwatar/daofactory/AppTest.java create mode 100644 dao-factory/src/test/java/com/iluwatar/daofactory/DAOFactoryTest.java create mode 100644 dao-factory/src/test/java/com/iluwatar/daofactory/FlatFileCustomerDAOTest.java create mode 100644 dao-factory/src/test/java/com/iluwatar/daofactory/H2CustomerDAOTest.java create mode 100644 dao-factory/src/test/java/com/iluwatar/daofactory/MongoCustomerDAOTest.java diff --git a/dao-factory/README.md b/dao-factory/README.md new file mode 100644 index 000000000..c3ee3c5e8 --- /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 0000000000000000000000000000000000000000..d93547d799575ecf42efa9c27aac4e16c437723b GIT binary patch literal 113583 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 { + 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 000000000..2e771e736 --- /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 000000000..b80d3c5ac --- /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 000000000..9559e765c --- /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 000000000..95b675487 --- /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 000000000..34316b4c4 --- /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 000000000..e7d33186b --- /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 000000000..08585622d --- /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 000000000..da01d451f --- /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 000000000..8f1f1f144 --- /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 000000000..f42337670 --- /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 000000000..fe0274263 --- /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 000000000..dbb39dd98 --- /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 000000000..1870f61e8 --- /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 000000000..5a7b1f1b1 --- /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 000000000..f82341ebb --- /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 000000000..12efea42b --- /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 000000000..f8aaf1997 --- /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 000000000..470964f42 --- /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 000000000..ce7def36e --- /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 000000000..c56e72c30 --- /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/pom.xml b/pom.xml index a7fdcb706..2816ef0ca 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ converter curiously-recurring-template-pattern currying + dao-factory data-access-object data-bus data-locality