-
Notifications
You must be signed in to change notification settings - Fork 12
Message config for whatsapp multi language flow #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package io.appform.statesman.engine; | ||
|
|
||
| import io.appform.statesman.model.MessageConfig; | ||
| import io.appform.statesman.model.action.template.ActionTemplate; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * Interface for storing and accessing message config from cache backed by DB | ||
| */ | ||
| public interface MessageConfigStore { | ||
|
|
||
| Optional<MessageConfig> create(MessageConfig messageConfig); | ||
|
|
||
| Optional<MessageConfig> get(String messageConfigId); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package io.appform.statesman.model; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import javax.validation.constraints.NotNull; | ||
|
|
||
| /** | ||
| * REST entity for posting the message config resource to the API | ||
| */ | ||
| @Data | ||
| @Builder | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public class MessageConfig { | ||
| @NotNull | ||
| private String messageId; | ||
|
|
||
| @NotNull | ||
| private JsonNode messageBody; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package io.appform.statesman.server.dao.message; | ||
|
|
||
| /** | ||
| * Interface for constructing the message for a given | ||
| * message id,language and state from the config defined | ||
| */ | ||
| public interface IMessageConstructor { | ||
| String constructMessage(String messageId,String language,String state); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package io.appform.statesman.server.dao.message; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.github.benmanes.caffeine.cache.Caffeine; | ||
| import com.github.benmanes.caffeine.cache.LoadingCache; | ||
| import io.appform.dropwizard.sharding.dao.LookupDao; | ||
| import io.appform.statesman.engine.MessageConfigStore; | ||
| import io.appform.statesman.model.MessageConfig; | ||
| import io.appform.statesman.model.exception.ResponseCode; | ||
| import io.appform.statesman.model.exception.StatesmanError; | ||
| import io.appform.statesman.server.utils.MapperUtils; | ||
| import io.appform.statesman.server.utils.WorkflowUtils; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import javax.inject.Inject; | ||
| import javax.inject.Singleton; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| /** | ||
| * Responsible for storing and accessing message config from cache backed by DB | ||
| */ | ||
| @Slf4j | ||
| @Singleton | ||
| public class MessageConfigStoreCommand implements MessageConfigStore { | ||
|
|
||
| private final LookupDao<StoredMessageConfig> messageLookupDao; | ||
| private final LoadingCache<String, Optional<MessageConfig>> messageConfigCache; | ||
|
|
||
| @Inject | ||
| public MessageConfigStoreCommand(LookupDao<StoredMessageConfig> messageLookupDao) { | ||
| this.messageLookupDao = messageLookupDao; | ||
| messageConfigCache = Caffeine.newBuilder() | ||
| .maximumSize(1_000) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does 1_000 mean ?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a digit seperator notion that I took from other classes. For grouping digits for better readability |
||
| .expireAfterWrite(120, TimeUnit.MINUTES) | ||
| .refreshAfterWrite(15, TimeUnit.MINUTES) | ||
| .build(key -> { | ||
| log.debug("Loading data for action for key: {}", key); | ||
| return getFromDb(key); | ||
| }); | ||
| } | ||
|
|
||
| private Optional<MessageConfig> getFromDb(String messageId) { | ||
| try { | ||
| return messageLookupDao.get(messageId) | ||
| .map(config -> | ||
| new MessageConfig(config.getMessageId(), MapperUtils.readTree(config.getMessageConfigBody()))); | ||
| } catch (Exception e) { | ||
| throw StatesmanError.propagate(e, ResponseCode.DAO_ERROR); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public Optional<MessageConfig> create(MessageConfig messageConfig) { | ||
| try { | ||
| return messageLookupDao | ||
| .save(WorkflowUtils.toDao(messageConfig)) | ||
| .map(config -> | ||
| new MessageConfig(config.getMessageId(), MapperUtils.readTree(config.getMessageConfigBody()))); | ||
|
|
||
| } catch (Exception e) { | ||
| throw StatesmanError.propagate(e, ResponseCode.DAO_ERROR); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<MessageConfig> get(String messageConfigId) { | ||
| try { | ||
| return messageConfigCache.get(messageConfigId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how are we pre populating data ? should we fall back to DB if cache miss occurs ? because ideally we don't expect to query for a msg id which doesn't exist. so either msg is present in cache or in DB. or are we pre populating the DB somehow ? in that case how will new writes get cached ?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have this method called getFromDb which is hooked into caffine while building the cache, so any cache miss will lead to a DB call. |
||
| } catch (Exception e) { | ||
| throw StatesmanError.propagate(e, ResponseCode.DAO_ERROR); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package io.appform.statesman.server.dao.message; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import io.appform.statesman.model.MessageConfig; | ||
| import io.appform.statesman.model.exception.ResponseCode; | ||
| import io.appform.statesman.model.exception.StatesmanError; | ||
| import io.appform.statesman.server.utils.MapperUtils; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import javax.inject.Inject; | ||
| import java.util.Optional; | ||
|
|
||
|
|
||
| /** | ||
| * This class is mainly responsible for constructing the message for a given | ||
| * message id,language and state from the config defined | ||
| */ | ||
| @Slf4j | ||
| public class MessageConstructor implements IMessageConstructor { | ||
|
|
||
| private final MessageConfigStoreCommand messageConfigStoreCommand; | ||
| private final String DEFAULT_FILED_NAME = "default"; | ||
|
|
||
| @Inject | ||
| public MessageConstructor(MessageConfigStoreCommand messageConfigStoreCommand) { | ||
| this.messageConfigStoreCommand = messageConfigStoreCommand; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * | ||
| * @param messageId : id for the message config | ||
| * @param language : language in which the message needs to be build | ||
| * @param state : state for which the message is intended | ||
| * Example Json : | ||
| *{ | ||
| * "messageId":"welcome", | ||
| * "messageBody":{ | ||
| * "default":{ | ||
| * "KA":"msg1", | ||
| * "default":"msg2" | ||
| * }, | ||
| * "hindi":{ | ||
| * "RAJ":"msg3", | ||
| * "UP":"msg4", | ||
| * "default":"msg5" | ||
| * }, | ||
| * "marathi":{ | ||
| * "MAH":"msg6", | ||
| * "default":"msg7" | ||
| * } | ||
| * } | ||
| * } | ||
| * @return : Completely constructed message | ||
| */ | ||
| @Override | ||
| public String constructMessage(String messageId, String language, String state) { | ||
|
|
||
| Optional<MessageConfig> optionalMessageConfig = messageConfigStoreCommand.get(messageId); | ||
| if(!optionalMessageConfig.isPresent()){ | ||
| throw new StatesmanError("No message config found", ResponseCode.NO_PROVIDER_FOUND); | ||
| } | ||
|
|
||
| MessageConfig storeOutput = optionalMessageConfig.get(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a comment of the json structure ? will help whoever will read this code in future
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| JsonNode root = storeOutput.getMessageBody(); | ||
| JsonNode languageNode; | ||
|
|
||
| if(root == null){ | ||
| throw new StatesmanError("No message config found", ResponseCode.NO_PROVIDER_FOUND); | ||
| }else if((root.get(language) == null)){ | ||
| if(root.get(DEFAULT_FILED_NAME) == null) { | ||
| throw new StatesmanError("No default language found", ResponseCode.NO_PROVIDER_FOUND); | ||
| }else{ | ||
| languageNode = root.get(DEFAULT_FILED_NAME); | ||
| } | ||
| }else{ | ||
| languageNode = root.get(language); | ||
| } | ||
|
|
||
| if(languageNode.get(state) != null) { | ||
| return languageNode.get(state).asText(); | ||
| }else if(languageNode.get(DEFAULT_FILED_NAME) != null) { | ||
| return languageNode.get(DEFAULT_FILED_NAME).asText(); | ||
| }else{ | ||
| return root.get(DEFAULT_FILED_NAME).get(DEFAULT_FILED_NAME).asText(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package io.appform.statesman.server.dao.message; | ||
|
|
||
| import io.appform.dropwizard.sharding.sharding.LookupKey; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import javax.persistence.*; | ||
|
|
||
|
|
||
| /** | ||
| * Entity that represents the message config that needs to stored and fetched from the DB | ||
| */ | ||
| @Entity | ||
| @Table(name = "message_config", uniqueConstraints = { | ||
| @UniqueConstraint(columnNames = "message_id") | ||
| }) | ||
| @Data | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| @Builder | ||
| public class StoredMessageConfig { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we merge this with MessageConfig ? |
||
|
|
||
| @Id | ||
| @Column(name = "message_id") | ||
| @LookupKey | ||
| private String messageId; | ||
|
|
||
| @Column(name = "message_config_body") | ||
| private byte[] messageConfigBody; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should create return optional ? if it fails, it should throw the exception right rather than fail internally ?