Compare commits

...

18 Commits

Author SHA1 Message Date
Ilkka Seppälä
e03aa88a58
docs: fixes for yaml frontmatter 2021-10-12 14:58:48 +03:00
Ilkka Seppälä
b00f93752f
docs: add missing caching pattern class diagram 2021-10-12 14:42:35 +03:00
Ilkka Seppälä
801be2520b
Merge branch 'master' of https://github.com/iluwatar/java-design-patterns 2021-10-12 13:59:21 +03:00
Ilkka Seppälä
ce4a6df5c5
Merge branch 'master' of https://github.com/iluwatar/java-design-patterns 2021-10-09 09:34:47 +03:00
Ilkka Seppälä
95699c5de3
Merge branch 'issue-1694' of https://github.com/VictorZZZZ/java-design-patterns 2021-10-08 20:17:49 +03:00
Victor Zalevskii
c459b92933 Fixes according PR comments. Mainly Readme edits. 2021-10-06 15:34:04 +03:00
Victor Zalevskii
2a01563d21 Comments to start Application with mongo. 2021-09-02 15:33:06 +03:00
Victor Zalevskii
45af6987c1 Provided missing tests 2021-09-02 15:30:07 +03:00
Victor Zalevskii
6adb27ca3d Added docker-compose for mongo db. MongoDb db work fixed. 2021-09-02 15:24:08 +03:00
Victor Zalevskii
c0e4bf3d1d Add test 2021-09-01 10:27:46 +03:00
Victor Zalevskii
50755b7215 Fix Bug with mongo connection. Used "Try with resources" 2021-08-30 09:13:08 +03:00
Victor Zalevskii
49039843e5 Fix issues in VALIDATE phase 2021-08-27 15:29:58 +03:00
Victor Zalevskii
dabe4d2022 Fix sonar issues 2021-08-27 13:27:50 +03:00
Victor Zalevskii
aff7ef8782 Fix last errors in checkstyle. 2021-08-23 12:37:24 +03:00
Victor Z
c150e5f38b Fix CacheStore errors from checkstyle plugin 107 left 2021-08-22 11:12:53 +03:00
Victor Z
3c213fcd69 Fix 40 errors from checkstyle plugin run. 139 left)) 2021-08-17 22:05:01 +03:00
Victor Zalevskii
a437f5b2eb Fix Logs 2021-08-17 16:53:44 +03:00
Victor Zalevskii
399ad53b09 Changed database implementation. Removed static objects. 2021-08-17 16:25:59 +03:00
29 changed files with 948 additions and 471 deletions

View File

