From 4b1d1eee86033e4302b3619d76c57ee00134e8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Alloza=20Gonz=C3=A1lez?= Date: Mon, 16 Jan 2023 23:51:26 +0100 Subject: [PATCH] feat: add cache for listing users, choreTypes, chores and weeklyChores (#11) --- README.md | 4 +- build.gradle | 10 +-- src/main/java/Module.java | 6 ++ .../{services => base}/CacheableModule.java | 4 +- src/main/java/bot/BaseChoreManagementBot.java | 5 +- src/main/java/bot/ChoreManagementBot.java | 11 ++- src/main/java/constants/CacheConstants.java | 13 +++ .../java/repositories/BaseRepository.java | 37 +++++--- .../ChoreManagementRepository.java | 18 ---- .../ChoreManagementRepositoryImp.java | 32 +------ .../repositories/chores/ChoresRepository.java | 14 +++ .../ChoresRepositoryCacheableModule.java | 13 +++ .../chores/ChoresRepositoryCached.java | 85 +++++++++++++++++++ .../chores/ChoresRepositoryNonCached.java | 43 ++++++++++ .../choretypes/ChoreTypesRepository.java | 11 +++ .../ChoreTypesRepositoryCacheableModule.java | 13 +++ .../ChoreTypesRepositoryCached.java | 45 ++++++++++ .../ChoreTypesRepositoryNonCached.java | 26 ++++++ .../repositories/users/UsersRepository.java | 10 +++ .../users/UsersRepositoryCacheableModule.java | 13 +++ .../users/UsersRepositoryCached.java | 46 ++++++++++ .../users/UsersRepositoryNonCached.java | 26 ++++++ src/main/java/security/Security.java | 7 +- src/main/java/security/SecurityImp.java | 56 +++++------- .../java/services/ChoreManagementService.java | 8 +- .../services/ChoreManagementServiceImp.java | 41 ++++++--- src/main/java/services/RedisService.java | 5 ++ .../services/latex/LatexCacheableModule.java | 4 +- .../services/latex/LatexServiceCached.java | 4 +- src/main/resources/application.conf | 15 ++-- 30 files changed, 485 insertions(+), 140 deletions(-) rename src/main/java/{services => base}/CacheableModule.java (72%) create mode 100644 src/main/java/constants/CacheConstants.java create mode 100644 src/main/java/repositories/chores/ChoresRepository.java create mode 100644 src/main/java/repositories/chores/ChoresRepositoryCacheableModule.java create mode 100644 src/main/java/repositories/chores/ChoresRepositoryCached.java create mode 100644 src/main/java/repositories/chores/ChoresRepositoryNonCached.java create mode 100644 src/main/java/repositories/choretypes/ChoreTypesRepository.java create mode 100644 src/main/java/repositories/choretypes/ChoreTypesRepositoryCacheableModule.java create mode 100644 src/main/java/repositories/choretypes/ChoreTypesRepositoryCached.java create mode 100644 src/main/java/repositories/choretypes/ChoreTypesRepositoryNonCached.java create mode 100644 src/main/java/repositories/users/UsersRepository.java create mode 100644 src/main/java/repositories/users/UsersRepositoryCacheableModule.java create mode 100644 src/main/java/repositories/users/UsersRepositoryCached.java create mode 100644 src/main/java/repositories/users/UsersRepositoryNonCached.java diff --git a/README.md b/README.md index 80eb8f6..7ba7bd3 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,6 @@ Configuration is done by setting environment variables. - ***API_BASE_URL***: base url where the Chore Management API is deployed. - ***API_HTTP2***: Enable HTTP/2. Defaults to `true`. - ***LATEX_CACHE_ENABLED***: Enable the cache for latex generated images. Defaults to `true`. -- ***USERS_CACHE***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Designed to be used only in testing mode. Defaults to `true`. +- ***CHORE_TYPES_CACHE_ENABLED***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Defaults to `true`. +- ***CHORES_CACHE_ENABLED***: Enable or disable the chores cache. It manages the `listWeeklyChores` and the `listChores` endpoints. Defaults to `true`. +- ***USERS_CACHE_ENABLED***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Defaults to `true`. diff --git a/build.gradle b/build.gradle index 985db55..7995440 100644 --- a/build.gradle +++ b/build.gradle @@ -32,11 +32,11 @@ dependencies { implementation 'org.scilab.forge:jlatexmath:1.0.7' // Json encoding - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0' // Redis - implementation 'redis.clients:jedis:4.2.3' + implementation 'redis.clients:jedis:4.3.1' // Telegram implementation 'org.telegram:telegrambots:6.1.0' @@ -44,9 +44,9 @@ dependencies { // Logging implementation 'org.slf4j:slf4j-api:1.7.36' - implementation 'com.tersesystems.logback:logback-classic:1.0.3' + implementation 'com.tersesystems.logback:logback-classic:1.1.1' implementation 'com.tersesystems.logback:logback-structured-config:1.0.3' - implementation 'com.tersesystems.logback:logback-typesafe-config:1.0.3' + implementation 'com.tersesystems.logback:logback-typesafe-config:1.1.1' implementation 'net.logstash.logback:logstash-logback-encoder:7.2' } diff --git a/src/main/java/Module.java b/src/main/java/Module.java index 12cf99c..13af647 100644 --- a/src/main/java/Module.java +++ b/src/main/java/Module.java @@ -4,6 +4,9 @@ import com.typesafe.config.ConfigFactory; import repositories.ChoreManagementRepository; import repositories.ChoreManagementRepositoryImp; +import repositories.chores.ChoresRepositoryCacheableModule; +import repositories.choretypes.ChoreTypesRepositoryCacheableModule; +import repositories.users.UsersRepositoryCacheableModule; import security.Security; import security.SecurityImp; import services.ChoreManagementService; @@ -18,6 +21,9 @@ public class Module extends AbstractModule { @Override protected void configure() { install(new LatexCacheableModule()); + install(new ChoreTypesRepositoryCacheableModule()); + install(new UsersRepositoryCacheableModule()); + install(new ChoresRepositoryCacheableModule()); Config config = ConfigFactory.load(); String botName = config.getString("telegram.bot.username"); diff --git a/src/main/java/services/CacheableModule.java b/src/main/java/base/CacheableModule.java similarity index 72% rename from src/main/java/services/CacheableModule.java rename to src/main/java/base/CacheableModule.java index 5af33d5..bb05451 100644 --- a/src/main/java/services/CacheableModule.java +++ b/src/main/java/base/CacheableModule.java @@ -1,4 +1,4 @@ -package services; +package base; import com.google.inject.AbstractModule; import com.typesafe.config.ConfigFactory; @@ -6,7 +6,7 @@ @Slf4j public class CacheableModule extends AbstractModule { - public Class getServiceByConfig(String tag, Class cachedClass, Class nonCachedClass) { + public Class getComponentByConfig(String tag, Class cachedClass, Class nonCachedClass) { boolean result = ConfigFactory.load().getBoolean("cache." + tag + ".enabled"); log.info("Cache for " + tag + " is " + (result ? "enabled" : "disabled")); return result ? cachedClass : nonCachedClass; diff --git a/src/main/java/bot/BaseChoreManagementBot.java b/src/main/java/bot/BaseChoreManagementBot.java index 103099a..032a3e4 100644 --- a/src/main/java/bot/BaseChoreManagementBot.java +++ b/src/main/java/bot/BaseChoreManagementBot.java @@ -70,9 +70,10 @@ protected void sendMessage(String msgStr, String chatId, boolean markdown) { } } - protected boolean requireUser(MessageContext ctx) { + protected Boolean requireUser(MessageContext ctx) { var chatId = ctx.chatId().toString(); - if (!security.isAuthenticated(chatId)) { + var isAuthenticated = security.isAuthenticated(chatId).join(); + if (!isAuthenticated) { sendMessage("You don't have permission to execute this action", chatId, false); return false; } diff --git a/src/main/java/bot/ChoreManagementBot.java b/src/main/java/bot/ChoreManagementBot.java index 18a3ec2..f90117a 100644 --- a/src/main/java/bot/ChoreManagementBot.java +++ b/src/main/java/bot/ChoreManagementBot.java @@ -34,7 +34,6 @@ import java.util.stream.Collectors; import static org.telegram.abilitybots.api.objects.Locality.USER; -import static org.telegram.abilitybots.api.objects.Privacy.CREATOR; import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; @Slf4j @@ -176,19 +175,19 @@ private void processMsg(MessageContext ctx) { switch (userMessage) { case UserMessages.TICKETS: - choreTypesFuture = service.getChoreTypes(); + choreTypesFuture = service.listChoreTypes(); service.getTickets(chatId) .thenCombine(choreTypesFuture, Normalizers::normalizeTickets) .thenAcceptAsync(tickets -> sendTable(tickets, chatId, TICKETS_TABLE_PNG, Messages.NO_TICKETS_FOUND), executor); break; case UserMessages.TASKS: - choreTypesFuture = service.getChoreTypes(); + choreTypesFuture = service.listChoreTypes(); service.getWeeklyChores(chatId) .thenCombine(choreTypesFuture, Normalizers::normalizeWeeklyChores) .thenAcceptAsync(tasks -> sendTable(tasks, chatId, WEEKLY_TASKS_TABLE_PNG, Messages.NO_TASKS), executor); break; case UserMessages.COMPLETE_TASK: - choreTypesFuture = service.getChoreTypes(); + choreTypesFuture = service.listChoreTypes(); service.getChores(chatId) .thenCombineAsync(choreTypesFuture, (choreList, choreTypeList) -> startFlowSelectTask(ctx, choreList, choreTypeList), executor); @@ -216,7 +215,7 @@ private void processReplyMsg(MessageContext ctx) { .handle(replyHandler(ctx, "Week skipped: " + userMessage)); break; case Messages.ASK_FOR_WEEK_TO_UNSKIP: - service.unskipWeek(chatId, userMessage) + service.unSkipWeek(chatId, userMessage) .handle(replyHandler(ctx, "Week unskipped: " + userMessage)); break; default: @@ -233,7 +232,7 @@ private void processQueryData(MessageContext ctx) { switch (callbackData.getType()) { case COMPLETE_TASK: - service.completeTask(chatId, callbackData.getWeekId(), callbackData.getChoreType()) + service.completeChore(chatId, callbackData.getWeekId(), callbackData.getChoreType()) .handle(callbackQueryHandler(ctx, queryId, Messages.TASK_COMPLETED, QueryType.COMPLETE_TASK)); break; default: diff --git a/src/main/java/constants/CacheConstants.java b/src/main/java/constants/CacheConstants.java new file mode 100644 index 0000000..05a4f9b --- /dev/null +++ b/src/main/java/constants/CacheConstants.java @@ -0,0 +1,13 @@ +package constants; + +public class CacheConstants { + public static final int LATEX_CACHE_EXPIRE_SECONDS = 2 * 7 * 24 * 3600; + public static final int USERS_CACHE_EXPIRE_SECONDS = 4 * 7 * 24 * 3600; + public static final int CHORE_TYPES_CACHE_EXPIRE_SECONDS = 4 * 7 * 24 * 3600; + public static final int CHORES_CACHE_EXPIRE_SECONDS = 7 * 24 * 3600; + public static final int WEEKLY_CHORES_CACHE_EXPIRE_SECONDS = 7 * 24 * 3600; + + public static final String USERS_REDIS_KEY_PREFIX = "api::users"; + public static final String CHORES_REDIS_KEY_PREFIX = "api::chores"; + public static final String WEEKLY_CHORES_REDIS_KEY_PREFIX = "api::weeklyChores"; +} diff --git a/src/main/java/repositories/BaseRepository.java b/src/main/java/repositories/BaseRepository.java index 24f3d85..a7cfa1d 100644 --- a/src/main/java/repositories/BaseRepository.java +++ b/src/main/java/repositories/BaseRepository.java @@ -28,15 +28,14 @@ public class BaseRepository { private static final Set VALID_STATUS_CODES = Set.of(200, 201, 204); private final String baseURL; - private final String apiToken; + private final String adminApiKey; private final boolean http2; private final Security security; protected final Executor executor; - public BaseRepository(String baseURL, String apiToken, ConfigRepository config, - Security security, Executor executor) { - this.baseURL = baseURL; - this.apiToken = apiToken; + public BaseRepository(ConfigRepository config, Security security, Executor executor) { + this.baseURL = config.getString("api.baseURL"); + this.adminApiKey = config.getString("api.adminApiKey"); this.http2 = config.getBoolean("api.http2"); this.security = security; this.executor = executor; @@ -47,21 +46,21 @@ private HttpClient.Version getHttpClientVersion() { } protected CompletableFuture sendGetRequest(String path, Class clazz, String userId) { - String token = security.getTenantToken(userId); - return sendRequest("GET", path, clazz, token, null); + return security.getUserApiKey(userId) + .thenComposeAsync(token -> sendRequest("GET", path, clazz, token, null), executor); } protected CompletableFuture sendPostRequest(String path, Class clazz, String userId) { - String token = security.getTenantToken(userId); - return sendRequest("POST", path, clazz, token, null); + return security.getUserApiKey(userId) + .thenComposeAsync(token -> sendRequest("POST", path, clazz, token, null), executor); } protected CompletableFuture sendPostRequestAdmin(String path, Class clazz) { - return sendRequest("POST", path, clazz, apiToken, null); + return sendRequest("POST", path, clazz, adminApiKey, null); } protected CompletableFuture sendGetRequestAdmin(String path, Class clazz) { - return sendRequest("GET", path, clazz, apiToken, null); + return sendRequest("GET", path, clazz, adminApiKey, null); } private CompletableFuture sendRequest(String method, String path, Class clazz, @@ -111,10 +110,10 @@ private CompletableFuture sendRequest(String method, String path, Class bodyOpt.map(body -> processBody(body, clazz)).orElse(null), executor); + .thenApplyAsync(bodyOpt -> bodyOpt.map(body -> fromJson(body, clazz)).orElse(null), executor); } - private T processBody(String body, Class clazz) { + protected T fromJson(String body, Class clazz) { if (clazz == null) { return null; } @@ -123,7 +122,17 @@ private T processBody(String body, Class clazz) { try { return mapper.readValue(body, clazz); } catch (JsonProcessingException e) { - log.error("Error parsing response: " + body, e); + log.error("Error parsing json: " + body, e); + return null; + } + } + + protected String toJson(Object object) { + ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + try { + return mapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + log.error("Error parsing json: " + object, e); return null; } } diff --git a/src/main/java/repositories/ChoreManagementRepository.java b/src/main/java/repositories/ChoreManagementRepository.java index f6ee276..1d29c6d 100644 --- a/src/main/java/repositories/ChoreManagementRepository.java +++ b/src/main/java/repositories/ChoreManagementRepository.java @@ -1,30 +1,12 @@ package repositories; -import models.Chore; -import models.ChoreType; import models.Ticket; -import models.User; -import models.WeeklyChores; import java.util.List; import java.util.concurrent.CompletableFuture; public interface ChoreManagementRepository { CompletableFuture> getTickets(String userId); - - CompletableFuture> getWeeklyChores(String userId); - - CompletableFuture completeTask(String userId, String weekId, String choreType); - - CompletableFuture> getChores(String userId); - CompletableFuture skipWeek(String userId, String weekId); - CompletableFuture unskipWeek(String userId, String weekId); - - CompletableFuture createWeeklyChores(String weekId); - - CompletableFuture> listUsersAdminToken(); - - CompletableFuture> getChoreTypes(); } diff --git a/src/main/java/repositories/ChoreManagementRepositoryImp.java b/src/main/java/repositories/ChoreManagementRepositoryImp.java index 0d24c24..8d973e2 100644 --- a/src/main/java/repositories/ChoreManagementRepositoryImp.java +++ b/src/main/java/repositories/ChoreManagementRepositoryImp.java @@ -17,7 +17,7 @@ public class ChoreManagementRepositoryImp extends BaseRepository implements ChoreManagementRepository { @Inject public ChoreManagementRepositoryImp(ConfigRepository config, Security security, Executor executor) { - super(config.getString("api.baseURL"), config.getString("api.adminApiKey"), config, security, executor); + super(config, security, executor); } public CompletableFuture> getTickets(String userId) { @@ -25,21 +25,6 @@ public CompletableFuture> getTickets(String userId) { .thenApply(Arrays::asList); } - public CompletableFuture> getWeeklyChores(String userId) { - return sendGetRequest("/api/v1/weekly-chores?missing_only=true", WeeklyChores[].class, userId) - .thenApply(Arrays::asList); - } - - public CompletableFuture completeTask(String userId, String weekId, String choreType) { - String path = "/api/v1/chores/" + weekId + "/type/" + choreType + "/complete"; - return sendPostRequest(path, null, userId); - } - - public CompletableFuture> getChores(String userId) { - return sendGetRequest("/api/v1/chores?user_id=me&done=false", Chore[].class, userId) - .thenApply(Arrays::asList); - } - public CompletableFuture skipWeek(String userId, String weekId) { return sendPostRequest("/api/v1/users/me/deactivate/" + weekId, null, userId); } @@ -47,19 +32,4 @@ public CompletableFuture skipWeek(String userId, String weekId) { public CompletableFuture unskipWeek(String userId, String weekId) { return sendPostRequest("/api/v1/users/me/reactivate/" + weekId, null, userId); } - - public CompletableFuture createWeeklyChores(String weekId) { - return sendPostRequestAdmin("/api/v1/weekly-chores/" + weekId, WeeklyChores.class); - } - - public CompletableFuture> listUsersAdminToken() { - return sendGetRequestAdmin("/api/v1/users", User[].class) - .thenApply(Arrays::asList); - } - - @Override - public CompletableFuture> getChoreTypes() { - return sendGetRequestAdmin("/api/v1/chore-types", ChoreType[].class) - .thenApply(Arrays::asList); - } } diff --git a/src/main/java/repositories/chores/ChoresRepository.java b/src/main/java/repositories/chores/ChoresRepository.java new file mode 100644 index 0000000..c0112c3 --- /dev/null +++ b/src/main/java/repositories/chores/ChoresRepository.java @@ -0,0 +1,14 @@ +package repositories.chores; + +import models.Chore; +import models.WeeklyChores; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface ChoresRepository { + CompletableFuture> listChores(String userId); + CompletableFuture> listWeeklyChores(String userId); + CompletableFuture createWeeklyChores(String weekId); + CompletableFuture completeChore(String userId, String weekId, String choreType); +} diff --git a/src/main/java/repositories/chores/ChoresRepositoryCacheableModule.java b/src/main/java/repositories/chores/ChoresRepositoryCacheableModule.java new file mode 100644 index 0000000..fe75610 --- /dev/null +++ b/src/main/java/repositories/chores/ChoresRepositoryCacheableModule.java @@ -0,0 +1,13 @@ +package repositories.chores; + +import base.CacheableModule; + +public class ChoresRepositoryCacheableModule extends CacheableModule { + @Override + protected void configure() { + bind(ChoresRepository.class).to(getComponentByConfig( + "chores", + ChoresRepositoryCached.class, + ChoresRepositoryNonCached.class)); + } +} diff --git a/src/main/java/repositories/chores/ChoresRepositoryCached.java b/src/main/java/repositories/chores/ChoresRepositoryCached.java new file mode 100644 index 0000000..fe40a1b --- /dev/null +++ b/src/main/java/repositories/chores/ChoresRepositoryCached.java @@ -0,0 +1,85 @@ +package repositories.chores; + +import com.google.inject.Inject; +import config.ConfigRepository; +import lombok.extern.slf4j.Slf4j; +import models.Chore; +import models.WeeklyChores; +import repositories.BaseRepository; +import security.Security; +import services.RedisService; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import static constants.CacheConstants.CHORES_CACHE_EXPIRE_SECONDS; +import static constants.CacheConstants.CHORES_REDIS_KEY_PREFIX; +import static constants.CacheConstants.WEEKLY_CHORES_CACHE_EXPIRE_SECONDS; +import static constants.CacheConstants.WEEKLY_CHORES_REDIS_KEY_PREFIX; + +@Slf4j +public class ChoresRepositoryCached extends BaseRepository implements ChoresRepository { + private final RedisService redisService; + private final ChoresRepositoryNonCached choresRepository; + + @Inject + public ChoresRepositoryCached(ConfigRepository config, Security security, Executor executor, + RedisService redisService, ChoresRepositoryNonCached choresRepository) { + super(config, security, executor); + this.redisService = redisService; + this.choresRepository = choresRepository; + } + + @Override + public CompletableFuture> listChores(String userId) { + var result = redisService.get(CHORES_REDIS_KEY_PREFIX); + if (result != null) { + log.debug("Cache hit for chores"); + return CompletableFuture.completedFuture(fromJson(result, Chore[].class)) + .thenApply(List::of); + } + log.debug("Cache miss for chores"); + return choresRepository.listChores(userId) + .thenApply(chores -> { + redisService.setex(CHORES_REDIS_KEY_PREFIX, CHORES_CACHE_EXPIRE_SECONDS, toJson(chores)); + return chores; + }); + } + + @Override + public CompletableFuture> listWeeklyChores(String userId) { + var result = redisService.get(WEEKLY_CHORES_REDIS_KEY_PREFIX); + if (result != null) { + log.debug("Cache hit for weeklyChores"); + return CompletableFuture.completedFuture(fromJson(result, WeeklyChores[].class)) + .thenApply(List::of); + } + log.debug("Cache miss for weeklyChores"); + return choresRepository.listWeeklyChores(userId) + .thenApply(weeklyChores -> { + redisService.setex(WEEKLY_CHORES_REDIS_KEY_PREFIX, WEEKLY_CHORES_CACHE_EXPIRE_SECONDS, toJson(weeklyChores)); + return weeklyChores; + }); + } + + @Override + public CompletableFuture createWeeklyChores(String weekId) { + return choresRepository.createWeeklyChores(weekId) + .thenApply(weeklyChores -> { + redisService.del(CHORES_REDIS_KEY_PREFIX); + redisService.del(WEEKLY_CHORES_REDIS_KEY_PREFIX); + return weeklyChores; + }); + } + + @Override + public CompletableFuture completeChore(String userId, String weekId, String choreType) { + return choresRepository.completeChore(userId, weekId, choreType) + .thenApply(weeklyChores -> { + redisService.del(CHORES_REDIS_KEY_PREFIX); + redisService.del(WEEKLY_CHORES_REDIS_KEY_PREFIX); + return weeklyChores; + }); + } +} diff --git a/src/main/java/repositories/chores/ChoresRepositoryNonCached.java b/src/main/java/repositories/chores/ChoresRepositoryNonCached.java new file mode 100644 index 0000000..9fe5603 --- /dev/null +++ b/src/main/java/repositories/chores/ChoresRepositoryNonCached.java @@ -0,0 +1,43 @@ +package repositories.chores; + +import com.google.inject.Inject; +import config.ConfigRepository; +import models.Chore; +import models.WeeklyChores; +import repositories.BaseRepository; +import security.Security; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class ChoresRepositoryNonCached extends BaseRepository implements ChoresRepository { + @Inject + public ChoresRepositoryNonCached(ConfigRepository config, Security security, Executor executor) { + super(config, security, executor); + } + + @Override + public CompletableFuture> listChores(String userId) { + return sendGetRequest("/api/v1/chores?user_id=me&done=false", Chore[].class, userId) + .thenApply(Arrays::asList); + } + + @Override + public CompletableFuture> listWeeklyChores(String userId) { + return sendGetRequest("/api/v1/weekly-chores?missing_only=true", WeeklyChores[].class, userId) + .thenApply(Arrays::asList); + } + + @Override + public CompletableFuture createWeeklyChores(String weekId) { + return sendPostRequestAdmin("/api/v1/weekly-chores/" + weekId, WeeklyChores.class); + } + + @Override + public CompletableFuture completeChore(String userId, String weekId, String choreType) { + String path = "/api/v1/chores/" + weekId + "/type/" + choreType + "/complete"; + return sendPostRequest(path, null, userId); + } +} diff --git a/src/main/java/repositories/choretypes/ChoreTypesRepository.java b/src/main/java/repositories/choretypes/ChoreTypesRepository.java new file mode 100644 index 0000000..e0237c7 --- /dev/null +++ b/src/main/java/repositories/choretypes/ChoreTypesRepository.java @@ -0,0 +1,11 @@ +package repositories.choretypes; + +import models.ChoreType; +import models.User; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface ChoreTypesRepository { + CompletableFuture> listChoreTypes(); +} diff --git a/src/main/java/repositories/choretypes/ChoreTypesRepositoryCacheableModule.java b/src/main/java/repositories/choretypes/ChoreTypesRepositoryCacheableModule.java new file mode 100644 index 0000000..96a1bca --- /dev/null +++ b/src/main/java/repositories/choretypes/ChoreTypesRepositoryCacheableModule.java @@ -0,0 +1,13 @@ +package repositories.choretypes; + +import base.CacheableModule; + +public class ChoreTypesRepositoryCacheableModule extends CacheableModule { + @Override + protected void configure() { + bind(ChoreTypesRepository.class).to(getComponentByConfig( + "choreTypes", + ChoreTypesRepositoryCached.class, + ChoreTypesRepositoryNonCached.class)); + } +} diff --git a/src/main/java/repositories/choretypes/ChoreTypesRepositoryCached.java b/src/main/java/repositories/choretypes/ChoreTypesRepositoryCached.java new file mode 100644 index 0000000..c10d5e1 --- /dev/null +++ b/src/main/java/repositories/choretypes/ChoreTypesRepositoryCached.java @@ -0,0 +1,45 @@ +package repositories.choretypes; + +import com.google.inject.Inject; +import config.ConfigRepository; +import lombok.extern.slf4j.Slf4j; +import models.ChoreType; +import repositories.BaseRepository; +import security.Security; +import services.RedisService; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import static constants.CacheConstants.CHORE_TYPES_CACHE_EXPIRE_SECONDS; + +@Slf4j +public class ChoreTypesRepositoryCached extends BaseRepository implements ChoreTypesRepository { + private final RedisService redisService; + private final ChoreTypesRepositoryNonCached choreTypesRepository; + + @Inject + public ChoreTypesRepositoryCached(ConfigRepository config, Security security, Executor executor, + RedisService redisService, ChoreTypesRepositoryNonCached choreTypesRepository) { + super(config, security, executor); + this.redisService = redisService; + this.choreTypesRepository = choreTypesRepository; + } + + @Override + public CompletableFuture> listChoreTypes() { + var result = redisService.get("api::choreTypes"); + if (result != null) { + log.debug("Cache hit for chore types"); + return CompletableFuture.completedFuture(fromJson(result, ChoreType[].class)) + .thenApply(List::of); + } + log.debug("Cache miss for chore types"); + return choreTypesRepository.listChoreTypes() + .thenApply(choreTypes -> { + redisService.setex("api::choreTypes", CHORE_TYPES_CACHE_EXPIRE_SECONDS, toJson(choreTypes)); + return choreTypes; + }); + } +} diff --git a/src/main/java/repositories/choretypes/ChoreTypesRepositoryNonCached.java b/src/main/java/repositories/choretypes/ChoreTypesRepositoryNonCached.java new file mode 100644 index 0000000..c3caad1 --- /dev/null +++ b/src/main/java/repositories/choretypes/ChoreTypesRepositoryNonCached.java @@ -0,0 +1,26 @@ +package repositories.choretypes; + +import com.google.inject.Inject; +import config.ConfigRepository; +import models.ChoreType; +import repositories.BaseRepository; +import security.Security; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class ChoreTypesRepositoryNonCached extends BaseRepository implements ChoreTypesRepository { + @Inject + public ChoreTypesRepositoryNonCached(String baseURL, String apiToken, ConfigRepository config, + Security security, Executor executor) { + super(config, security, executor); + } + + @Override + public CompletableFuture> listChoreTypes() { + return sendGetRequestAdmin("/api/v1/chore-types", ChoreType[].class) + .thenApply(Arrays::asList); + } +} diff --git a/src/main/java/repositories/users/UsersRepository.java b/src/main/java/repositories/users/UsersRepository.java new file mode 100644 index 0000000..e400778 --- /dev/null +++ b/src/main/java/repositories/users/UsersRepository.java @@ -0,0 +1,10 @@ +package repositories.users; + +import models.User; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface UsersRepository { + CompletableFuture> listUsers(); +} diff --git a/src/main/java/repositories/users/UsersRepositoryCacheableModule.java b/src/main/java/repositories/users/UsersRepositoryCacheableModule.java new file mode 100644 index 0000000..f88ebe8 --- /dev/null +++ b/src/main/java/repositories/users/UsersRepositoryCacheableModule.java @@ -0,0 +1,13 @@ +package repositories.users; + +import base.CacheableModule; + +public class UsersRepositoryCacheableModule extends CacheableModule { + @Override + protected void configure() { + bind(UsersRepository.class).to(getComponentByConfig( + "users", + UsersRepositoryCached.class, + UsersRepositoryNonCached.class)); + } +} diff --git a/src/main/java/repositories/users/UsersRepositoryCached.java b/src/main/java/repositories/users/UsersRepositoryCached.java new file mode 100644 index 0000000..a05ae61 --- /dev/null +++ b/src/main/java/repositories/users/UsersRepositoryCached.java @@ -0,0 +1,46 @@ +package repositories.users; + +import com.google.inject.Inject; +import config.ConfigRepository; +import lombok.extern.slf4j.Slf4j; +import models.User; +import repositories.BaseRepository; +import security.Security; +import services.RedisService; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import static constants.CacheConstants.USERS_CACHE_EXPIRE_SECONDS; +import static constants.CacheConstants.USERS_REDIS_KEY_PREFIX; + +@Slf4j +public class UsersRepositoryCached extends BaseRepository implements UsersRepository { + private final RedisService redisService; + private final UsersRepositoryNonCached usersRepository; + + @Inject + public UsersRepositoryCached(ConfigRepository config, Security security, Executor executor, + RedisService redisService, UsersRepositoryNonCached usersRepository) { + super(config, security, executor); + this.redisService = redisService; + this.usersRepository = usersRepository; + } + + @Override + public CompletableFuture> listUsers() { + var result = redisService.get(USERS_REDIS_KEY_PREFIX); + if (result != null) { + log.debug("Cache hit for users"); + return CompletableFuture.completedFuture(fromJson(result, User[].class)) + .thenApply(List::of); + } + log.debug("Cache miss for users"); + return usersRepository.listUsers() + .thenApply(users -> { + redisService.setex(USERS_REDIS_KEY_PREFIX, USERS_CACHE_EXPIRE_SECONDS, toJson(users)); + return users; + }); + } +} diff --git a/src/main/java/repositories/users/UsersRepositoryNonCached.java b/src/main/java/repositories/users/UsersRepositoryNonCached.java new file mode 100644 index 0000000..946edbf --- /dev/null +++ b/src/main/java/repositories/users/UsersRepositoryNonCached.java @@ -0,0 +1,26 @@ +package repositories.users; + +import com.google.inject.Inject; +import config.ConfigRepository; +import models.User; +import repositories.BaseRepository; +import security.Security; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class UsersRepositoryNonCached extends BaseRepository implements UsersRepository { + @Inject + public UsersRepositoryNonCached(String baseURL, String apiToken, ConfigRepository config, + Security security, Executor executor) { + super(config, security, executor); + } + + @Override + public CompletableFuture> listUsers() { + return sendGetRequestAdmin("/api/v1/users", User[].class) + .thenApply(Arrays::asList); + } +} diff --git a/src/main/java/security/Security.java b/src/main/java/security/Security.java index f0d6c86..e321ecc 100644 --- a/src/main/java/security/Security.java +++ b/src/main/java/security/Security.java @@ -1,7 +1,8 @@ package security; -public interface Security { - String getTenantToken(String userId); +import java.util.concurrent.CompletableFuture; - boolean isAuthenticated(String userId); +public interface Security { + CompletableFuture getUserApiKey(String userId); + CompletableFuture isAuthenticated(String userId); } diff --git a/src/main/java/security/SecurityImp.java b/src/main/java/security/SecurityImp.java index cc5ee44..4412ab3 100644 --- a/src/main/java/security/SecurityImp.java +++ b/src/main/java/security/SecurityImp.java @@ -1,51 +1,41 @@ package security; import com.google.inject.Singleton; -import com.typesafe.config.Config; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import models.User; import services.ChoreManagementService; import javax.inject.Inject; import java.util.List; +import java.util.concurrent.CompletableFuture; @Slf4j @Singleton public class SecurityImp implements Security { - private final ChoreManagementService service; - private final boolean cached; - private List userList; + private final ChoreManagementService service; - @Inject - public SecurityImp(ChoreManagementService service, Config config) { - this.service = service; - cached = config.getBoolean("api.users.cache"); - log.debug("Security is {}", cached ? "cached" : "not cached"); - } + @Inject + public SecurityImp(ChoreManagementService service) { + this.service = service; + } - public String getTenantToken(String tenantId) { - return getUsers().stream() - .filter(t -> t.getId().equals(tenantId)) - .findFirst() - .map(User::getApiKey) - .orElse(null); - } + public CompletableFuture getUserApiKey(String userId) { + return getUsers() + .thenApply(users -> users.stream() + .filter(user -> user.getId().equals(userId)) + .findFirst() + .map(User::getApiKey) + .orElse(null)); + } - public boolean isAuthenticated(String tenantId) { - return getUsers().stream() - .anyMatch(t -> t.getId().equals(tenantId)); - } + public CompletableFuture isAuthenticated(String userId) { + return getUsers() + .thenApply(users -> users.stream() + .anyMatch(t -> t.getId().equals(userId))); + } - private List getUsers() { - if (!cached || userList == null) { - try { - userList = service.listUsersAdminToken().get(); - } catch (Exception e) { - log.error("Error getting users", e); - e.printStackTrace(); - throw new RuntimeException(e); - } - } - return userList; - } + private CompletableFuture> getUsers() { + return service.listUsers(); + } } diff --git a/src/main/java/services/ChoreManagementService.java b/src/main/java/services/ChoreManagementService.java index 1e82b55..b1ccb54 100644 --- a/src/main/java/services/ChoreManagementService.java +++ b/src/main/java/services/ChoreManagementService.java @@ -17,15 +17,15 @@ public interface ChoreManagementService { CompletableFuture> getChores(String userId); - CompletableFuture completeTask(String userId, String weekId, String choreType); + CompletableFuture completeChore(String userId, String weekId, String choreType); CompletableFuture skipWeek(String userId, String weekId); - CompletableFuture unskipWeek(String userId, String weekId); + CompletableFuture unSkipWeek(String userId, String weekId); - CompletableFuture> getChoreTypes(); + CompletableFuture> listChoreTypes(); CompletableFuture createWeeklyChores(String weekId); - CompletableFuture> listUsersAdminToken(); + CompletableFuture> listUsers(); } diff --git a/src/main/java/services/ChoreManagementServiceImp.java b/src/main/java/services/ChoreManagementServiceImp.java index ade6620..b74af06 100644 --- a/src/main/java/services/ChoreManagementServiceImp.java +++ b/src/main/java/services/ChoreManagementServiceImp.java @@ -8,54 +8,71 @@ import models.User; import models.WeeklyChores; import repositories.ChoreManagementRepository; +import repositories.chores.ChoresRepository; +import repositories.choretypes.ChoreTypesRepository; +import repositories.users.UsersRepository; import java.util.List; import java.util.concurrent.CompletableFuture; - @Slf4j public class ChoreManagementServiceImp implements ChoreManagementService { private final ChoreManagementRepository repository; + private final UsersRepository usersRepository; + private final ChoreTypesRepository choreTypesRepository; + private final ChoresRepository choresRepository; @Inject - public ChoreManagementServiceImp(ChoreManagementRepository repository) { + public ChoreManagementServiceImp(ChoreManagementRepository repository, UsersRepository usersRepository, + ChoreTypesRepository choreTypesRepository, ChoresRepository choresRepository) { this.repository = repository; + this.usersRepository = usersRepository; + this.choreTypesRepository = choreTypesRepository; + this.choresRepository = choresRepository; } + @Override public CompletableFuture> getTickets(String userId) { return repository.getTickets(userId); } + @Override public CompletableFuture> getWeeklyChores(String userId) { - return repository.getWeeklyChores(userId); + return choresRepository.listWeeklyChores(userId); } + @Override public CompletableFuture> getChores(String userId) { - return repository.getChores(userId); + return choresRepository.listChores(userId); } - public CompletableFuture completeTask(String userId, String weekId, String choreType) { - return repository.completeTask(userId, weekId, choreType); + @Override + public CompletableFuture completeChore(String userId, String weekId, String choreType) { + return choresRepository.completeChore(userId, weekId, choreType); } + @Override public CompletableFuture skipWeek(String userId, String weekId) { return repository.skipWeek(userId, weekId); } - public CompletableFuture unskipWeek(String userId, String weekId) { + @Override + public CompletableFuture unSkipWeek(String userId, String weekId) { return repository.unskipWeek(userId, weekId); } @Override - public CompletableFuture> getChoreTypes() { - return repository.getChoreTypes(); + public CompletableFuture> listChoreTypes() { + return choreTypesRepository.listChoreTypes(); } + @Override public CompletableFuture createWeeklyChores(String weekId) { - return repository.createWeeklyChores(weekId); + return choresRepository.createWeeklyChores(weekId); } - public CompletableFuture> listUsersAdminToken() { - return repository.listUsersAdminToken(); + @Override + public CompletableFuture> listUsers() { + return usersRepository.listUsers(); } } diff --git a/src/main/java/services/RedisService.java b/src/main/java/services/RedisService.java index afda470..e4c4001 100644 --- a/src/main/java/services/RedisService.java +++ b/src/main/java/services/RedisService.java @@ -37,6 +37,11 @@ public void setex(String key, int expire, String value) { jedis.setex(key, expire, value); } + public void del(String key) { + log.debug("Deleting key {}", key); + jedis.del(key); + } + public String get(String key) { return jedis.get(key); } diff --git a/src/main/java/services/latex/LatexCacheableModule.java b/src/main/java/services/latex/LatexCacheableModule.java index 2df3285..5682328 100644 --- a/src/main/java/services/latex/LatexCacheableModule.java +++ b/src/main/java/services/latex/LatexCacheableModule.java @@ -1,11 +1,11 @@ package services.latex; -import services.CacheableModule; +import base.CacheableModule; public class LatexCacheableModule extends CacheableModule { @Override protected void configure() { - bind(LatexService.class).to(getServiceByConfig( + bind(LatexService.class).to(getComponentByConfig( "latex", LatexServiceCached.class, LatexServiceNonCached.class)); diff --git a/src/main/java/services/latex/LatexServiceCached.java b/src/main/java/services/latex/LatexServiceCached.java index 76d4b78..3ac24c5 100644 --- a/src/main/java/services/latex/LatexServiceCached.java +++ b/src/main/java/services/latex/LatexServiceCached.java @@ -14,11 +14,11 @@ import java.util.Base64; import java.util.List; +import static constants.CacheConstants.LATEX_CACHE_EXPIRE_SECONDS; import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_256; @Slf4j public class LatexServiceCached implements LatexService { - private static final int LATEX_CACHE_EXPIRE_SECONDS = 2 * 7 * 24 * 3600; private final LatexServiceNonCached latexService; private final RedisService redisService; @@ -39,7 +39,7 @@ public void genTable(List> data, String path) { } private String getKey(List> data, String prefix) { - return prefix + "." + new DigestUtils(SHA_256).digestAsHex(String.valueOf(data)); + return prefix + "::" + new DigestUtils(SHA_256).digestAsHex(String.valueOf(data)); } private boolean getFromCache(String key, String path) { diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index c21664c..26d56b1 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -17,14 +17,19 @@ api { baseURL = ${API_BASE_URL} http2 = true http2 = ${?API_HTTP2} - adminApiKey =${ADMIN_API_KEY} - users { - cache = true - cache = ${?USERS_CACHE} - } + adminApiKey = ${ADMIN_API_KEY} } cache { + choreTypes.enabled = true + choreTypes.enabled = ${?CHORE_TYPES_CACHE_ENABLED} + + chores.enabled = true + chores.enabled = ${?CHORES_CACHE_ENABLED} + + users.enabled = true + users.enabled = ${?USERS_CACHE_ENABLED} + latex.enabled = true latex.enabled = ${?LATEX_CACHE_ENABLED} }