@ -43,39 +43,29 @@ Wikipedia says:
**Programmatic Example** **Programmatic Example**
Let's first look at the data layer of our application. The interesting classes are `UserAccount` Let's first look at the data layer of our application. The interesting classes are `UserAccount`
which is a simple Java object containing the user account details, and `DbManager` which handles which is a simple Java object containing the user account details, and `DbManager` interface which handles
reading and writing of these objects to/from MongoDB database. reading and writing of these objects to/from database.
```java ```java
@Setter @Data
@Getter
@AllArgsConstructor @AllArgsConstructor
@ToString @ToString
@EqualsAndHashCode
public class UserAccount { public class UserAccount {
private String userId; private String userId;
private String userName; private String userName;
private String additionalInfo; private String additionalInfo;
} }
@Slf4j public interface DbManager {
public final class DbManager {
private static MongoClient mongoClient; void connect();
private static MongoDatabase db; void disconnect();
private DbManager() { /*...*/ } UserAccount readFromDb(String userId);
UserAccount writeToDb(UserAccount userAccount);
public static void createVirtualDb() { /*...*/ } UserAccount updateDb(UserAccount userAccount);
UserAccount upsertDb(UserAccount userAccount);
public static void connect() throws ParseException { /*...*/ }
public static UserAccount readFromDb(String userId) { /*...*/ }
public static void writeToDb(UserAccount userAccount) { /*...*/ }
public static void updateDb(UserAccount userAccount) { /*...*/ }
public static void upsertDb(UserAccount userAccount) { /*...*/ }
} }
``` ```
@ -171,30 +161,43 @@ strategies.
@Slf4j @Slf4j
public class CacheStore { public class CacheStore {
private static final int CAPACITY = 3;
private static LruCache cache; private static LruCache cache;
private final DbManager dbManager;
/* ... details omitted ... */ /* ... details omitted ... */
public static UserAccount readThrough(String userId) { public UserAccount readThrough(final String userId) {
if (cache.contains(userId)) { if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!"); LOGGER.info("# Found in Cache!");
return cache.get(userId); return cache.get(userId);
} }
LOGGER.info("# Cache Miss!"); LOGGER.info("# Not found in cache! Go to DB!!");
UserAccount userAccount = DbManager.readFromDb(userId); UserAccount userAccount = dbManager.readFromDb(userId);
cache.set(userId, userAccount); cache.set(userId, userAccount);
return userAccount; return userAccount;
} }
public static void writeThrough(UserAccount userAccount) { public void writeThrough(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) { if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount); dbManager.updateDb(userAccount);
} else { } else {
DbManager.writeToDb(userAccount); dbManager.writeToDb(userAccount);
} }
cache.set(userAccount.getUserId(), userAccount); cache.set(userAccount.getUserId(), userAccount);
} }
public void writeAround(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
dbManager.updateDb(userAccount);
// Cache data has been updated -- remove older
cache.invalidate(userAccount.getUserId());
// version from cache.
} else {
dbManager.writeToDb(userAccount);
}
}
public static void clearCache() { public static void clearCache() {
if (cache != null) { if (cache != null) {
cache.clear(); cache.clear();
@ -225,34 +228,39 @@ class.
public final class AppManager { public final class AppManager {
private static CachingPolicy cachingPolicy; private static CachingPolicy cachingPolicy;
private final DbManager dbManager;
private final CacheStore cacheStore;
private AppManager() { private AppManager() {
} }
public static void initDb(boolean useMongoDb) { /* ... */ } public void initDb() { /* ... */ }
public static void initCachingPolicy(CachingPolicy policy) { /* ... */ } public static void initCachingPolicy(CachingPolicy policy) { /* ... */ }
public static void initCacheCapacity(int capacity) { /* ... */ } public static void initCacheCapacity(int capacity) { /* ... */ }
public static UserAccount find(String userId) { public UserAccount find(final String userId) {
if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) { LOGGER.info("Trying to find {} in cache", userId);
return CacheStore.readThrough(userId); if (cachingPolicy == CachingPolicy.THROUGH
|| cachingPolicy == CachingPolicy.AROUND) {
return cacheStore.readThrough(userId);
} else if (cachingPolicy == CachingPolicy.BEHIND) { } else if (cachingPolicy == CachingPolicy.BEHIND) {
return CacheStore.readThroughWithWriteBackPolicy(userId); return cacheStore.readThroughWithWriteBackPolicy(userId);
} else if (cachingPolicy == CachingPolicy.ASIDE) { } else if (cachingPolicy == CachingPolicy.ASIDE) {
return findAside(userId); return findAside(userId);
} }
return null; return null;
} }
public static void save(UserAccount userAccount) { public void save(final UserAccount userAccount) {
LOGGER.info("Save record!");
if (cachingPolicy == CachingPolicy.THROUGH) { if (cachingPolicy == CachingPolicy.THROUGH) {
CacheStore.writeThrough(userAccount); cacheStore.writeThrough(userAccount);
} else if (cachingPolicy == CachingPolicy.AROUND) { } else if (cachingPolicy == CachingPolicy.AROUND) {
CacheStore.writeAround(userAccount); cacheStore.writeAround(userAccount);
} else if (cachingPolicy == CachingPolicy.BEHIND) { } else if (cachingPolicy == CachingPolicy.BEHIND) {
CacheStore.writeBehind(userAccount); cacheStore.writeBehind(userAccount);
} else if (cachingPolicy == CachingPolicy.ASIDE) { } else if (cachingPolicy == CachingPolicy.ASIDE) {
saveAside(userAccount); saveAside(userAccount);
} }
@ -272,24 +280,35 @@ Here is what we do in the main class of the application.
@Slf4j @Slf4j
public class App { public class App {
public static void main(String[] args) { public static void main(final String[] args) {
AppManager.initDb(false); boolean isDbMongo = isDbMongo(args);
AppManager.initCacheCapacity(3); if(isDbMongo){
var app = new App(); LOGGER.info("Using the Mongo database engine to run the application.");
} else {
LOGGER.info("Using the 'in Memory' database to run the application.");
}
App app = new App(isDbMongo);
app.useReadAndWriteThroughStrategy(); app.useReadAndWriteThroughStrategy();
String splitLine = "==============================================";
LOGGER.info(splitLine);
app.useReadThroughAndWriteAroundStrategy(); app.useReadThroughAndWriteAroundStrategy();
LOGGER.info(splitLine);
app.useReadThroughAndWriteBehindStrategy(); app.useReadThroughAndWriteBehindStrategy();
LOGGER.info(splitLine);
app.useCacheAsideStategy(); app.useCacheAsideStategy();
LOGGER.info(splitLine);
} }
public void useReadAndWriteThroughStrategy() { public void useReadAndWriteThroughStrategy() {
LOGGER.info("# CachingPolicy.THROUGH"); LOGGER.info("# CachingPolicy.THROUGH");
AppManager.initCachingPolicy(CachingPolicy.THROUGH); appManager.initCachingPolicy(CachingPolicy.THROUGH);
var userAccount1 = new UserAccount("001", "John", "He is a boy."); var userAccount1 = new UserAccount("001", "John", "He is a boy.");
AppManager.save(userAccount1);
LOGGER.info(AppManager.printCacheContent()); appManager.save(userAccount1);
AppManager.find("001"); LOGGER.info(appManager.printCacheContent());
AppManager.find("001"); appManager.find("001");
appManager.find("001");
} }
public void useReadThroughAndWriteAroundStrategy() { /* ... */ } public void useReadThroughAndWriteAroundStrategy() { /* ... */ }
@ -300,16 +319,6 @@ public class App {
} }
``` ```
Finally, here is some of the console output from the program.
```
12:32:53.845 [main] INFO com.iluwatar.caching.App - # CachingPolicy.THROUGH
12:32:53.900 [main] INFO com.iluwatar.caching.App -
--CACHE CONTENT--
UserAccount(userId=001, userName=John, additionalInfo=He is a boy.)
----
```
## Class diagram ## Class diagram
![alt text](./etc/caching.png "Caching") ![alt text](./etc/caching.png "Caching")

View File

@ -0,0 +1,11 @@
version: '3.7'
services:
mongodb_container:
image: mongo:latest
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: rootpassword
ports:
- 27017:27017
volumes:
- ./mongo-data/:/data/db

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

@ -39,19 +39,21 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mongodb</groupId> <groupId>org.mockito</groupId>
<artifactId>mongodb-driver</artifactId> <artifactId>mockito-junit-jupiter</artifactId>
<version>3.12.1</version> <version>3.12.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mongodb</groupId> <groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId> <artifactId>mongo-java-driver</artifactId>
<version>3.0.4</version> <version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
<version>3.0.4</version>
</dependency> </dependency>
</dependencies> </dependencies>
<!-- <!--

View File

@ -1,58 +1,62 @@
/*
* The MIT License
* Copyright © 2014-2021 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.caching; package com.iluwatar.caching;
import com.iluwatar.caching.database.DbManager;
import com.iluwatar.caching.database.DbManagerFactory;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* The Caching pattern describes how to avoid expensive re-acquisition of resources by not releasing * The Caching pattern describes how to avoid expensive re-acquisition of
* the resources immediately after their use. The resources retain their identity, are kept in some * resources by not releasing the resources immediately after their use.
* fast-access storage, and are re-used to avoid having to acquire them again. There are four main * The resources retain their identity, are kept in some fast-access storage,
* caching strategies/techniques in this pattern; each with their own pros and cons. They are; * and are re-used to avoid having to acquire them again. There are four main
* <code>write-through</code> which writes data to the cache and DB in a single transaction, * caching strategies/techniques in this pattern; each with their own pros and
* <code>write-around</code> which writes data immediately into the DB instead of the cache, * cons. They are <code>write-through</code> which writes data to the cache and
* <code>write-behind</code> which writes data into the cache initially whilst the data is only * DB in a single transaction, <code>write-around</code> which writes data
* written into the DB when the cache is full, and <code>cache-aside</code> which pushes the * immediately into the DB instead of the cache, <code>write-behind</code>
* responsibility of keeping the data synchronized in both data sources to the application itself. * which writes data into the cache initially whilst the data is only
* The <code>read-through</code> strategy is also included in the mentioned four strategies -- * written into the DB when the cache is full, and <code>cache-aside</code>
* returns data from the cache to the caller <b>if</b> it exists <b>else</b> queries from DB and * which pushes the responsibility of keeping the data synchronized in both
* stores it into the cache for future use. These strategies determine when the data in the cache * data sources to the application itself. The <code>read-through</code>
* should be written back to the backing store (i.e. Database) and help keep both data sources * strategy is also included in the mentioned four strategies --
* synchronized/up-to-date. This pattern can improve performance and also helps to maintain * returns data from the cache to the caller <b>if</b> it exists <b>else</b>
* consistency between data held in the cache and the data in the underlying data store. * queries from DB and stores it into the cache for future use. These strategies
* determine when the data in the cache should be written back to the backing
* store (i.e. Database) and help keep both data sources
* synchronized/up-to-date. This pattern can improve performance and also helps
* to maintainconsistency between data held in the cache and the data in
* the underlying data store.
* *
* <p>In this example, the user account ({@link UserAccount}) entity is used as the underlying * <p>In this example, the user account ({@link UserAccount}) entity is used
* application data. The cache itself is implemented as an internal (Java) data structure. It adopts * as the underlying application data. The cache itself is implemented as an
* a Least-Recently-Used (LRU) strategy for evicting data from itself when its full. The four * internal (Java) data structure. It adopts a Least-Recently-Used (LRU)
* strategies are individually tested. The testing of the cache is restricted towards saving and * strategy for evicting data from itself when its full. The four
* querying of user accounts from the underlying data store ( {@link DbManager}). The main class ( * strategies are individually tested. The testing of the cache is restricted
* {@link App} is not aware of the underlying mechanics of the application (i.e. save and query) and * towards saving and querying of user accounts from the
* whether the data is coming from the cache or the DB (i.e. separation of concern). The AppManager * underlying data store( {@link DbManager}). The main class ( {@link App}
* ({@link AppManager}) handles the transaction of data to-and-from the underlying data store * is not aware of the underlying mechanics of the application
* (depending on the preferred caching policy/strategy). * (i.e. save and query) and whether the data is coming from the cache or the
* DB (i.e. separation of concern). The AppManager ({@link AppManager}) handles
* the transaction of data to-and-from the underlying data store (depending on
* the preferred caching policy/strategy).
* <p> * <p>
* <i>{@literal App --> AppManager --> CacheStore/LRUCache/CachingPolicy --> DBManager} </i> * <i>{@literal App --> AppManager --> CacheStore/LRUCache/CachingPolicy -->
* DBManager} </i>
* </p>
*
* <p>
* There are 2 ways to launch the application.
* - to use "in Memory" database.
* - to use the MongoDb as a database
*
* To run the application with "in Memory" database, just launch it without parameters
* Example: 'java -jar app.jar'
*
* To run the application with MongoDb you need to be installed the MongoDb
* in your system, or to launch it in the docker container.
* You may launch docker container from the root of current module with command:
* 'docker-compose up'
* Then you can start the application with parameter --mongo
* Example: 'java -jar app.jar --mongo'
* </p> * </p>
* *
* @see CacheStore * @see CacheStore
@ -61,23 +65,67 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@Slf4j @Slf4j
public class App { public class App {
/**
* Constant parameter name to use mongoDB.
*/
private static final String USE_MONGO_DB = "--mongo";
/**
* Application manager.
*/
private final AppManager appManager;
/**
* Constructor of current App.
*
* @param isMongo boolean
*/
public App(final boolean isMongo) {
DbManager dbManager = DbManagerFactory.initDb(isMongo);
appManager = new AppManager(dbManager);
appManager.initDb();
}
/** /**
* Program entry point. * Program entry point.
* *
* @param args command line args * @param args command line args
*/ */
public static void main(String[] args) { public static void main(final String[] args) {
AppManager.initDb(false); // VirtualDB (instead of MongoDB) was used in running the JUnit tests // VirtualDB (instead of MongoDB) was used in running the JUnit tests
// and the App class to avoid Maven compilation errors. Set flag to // and the App class to avoid Maven compilation errors. Set flag to
// true to run the tests with MongoDB (provided that MongoDB is // true to run the tests with MongoDB (provided that MongoDB is
// installed and socket connection is open). // installed and socket connection is open).
AppManager.initCacheCapacity(3); boolean isDbMongo = isDbMongo(args);
var app = new App(); if (isDbMongo) {
LOGGER.info("Using the Mongo database engine to run the application.");
} else {
LOGGER.info("Using the 'in Memory' database to run the application.");
}
App app = new App(isDbMongo);
app.useReadAndWriteThroughStrategy(); app.useReadAndWriteThroughStrategy();
String splitLine = "==============================================";
LOGGER.info(splitLine);
app.useReadThroughAndWriteAroundStrategy(); app.useReadThroughAndWriteAroundStrategy();
LOGGER.info(splitLine);
app.useReadThroughAndWriteBehindStrategy(); app.useReadThroughAndWriteBehindStrategy();
LOGGER.info(splitLine);
app.useCacheAsideStategy(); app.useCacheAsideStategy();
LOGGER.info(splitLine);
}
/**
* Check the input parameters. if
*
* @param args input params
* @return true if there is "--mongo" parameter in arguments
*/
private static boolean isDbMongo(final String[] args) {
for (String arg : args) {
if (arg.equals(USE_MONGO_DB)) {
return true;
}
}
return false;
} }
/** /**
@ -85,14 +133,14 @@ public class App {
*/ */
public void useReadAndWriteThroughStrategy() { public void useReadAndWriteThroughStrategy() {
LOGGER.info("# CachingPolicy.THROUGH"); LOGGER.info("# CachingPolicy.THROUGH");
AppManager.initCachingPolicy(CachingPolicy.THROUGH); appManager.initCachingPolicy(CachingPolicy.THROUGH);
var userAccount1 = new UserAccount("001", "John", "He is a boy."); var userAccount1 = new UserAccount("001", "John", "He is a boy.");
AppManager.save(userAccount1); appManager.save(userAccount1);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("001"); appManager.find("001");
AppManager.find("001"); appManager.find("001");
} }
/** /**
@ -100,21 +148,21 @@ public class App {
*/ */
public void useReadThroughAndWriteAroundStrategy() { public void useReadThroughAndWriteAroundStrategy() {
LOGGER.info("# CachingPolicy.AROUND"); LOGGER.info("# CachingPolicy.AROUND");
AppManager.initCachingPolicy(CachingPolicy.AROUND); appManager.initCachingPolicy(CachingPolicy.AROUND);
var userAccount2 = new UserAccount("002", "Jane", "She is a girl."); var userAccount2 = new UserAccount("002", "Jane", "She is a girl.");
AppManager.save(userAccount2); appManager.save(userAccount2);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("002"); appManager.find("002");
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
userAccount2 = AppManager.find("002"); userAccount2 = appManager.find("002");
userAccount2.setUserName("Jane G."); userAccount2.setUserName("Jane G.");
AppManager.save(userAccount2); appManager.save(userAccount2);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("002"); appManager.find("002");
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("002"); appManager.find("002");
} }
/** /**
@ -122,23 +170,31 @@ public class App {
*/ */
public void useReadThroughAndWriteBehindStrategy() { public void useReadThroughAndWriteBehindStrategy() {
LOGGER.info("# CachingPolicy.BEHIND"); LOGGER.info("# CachingPolicy.BEHIND");
AppManager.initCachingPolicy(CachingPolicy.BEHIND); appManager.initCachingPolicy(CachingPolicy.BEHIND);
var userAccount3 = new UserAccount("003", "Adam", "He likes food."); var userAccount3 = new UserAccount("003",
var userAccount4 = new UserAccount("004", "Rita", "She hates cats."); "Adam",
var userAccount5 = new UserAccount("005", "Isaac", "He is allergic to mustard."); "He likes food.");
var userAccount4 = new UserAccount("004",
"Rita",
"She hates cats.");
var userAccount5 = new UserAccount("005",
"Isaac",
"He is allergic to mustard.");
AppManager.save(userAccount3); appManager.save(userAccount3);
AppManager.save(userAccount4); appManager.save(userAccount4);
AppManager.save(userAccount5); appManager.save(userAccount5);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("003"); appManager.find("003");
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
UserAccount userAccount6 = new UserAccount("006", "Yasha", "She is an only child."); UserAccount userAccount6 = new UserAccount("006",
AppManager.save(userAccount6); "Yasha",
LOGGER.info(AppManager.printCacheContent()); "She is an only child.");
AppManager.find("004"); appManager.save(userAccount6);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
appManager.find("004");
LOGGER.info(appManager.printCacheContent());
} }
/** /**
@ -146,20 +202,26 @@ public class App {
*/ */
public void useCacheAsideStategy() { public void useCacheAsideStategy() {
LOGGER.info("# CachingPolicy.ASIDE"); LOGGER.info("# CachingPolicy.ASIDE");
AppManager.initCachingPolicy(CachingPolicy.ASIDE); appManager.initCachingPolicy(CachingPolicy.ASIDE);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
var userAccount3 = new UserAccount("003", "Adam", "He likes food."); var userAccount3 = new UserAccount("003",
var userAccount4 = new UserAccount("004", "Rita", "She hates cats."); "Adam",
var userAccount5 = new UserAccount("005", "Isaac", "He is allergic to mustard."); "He likes food.");
AppManager.save(userAccount3); var userAccount4 = new UserAccount("004",
AppManager.save(userAccount4); "Rita",
AppManager.save(userAccount5); "She hates cats.");
var userAccount5 = new UserAccount("005",
"Isaac",
"He is allergic to mustard.");
appManager.save(userAccount3);
appManager.save(userAccount4);
appManager.save(userAccount5);
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("003"); appManager.find("003");
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
AppManager.find("004"); appManager.find("004");
LOGGER.info(AppManager.printCacheContent()); LOGGER.info(appManager.printCacheContent());
} }
} }

View File

@ -23,65 +23,80 @@
package com.iluwatar.caching; package com.iluwatar.caching;
import java.text.ParseException; import com.iluwatar.caching.database.DbManager;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* AppManager helps to bridge the gap in communication between the main class and the application's * AppManager helps to bridge the gap in communication between the main class
* back-end. DB connection is initialized through this class. The chosen caching strategy/policy is * and the application's back-end. DB connection is initialized through this
* also initialized here. Before the cache can be used, the size of the cache has to be set. * class. The chosen caching strategy/policy is also initialized here.
* Depending on the chosen caching policy, AppManager will call the appropriate function in the * Before the cache can be used, the size of the cache has to be set.
* CacheStore class. * Depending on the chosen caching policy, AppManager will call the
* appropriate function in the CacheStore class.
*/ */
@Slf4j @Slf4j
public final class AppManager { public class AppManager {
/**
* Caching Policy.
*/
private CachingPolicy cachingPolicy;
/**
* Database Manager.
*/
private final DbManager dbManager;
/**
* Cache Store.
*/
private final CacheStore cacheStore;
private static CachingPolicy cachingPolicy; /**
* Constructor.
private AppManager() { *
* @param newDbManager database manager
*/
public AppManager(final DbManager newDbManager) {
this.dbManager = newDbManager;
this.cacheStore = new CacheStore(newDbManager);
} }
/** /**
* Developer/Tester is able to choose whether the application should use MongoDB as its underlying * Developer/Tester is able to choose whether the application should use
* data storage or a simple Java data structure to (temporarily) store the data/objects during * MongoDB as its underlying data storage or a simple Java data structure
* runtime. * to (temporarily) store the data/objects during runtime.
*/ */
public static void initDb(boolean useMongoDb) { public void initDb() {
if (useMongoDb) { dbManager.connect();
try {
DbManager.connect();
} catch (ParseException e) {
LOGGER.error("Error connecting to MongoDB", e);
}
} else {
DbManager.createVirtualDb();
}
} }
/** /**
* Initialize caching policy. * Initialize caching policy.
*
* @param policy is a {@link CachingPolicy}
*/ */
public static void initCachingPolicy(CachingPolicy policy) { public void initCachingPolicy(final CachingPolicy policy) {
cachingPolicy = policy; cachingPolicy = policy;
if (cachingPolicy == CachingPolicy.BEHIND) { if (cachingPolicy == CachingPolicy.BEHIND) {
Runtime.getRuntime().addShutdownHook(new Thread(CacheStore::flushCache)); Runtime.getRuntime().addShutdownHook(new Thread(cacheStore::flushCache));
} }
CacheStore.clearCache(); cacheStore.clearCache();
}
public static void initCacheCapacity(int capacity) {
CacheStore.initCapacity(capacity);
} }
/** /**
* Find user account. * Find user account.
*
* @param userId String
* @return {@link UserAccount}
*/ */
public static UserAccount find(String userId) { public UserAccount find(final String userId) {
if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) { LOGGER.info("Trying to find {} in cache", userId);
return CacheStore.readThrough(userId); if (cachingPolicy == CachingPolicy.THROUGH
|| cachingPolicy == CachingPolicy.AROUND) {
return cacheStore.readThrough(userId);
} else if (cachingPolicy == CachingPolicy.BEHIND) { } else if (cachingPolicy == CachingPolicy.BEHIND) {
return CacheStore.readThroughWithWriteBackPolicy(userId); return cacheStore.readThroughWithWriteBackPolicy(userId);
} else if (cachingPolicy == CachingPolicy.ASIDE) { } else if (cachingPolicy == CachingPolicy.ASIDE) {
return findAside(userId); return findAside(userId);
} }
@ -90,41 +105,55 @@ public final class AppManager {
/** /**
* Save user account. * Save user account.
*
* @param userAccount {@link UserAccount}
*/ */
public static void save(UserAccount userAccount) { public void save(final UserAccount userAccount) {
LOGGER.info("Save record!");
if (cachingPolicy == CachingPolicy.THROUGH) { if (cachingPolicy == CachingPolicy.THROUGH) {
CacheStore.writeThrough(userAccount); cacheStore.writeThrough(userAccount);
} else if (cachingPolicy == CachingPolicy.AROUND) { } else if (cachingPolicy == CachingPolicy.AROUND) {
CacheStore.writeAround(userAccount); cacheStore.writeAround(userAccount);
} else if (cachingPolicy == CachingPolicy.BEHIND) { } else if (cachingPolicy == CachingPolicy.BEHIND) {
CacheStore.writeBehind(userAccount); cacheStore.writeBehind(userAccount);
} else if (cachingPolicy == CachingPolicy.ASIDE) { } else if (cachingPolicy == CachingPolicy.ASIDE) {
saveAside(userAccount); saveAside(userAccount);
} }
} }
public static String printCacheContent() { /**
return CacheStore.print(); * Returns String.
*
* @return String
*/
public String printCacheContent() {
return cacheStore.print();
} }
/** /**
* Cache-Aside save user account helper. * Cache-Aside save user account helper.
*
* @param userAccount {@link UserAccount}
*/ */
private static void saveAside(UserAccount userAccount) { private void saveAside(final UserAccount userAccount) {
DbManager.updateDb(userAccount); dbManager.updateDb(userAccount);
CacheStore.invalidate(userAccount.getUserId()); cacheStore.invalidate(userAccount.getUserId());
} }
/** /**
* Cache-Aside find user account helper. * Cache-Aside find user account helper.
*
* @param userId String
* @return {@link UserAccount}
*/ */
private static UserAccount findAside(String userId) { private UserAccount findAside(final String userId) {
return Optional.ofNullable(CacheStore.get(userId)) return Optional.ofNullable(cacheStore.get(userId))
.or(() -> { .or(() -> {
Optional<UserAccount> userAccount = Optional.ofNullable(DbManager.readFromDb(userId)); Optional<UserAccount> userAccount =
userAccount.ifPresent(account -> CacheStore.set(userId, account)); Optional.ofNullable(dbManager.readFromDb(userId));
return userAccount; userAccount.ifPresent(account -> cacheStore.set(userId, account));
}) return userAccount;
.orElse(null); })
.orElse(null);
} }
} }

View File

@ -23,9 +23,11 @@
package com.iluwatar.caching; package com.iluwatar.caching;
import com.iluwatar.caching.database.DbManager;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -33,16 +35,34 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@Slf4j @Slf4j
public class CacheStore { public class CacheStore {
/**
* Cache capacity.
*/
private static final int CAPACITY = 3;
private static LruCache cache; /**
* Lru cache see {@link LruCache}.
*/
private LruCache cache;
/**
* DbManager.
*/
private final DbManager dbManager;
private CacheStore() { /**
* Cache Store.
* @param dataBaseManager {@link DbManager}
*/
public CacheStore(final DbManager dataBaseManager) {
this.dbManager = dataBaseManager;
initCapacity(CAPACITY);
} }
/** /**
* Init cache capacity. * Init cache capacity.
* @param capacity int
*/ */
public static void initCapacity(int capacity) { public void initCapacity(final int capacity) {
if (cache == null) { if (cache == null) {
cache = new LruCache(capacity); cache = new LruCache(capacity);
} else { } else {
@ -52,57 +72,64 @@ public class CacheStore {
/** /**
* Get user account using read-through cache. * Get user account using read-through cache.
* @param userId {@link String}
* @return {@link UserAccount}
*/ */
public static UserAccount readThrough(String userId) { public UserAccount readThrough(final String userId) {
if (cache.contains(userId)) { if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!"); LOGGER.info("# Found in Cache!");
return cache.get(userId); return cache.get(userId);
} }
LOGGER.info("# Cache Miss!"); LOGGER.info("# Not found in cache! Go to DB!!");
UserAccount userAccount = DbManager.readFromDb(userId); UserAccount userAccount = dbManager.readFromDb(userId);
cache.set(userId, userAccount); cache.set(userId, userAccount);
return userAccount; return userAccount;
} }
/** /**
* Get user account using write-through cache. * Get user account using write-through cache.
* @param userAccount {@link UserAccount}
*/ */
public static void writeThrough(UserAccount userAccount) { public void writeThrough(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) { if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount); dbManager.updateDb(userAccount);
} else { } else {
DbManager.writeToDb(userAccount); dbManager.writeToDb(userAccount);
} }
cache.set(userAccount.getUserId(), userAccount); cache.set(userAccount.getUserId(), userAccount);
} }
/** /**
* Get user account using write-around cache. * Get user account using write-around cache.
* @param userAccount {@link UserAccount}
*/ */
public static void writeAround(UserAccount userAccount) { public void writeAround(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) { if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount); dbManager.updateDb(userAccount);
cache.invalidate(userAccount.getUserId()); // Cache data has been updated -- remove older // Cache data has been updated -- remove older
cache.invalidate(userAccount.getUserId());
// version from cache. // version from cache.
} else { } else {
DbManager.writeToDb(userAccount); dbManager.writeToDb(userAccount);
} }
} }
/** /**
* Get user account using read-through cache with write-back policy. * Get user account using read-through cache with write-back policy.
* @param userId {@link String}
* @return {@link UserAccount}
*/ */
public static UserAccount readThroughWithWriteBackPolicy(String userId) { public UserAccount readThroughWithWriteBackPolicy(final String userId) {
if (cache.contains(userId)) { if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!"); LOGGER.info("# Found in cache!");
return cache.get(userId); return cache.get(userId);
} }
LOGGER.info("# Cache Miss!"); LOGGER.info("# Not found in Cache!");
UserAccount userAccount = DbManager.readFromDb(userId); UserAccount userAccount = dbManager.readFromDb(userId);
if (cache.isFull()) { if (cache.isFull()) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB..."); LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData(); UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb); dbManager.upsertDb(toBeWrittenToDb);
} }
cache.set(userId, userAccount); cache.set(userId, userAccount);
return userAccount; return userAccount;
@ -110,12 +137,13 @@ public class CacheStore {
/** /**
* Set user account. * Set user account.
* @param userAccount {@link UserAccount}
*/ */
public static void writeBehind(UserAccount userAccount) { public void writeBehind(final UserAccount userAccount) {
if (cache.isFull() && !cache.contains(userAccount.getUserId())) { if (cache.isFull() && !cache.contains(userAccount.getUserId())) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB..."); LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData(); UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb); dbManager.upsertDb(toBeWrittenToDb);
} }
cache.set(userAccount.getUserId(), userAccount); cache.set(userAccount.getUserId(), userAccount);
} }
@ -123,7 +151,7 @@ public class CacheStore {
/** /**
* Clears cache. * Clears cache.
*/ */
public static void clearCache() { public void clearCache() {
if (cache != null) { if (cache != null) {
cache.clear(); cache.clear();
} }
@ -132,44 +160,51 @@ public class CacheStore {
/** /**
* Writes remaining content in the cache into the DB. * Writes remaining content in the cache into the DB.
*/ */
public static void flushCache() { public void flushCache() {
LOGGER.info("# flushCache..."); LOGGER.info("# flushCache...");
Optional.ofNullable(cache) Optional.ofNullable(cache)
.map(LruCache::getCacheDataInListForm) .map(LruCache::getCacheDataInListForm)
.orElse(List.of()) .orElse(List.of())
.forEach(DbManager::updateDb); .forEach(dbManager::updateDb);
dbManager.disconnect();
} }
/** /**
* Print user accounts. * Print user accounts.
* @return {@link String}
*/ */
public static String print() { public String print() {
return Optional.ofNullable(cache) return Optional.ofNullable(cache)
.map(LruCache::getCacheDataInListForm) .map(LruCache::getCacheDataInListForm)
.orElse(List.of()) .orElse(List.of())
.stream() .stream()
.map(userAccount -> userAccount.toString() + "\n") .map(userAccount -> userAccount.toString() + "\n")
.collect(Collectors.joining("", "\n--CACHE CONTENT--\n", "----\n")); .collect(Collectors.joining("", "\n--CACHE CONTENT--\n", "----"));
} }
/** /**
* Delegate to backing cache store. * Delegate to backing cache store.
* @param userId {@link String}
* @return {@link UserAccount}
*/ */
public static UserAccount get(String userId) { public UserAccount get(final String userId) {
return cache.get(userId); return cache.get(userId);
} }
/** /**
* Delegate to backing cache store. * Delegate to backing cache store.
* @param userId {@link String}
* @param userAccount {@link UserAccount}
*/ */
public static void set(String userId, UserAccount userAccount) { public void set(final String userId, final UserAccount userAccount) {
cache.set(userId, userAccount); cache.set(userId, userAccount);
} }
/** /**
* Delegate to backing cache store. * Delegate to backing cache store.
* @param userId {@link String}
*/ */
public static void invalidate(String userId) { public void invalidate(final String userId) {
cache.invalidate(userId); cache.invalidate(userId);
} }
} }

View File

@ -32,10 +32,25 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public enum CachingPolicy { public enum CachingPolicy {
/**
* Through.
*/
THROUGH("through"), THROUGH("through"),
/**
* AROUND.
*/
AROUND("around"), AROUND("around"),
/**
* BEHIND.
*/
BEHIND("behind"), BEHIND("behind"),
/**
* ASIDE.
*/
ASIDE("aside"); ASIDE("aside");
/**
* Policy value.
*/
private final String policy; private final String policy;
} }

View File

@ -1,171 +0,0 @@
/*
* The MIT License
* Copyright © 2014-2021 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.caching;
import com.iluwatar.caching.constants.CachingConstants;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
/**
* <p>DBManager handles the communication with the underlying data store i.e. Database. It contains
* the implemented methods for querying, inserting, and updating data. MongoDB was used as the
* database for the application.</p>
*
* <p>Developer/Tester is able to choose whether the application should use MongoDB as its
* underlying data storage (connect()) or a simple Java data structure to (temporarily) store the
* data/objects during runtime (createVirtualDB()).</p>
*/
@Slf4j
public final class DbManager {
private static MongoClient mongoClient;
private static MongoDatabase db;
private static boolean useMongoDB;
private static Map<String, UserAccount> virtualDB;
private DbManager() {
}
/**
* Create DB.
*/
public static void createVirtualDb() {
useMongoDB = false;
virtualDB = new HashMap<>();
}
/**
* Connect to DB.
*/
public static void connect() throws ParseException {
useMongoDB = true;
mongoClient = new MongoClient();
db = mongoClient.getDatabase("test");
}
/**
* Read user account from DB.
*/
public static UserAccount readFromDb(String userId) {
if (!useMongoDB) {
if (virtualDB.containsKey(userId)) {
return virtualDB.get(userId);
}
return null;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
LOGGER.error("Error connecting to MongoDB", e);
}
}
var iterable = db
.getCollection(CachingConstants.USER_ACCOUNT)
.find(new Document(CachingConstants.USER_ID, userId));
if (iterable == null) {
return null;
}
Document doc = iterable.first();
String userName = doc.getString(CachingConstants.USER_NAME);
String appInfo = doc.getString(CachingConstants.ADD_INFO);
return new UserAccount(userId, userName, appInfo);
}
/**
* Write user account to DB.
*/
public static void writeToDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).insertOne(
new Document(CachingConstants.USER_ID, userAccount.getUserId())
.append(CachingConstants.USER_NAME, userAccount.getUserName())
.append(CachingConstants.ADD_INFO, userAccount.getAdditionalInfo())
);
}
/**
* Update DB.
*/
public static void updateDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).updateOne(
new Document(CachingConstants.USER_ID, userAccount.getUserId()),
new Document("$set", new Document(CachingConstants.USER_NAME, userAccount.getUserName())
.append(CachingConstants.ADD_INFO, userAccount.getAdditionalInfo())));
}
/**
* Insert data into DB if it does not exist. Else, update it.
*/
public static void upsertDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).updateOne(
new Document(CachingConstants.USER_ID, userAccount.getUserId()),
new Document("$set",
new Document(CachingConstants.USER_ID, userAccount.getUserId())
.append(CachingConstants.USER_NAME, userAccount.getUserName())
.append(CachingConstants.ADD_INFO, userAccount.getAdditionalInfo())
),
new UpdateOptions().upsert(true)
);
}
}

View File

@ -29,41 +29,83 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Data structure/implementation of the application's cache. The data structure consists of a hash * Data structure/implementation of the application's cache. The data structure
* table attached with a doubly linked-list. The linked-list helps in capturing and maintaining the * consists of a hash table attached with a doubly linked-list. The linked-list
* LRU data in the cache. When a data is queried (from the cache), added (to the cache), or updated, * helps in capturing and maintaining the LRU data in the cache. When a data is
* the data is moved to the front of the list to depict itself as the most-recently-used data. The * queried (from the cache), added (to the cache), or updated, the data is
* LRU data is always at the end of the list. * moved to the front of the list to depict itself as the most-recently-used
* data. The LRU data is always at the end of the list.
*/ */
@Slf4j @Slf4j
public class LruCache { public class LruCache {
/**
* Static class Node.
*/
static class Node { static class Node {
String userId; /**
UserAccount userAccount; * user id.
Node previous; */
Node next; private final String userId;
/**
* User Account.
*/
private UserAccount userAccount;
/**
* previous.
*/
private Node previous;
/**
* next.
*/
private Node next;
public Node(String userId, UserAccount userAccount) { /**
this.userId = userId; * Node definition.
this.userAccount = userAccount; *
* @param id String
* @param account {@link UserAccount}
*/
Node(final String id, final UserAccount account) {
this.userId = id;
this.userAccount = account;
} }
} }
int capacity; /**
Map<String, Node> cache = new HashMap<>(); * Capacity of Cache.
Node head; */
Node end; private int capacity;
/**
* Cache {@link HashMap}.
*/
private Map<String, Node> cache = new HashMap<>();
/**
* Head.
*/
private Node head;
/**
* End.
*/
private Node end;
public LruCache(int capacity) { /**
this.capacity = capacity; * Constructor.
*
* @param cap Integer.
*/
public LruCache(final int cap) {
this.capacity = cap;
} }
/** /**
* Get user account. * Get user account.
*
* @param userId String
* @return {@link UserAccount}
*/ */
public UserAccount get(String userId) { public UserAccount get(final String userId) {
if (cache.containsKey(userId)) { if (cache.containsKey(userId)) {
var node = cache.get(userId); var node = cache.get(userId);
remove(node); remove(node);
@ -75,8 +117,10 @@ public class LruCache {
/** /**
* Remove node from linked list. * Remove node from linked list.
*
* @param node {@link Node}
*/ */
public void remove(Node node) { public void remove(final Node node) {
if (node.previous != null) { if (node.previous != null) {
node.previous.next = node.next; node.previous.next = node.next;
} else { } else {
@ -91,8 +135,10 @@ public class LruCache {
/** /**
* Move node to the front of the list. * Move node to the front of the list.
*
* @param node {@link Node}
*/ */
public void setHead(Node node) { public void setHead(final Node node) {
node.next = head; node.next = head;
node.previous = null; node.previous = null;
if (head != null) { if (head != null) {
@ -106,8 +152,11 @@ public class LruCache {
/** /**
* Set user account. * Set user account.
*
* @param userAccount {@link UserAccount}
* @param userId {@link String}
*/ */
public void set(String userId, UserAccount userAccount) { public void set(final String userId, final UserAccount userAccount) {
if (cache.containsKey(userId)) { if (cache.containsKey(userId)) {
var old = cache.get(userId); var old = cache.get(userId);
old.userAccount = userAccount; old.userAccount = userAccount;
@ -127,25 +176,43 @@ public class LruCache {
} }
} }
public boolean contains(String userId) { /**
* Check if Cache contains the userId.
*
* @param userId {@link String}
* @return boolean
*/
public boolean contains(final String userId) {
return cache.containsKey(userId); return cache.containsKey(userId);
} }
/** /**
* Invalidate cache for user. * Invalidate cache for user.
*
* @param userId {@link String}
*/ */
public void invalidate(String userId) { public void invalidate(final String userId) {
var toBeRemoved = cache.remove(userId); var toBeRemoved = cache.remove(userId);
if (toBeRemoved != null) { if (toBeRemoved != null) {
LOGGER.info("# {} has been updated! Removing older version from cache...", userId); LOGGER.info("# {} has been updated! "
+ "Removing older version from cache...", userId);
remove(toBeRemoved); remove(toBeRemoved);
} }
} }
/**
* Check if the cache is full.
* @return boolean
*/
public boolean isFull() { public boolean isFull() {
return cache.size() >= capacity; return cache.size() >= capacity;
} }
/**
* Get LRU data.
*
* @return {@link UserAccount}
*/
public UserAccount getLruData() { public UserAccount getLruData() {
return end.userAccount; return end.userAccount;
} }
@ -161,6 +228,8 @@ public class LruCache {
/** /**
* Returns cache data in list form. * Returns cache data in list form.
*
* @return {@link List}
*/ */
public List<UserAccount> getCacheDataInListForm() { public List<UserAccount> getCacheDataInListForm() {
var listOfCacheData = new ArrayList<UserAccount>(); var listOfCacheData = new ArrayList<UserAccount>();
@ -174,10 +243,14 @@ public class LruCache {
/** /**
* Set cache capacity. * Set cache capacity.
*
* @param newCapacity int
*/ */
public void setCapacity(int newCapacity) { public void setCapacity(final int newCapacity) {
if (capacity > newCapacity) { if (capacity > newCapacity) {
clear(); // Behavior can be modified to accommodate for decrease in cache size. For now, we'll // Behavior can be modified to accommodate
// for decrease in cache size. For now, we'll
clear();
// just clear the cache. // just clear the cache.
} else { } else {
this.capacity = newCapacity; this.capacity = newCapacity;

View File

@ -24,19 +24,28 @@
package com.iluwatar.caching; package com.iluwatar.caching;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
/** /**
* Entity class (stored in cache and DB) used in the application. * Entity class (stored in cache and DB) used in the application.
*/ */
@Setter @Data
@Getter
@AllArgsConstructor @AllArgsConstructor
@ToString @ToString
@EqualsAndHashCode
public class UserAccount { public class UserAccount {
/**
* User Id.
*/
private String userId; private String userId;
/**
* User Name.
*/
private String userName; private String userName;
/**
* Additional Info.
*/
private String additionalInfo; private String additionalInfo;
} }

View File

@ -26,11 +26,27 @@ package com.iluwatar.caching.constants;
/** /**
* Constant class for defining constants. * Constant class for defining constants.
*/ */
public class CachingConstants { public final class CachingConstants {
/**
* User Account.
*/
public static final String USER_ACCOUNT = "user_accounts"; public static final String USER_ACCOUNT = "user_accounts";
/**
* User ID.
*/
public static final String USER_ID = "userID"; public static final String USER_ID = "userID";
/**
* User Name.
*/
public static final String USER_NAME = "userName"; public static final String USER_NAME = "userName";
/**
* Additional Info.
*/
public static final String ADD_INFO = "additionalInfo"; public static final String ADD_INFO = "additionalInfo";
/**
* Constructor.
*/
private CachingConstants() {
}
} }

View File

@ -0,0 +1,4 @@
/**
* Constants.
*/
package com.iluwatar.caching.constants;

View File

@ -0,0 +1,52 @@
package com.iluwatar.caching.database;
import com.iluwatar.caching.UserAccount;
/**
* <p>DBManager handles the communication with the underlying data store i.e.
* Database. It contains the implemented methods for querying, inserting,
* and updating data. MongoDB was used as the database for the application.</p>
*/
public interface DbManager {
/**
* Connect to DB.
*/
void connect();
/**
* Disconnect from DB.
*/
void disconnect();
/**
* Read from DB.
*
* @param userId {@link String}
* @return {@link UserAccount}
*/
UserAccount readFromDb(String userId);
/**
* Write to DB.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
UserAccount writeToDb(UserAccount userAccount);
/**
* Update record.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
UserAccount updateDb(UserAccount userAccount);
/**
* Update record or Insert if not exists.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
UserAccount upsertDb(UserAccount userAccount);
}

View File

@ -0,0 +1,25 @@
package com.iluwatar.caching.database;
/**
* Creates the database connection accroding the input parameter.
*/
public final class DbManagerFactory {
/**
* Private constructor.
*/
private DbManagerFactory() {
}
/**
* Init database.
*
* @param isMongo boolean
* @return {@link DbManager}
*/
public static DbManager initDb(final boolean isMongo) {
if (isMongo) {
return new MongoDb();
}
return new VirtualDb();
}
}

View File

@ -0,0 +1,128 @@
package com.iluwatar.caching.database;
import static com.iluwatar.caching.constants.CachingConstants.ADD_INFO;
import static com.iluwatar.caching.constants.CachingConstants.USER_ACCOUNT;
import static com.iluwatar.caching.constants.CachingConstants.USER_ID;
import static com.iluwatar.caching.constants.CachingConstants.USER_NAME;
import com.iluwatar.caching.UserAccount;
import com.iluwatar.caching.constants.CachingConstants;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
/**
* Implementation of DatabaseManager.
* implements base methods to work with MongoDb.
*/
@Slf4j
public class MongoDb implements DbManager {
private static final String DATABASE_NAME = "admin";
private static final String MONGO_USER = "root";
private static final String MONGO_PASSWORD = "rootpassword";
private MongoClient client;
private MongoDatabase db;
/**
* Connect to Db. Check th connection
*/
@Override
public void connect() {
MongoCredential mongoCredential = MongoCredential.createCredential(MONGO_USER,
DATABASE_NAME,
MONGO_PASSWORD.toCharArray());
MongoClientOptions options = MongoClientOptions.builder().build();
client = new MongoClient(new ServerAddress(), List.of(mongoCredential), options);
db = client.getDatabase(DATABASE_NAME);
}
@Override
public void disconnect() {
client.close();
}
/**
* Read data from DB.
*
* @param userId {@link String}
* @return {@link UserAccount}
*/
@Override
public UserAccount readFromDb(final String userId) {
var iterable = db
.getCollection(CachingConstants.USER_ACCOUNT)
.find(new Document(USER_ID, userId));
if (iterable.first() == null) {
return null;
}
Document doc = iterable.first();
if (doc != null) {
String userName = doc.getString(USER_NAME);
String appInfo = doc.getString(ADD_INFO);
return new UserAccount(userId, userName, appInfo);
} else {
return null;
}
}
/**
* Write data to DB.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount writeToDb(final UserAccount userAccount) {
db.getCollection(USER_ACCOUNT).insertOne(
new Document(USER_ID, userAccount.getUserId())
.append(USER_NAME, userAccount.getUserName())
.append(ADD_INFO, userAccount.getAdditionalInfo())
);
return userAccount;
}
/**
* Update DB.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount updateDb(final UserAccount userAccount) {
Document id = new Document(USER_ID, userAccount.getUserId());
Document dataSet = new Document(USER_NAME, userAccount.getUserName())
.append(ADD_INFO, userAccount.getAdditionalInfo());
db.getCollection(CachingConstants.USER_ACCOUNT)
.updateOne(id, new Document("$set", dataSet));
return userAccount;
}
/**
* Update data if exists.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount upsertDb(final UserAccount userAccount) {
String userId = userAccount.getUserId();
String userName = userAccount.getUserName();
String additionalInfo = userAccount.getAdditionalInfo();
db.getCollection(CachingConstants.USER_ACCOUNT).updateOne(
new Document(USER_ID, userId),
new Document("$set",
new Document(USER_ID, userId)
.append(USER_NAME, userName)
.append(ADD_INFO, additionalInfo)
),
new UpdateOptions().upsert(true)
);
return userAccount;
}
}

View File

@ -0,0 +1,78 @@
package com.iluwatar.caching.database;
import com.iluwatar.caching.UserAccount;
import java.util.HashMap;
import java.util.Map;
/**
* Implementation of DatabaseManager.
* implements base methods to work with hashMap as database.
*/
public class VirtualDb implements DbManager {
/**
* Virtual DataBase.
*/
private Map<String, UserAccount> db;
/**
* Creates new HashMap.
*/
@Override
public void connect() {
db = new HashMap<>();
}
@Override
public void disconnect() {
db = null;
}
/**
* Read from Db.
*
* @param userId {@link String}
* @return {@link UserAccount}
*/
@Override
public UserAccount readFromDb(final String userId) {
if (db.containsKey(userId)) {
return db.get(userId);
}
return null;
}
/**
* Write to DB.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount writeToDb(final UserAccount userAccount) {
db.put(userAccount.getUserId(), userAccount);
return userAccount;
}
/**
* Update reecord in DB.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount updateDb(final UserAccount userAccount) {
return writeToDb(userAccount);
}
/**
* Update.
*
* @param userAccount {@link UserAccount}
* @return {@link UserAccount}
*/
@Override
public UserAccount upsertDb(final UserAccount userAccount) {
return updateDb(userAccount);
}
}

View File

@ -0,0 +1,4 @@
/**
* Database classes.
*/
package com.iluwatar.caching.database;

View File

@ -0,0 +1,20 @@
/**
* The MIT License
* Copyright © 2014-2021 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.caching;

View File

@ -25,25 +25,21 @@ package com.iluwatar.caching;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/** /**
* Tests that Caching example runs without errors. * Tests that Caching example runs without errors.
*/ */
class AppTest { class AppTest {
/** /**
* Issue: Add at least one assertion to this test case. * Issue: Add at least one assertion to this test case.
* * <p>
* Solution: Inserted assertion to check whether the execution of the main method in {@link App} * Solution: Inserted assertion to check whether the execution of the main method in {@link App}
* throws an exception. * throws an exception.
*/ */
@Test @Test
void shouldExecuteApplicationWithoutException() { void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[]{})); assertDoesNotThrow(() -> App.main(new String[]{}));
} }
} }

View File

@ -23,11 +23,11 @@
package com.iluwatar.caching; package com.iluwatar.caching;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/** /**
* Application test * Application test
*/ */
@ -43,32 +43,30 @@ class CachingTest {
// to avoid Maven compilation errors. Set flag to true to run the // to avoid Maven compilation errors. Set flag to true to run the
// tests with MongoDB (provided that MongoDB is installed and socket // tests with MongoDB (provided that MongoDB is installed and socket
// connection is open). // connection is open).
AppManager.initDb(false); app = new App(false);
AppManager.initCacheCapacity(3);
app = new App();
} }
@Test @Test
void testReadAndWriteThroughStrategy() { void testReadAndWriteThroughStrategy() {
assertNotNull(app); assertNotNull(app);
app.useReadAndWriteThroughStrategy(); app.useReadAndWriteThroughStrategy();
} }
@Test @Test
void testReadThroughAndWriteAroundStrategy() { void testReadThroughAndWriteAroundStrategy() {
assertNotNull(app); assertNotNull(app);
app.useReadThroughAndWriteAroundStrategy(); app.useReadThroughAndWriteAroundStrategy();
} }
@Test @Test
void testReadThroughAndWriteBehindStrategy() { void testReadThroughAndWriteBehindStrategy() {
assertNotNull(app); assertNotNull(app);
app.useReadThroughAndWriteBehindStrategy(); app.useReadThroughAndWriteBehindStrategy();
} }
@Test @Test
void testCacheAsideStrategy() { void testCacheAsideStrategy() {
assertNotNull(app); assertNotNull(app);
app.useCacheAsideStategy(); app.useCacheAsideStategy();
} }
} }

View File

@ -0,0 +1,84 @@
package com.iluwatar.caching.database;
import com.iluwatar.caching.UserAccount;
import com.iluwatar.caching.constants.CachingConstants;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.internal.util.reflection.Whitebox;
import static com.iluwatar.caching.constants.CachingConstants.ADD_INFO;
import static com.iluwatar.caching.constants.CachingConstants.USER_ID;
import static com.iluwatar.caching.constants.CachingConstants.USER_NAME;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class MongoDbTest {
private static final String ID = "123";
private static final String NAME = "Some user";
private static final String ADDITIONAL_INFO = "Some app Info";
@Mock
MongoDatabase db;
private MongoDb mongoDb = new MongoDb();
private UserAccount userAccount;
@BeforeEach
void init() {
db = mock(MongoDatabase.class);
Whitebox.setInternalState(mongoDb, "db", db);
userAccount = new UserAccount(ID, NAME, ADDITIONAL_INFO);
}
@Test
void connect() {
assertDoesNotThrow(() -> mongoDb.connect());
}
@Test
void readFromDb() {
Document document = new Document(USER_ID, ID)
.append(USER_NAME, NAME)
.append(ADD_INFO, ADDITIONAL_INFO);
MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
when(db.getCollection(CachingConstants.USER_ACCOUNT)).thenReturn(mongoCollection);
FindIterable<Document> findIterable = mock(FindIterable.class);
when(mongoCollection.find(any(Document.class))).thenReturn(findIterable);
when(findIterable.first()).thenReturn(document);
assertEquals(mongoDb.readFromDb(ID),userAccount);
}
@Test
void writeToDb() {
MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
when(db.getCollection(CachingConstants.USER_ACCOUNT)).thenReturn(mongoCollection);
doNothing().when(mongoCollection).insertOne(any(Document.class));
assertDoesNotThrow(()-> {mongoDb.writeToDb(userAccount);});
}
@Test
void updateDb() {
MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
when(db.getCollection(CachingConstants.USER_ACCOUNT)).thenReturn(mongoCollection);
assertDoesNotThrow(()-> {mongoDb.updateDb(userAccount);});
}
@Test
void upsertDb() {
MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
when(db.getCollection(CachingConstants.USER_ACCOUNT)).thenReturn(mongoCollection);
assertDoesNotThrow(()-> {mongoDb.upsertDb(userAccount);});
}
}

View File

@ -3,7 +3,6 @@ layout: pattern
title: Data Bus title: Data Bus
folder: data-bus folder: data-bus
permalink: /patterns/data-bus/ permalink: /patterns/data-bus/
categories: Architectural categories: Architectural
language: en language: en
tags: tags:

View File

@ -3,10 +3,8 @@ layout: pattern
title: Filterer title: Filterer
folder: filterer folder: filterer
permalink: /patterns/filterer/ permalink: /patterns/filterer/
description: Design pattern that helps container-like objects to return filtered version of themselves.# short meta description that shows in Google search results
language: en language: en
categories: categories: Functional
- Functional
tags: tags:
- Extensibility - Extensibility
--- ---

View File

@ -3,6 +3,7 @@ layout: pattern
title: Lockable Object title: Lockable Object
folder: lockable-object folder: lockable-object
permalink: /patterns/lockable-object/ permalink: /patterns/lockable-object/
language: en
categories: Concurrency categories: Concurrency
tags: tags:
- Performance - Performance

View File

@ -8,6 +8,7 @@ language: en
tags: tags:
- Decoupling - Decoupling
--- ---
## Also known as ## Also known as
Application Model Application Model

View File

@ -4,9 +4,11 @@ title: Table Module
folder: table-module folder: table-module
permalink: /patterns/table-module/ permalink: /patterns/table-module/
categories: Structural categories: Structural
language: en
tags: tags:
- Data access - Data access
--- ---
## Intent ## Intent
Table Module organizes domain logic with one class per table in the database, and a single instance of a class contains the various procedures that will act on the data. Table Module organizes domain logic with one class per table in the database, and a single instance of a class contains the various procedures that will act on the data.

View File

@ -3,7 +3,6 @@ layout: pattern
title: Unit Of Work title: Unit Of Work
folder: unit-of-work folder: unit-of-work
permalink: /patterns/unit-of-work/ permalink: /patterns/unit-of-work/
categories: Architectural categories: Architectural
language: en language: en
tags: tags:

View File

@ -1,10 +1,8 @@
--- ---
layout: pattern layout: pattern
title: Version Number title: Version Number
folder: versionnumber folder: version-number
permalink: /patterns/versionnumber/ permalink: /patterns/version-number/
description: Entity versioning with version number
categories: Concurrency categories: Concurrency
language: en language: en
tags: tags: