Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);

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 ?


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;
}
6 changes: 6 additions & 0 deletions statesman-server/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,9 @@ CREATE TABLE `workflow_templates` (
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_template_id` (`template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `message_config` (
`message_id` varchar(64) DEFAULT NULL,
`message_config_body` blob DEFAULT NULL,
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does 1_000 mean ?

Copy link
Author

Choose a reason for hiding this comment

The 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);

Choose a reason for hiding this comment

The 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 ?

Copy link
Author

Choose a reason for hiding this comment

The 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();

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The 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 {

Choose a reason for hiding this comment

The 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.appform.dropwizard.sharding.dao.RelationalDao;
import io.appform.statesman.server.dao.action.StoredActionTemplate;
import io.appform.statesman.server.dao.callback.StoredCallbackTransformationTemplate;
import io.appform.statesman.server.dao.message.StoredMessageConfig;
import io.appform.statesman.server.dao.providers.StoredProvider;
import io.appform.statesman.server.dao.transition.StoredStateTransition;
import io.appform.statesman.server.dao.workflow.StoredWorkflowInstance;
Expand All @@ -34,6 +35,12 @@ public LookupDao<StoredActionTemplate> provideActionTemplateLookupDao() {
return dbShardingBundle.createParentObjectDao(StoredActionTemplate.class);
}

@Singleton
@Provides
public LookupDao<StoredMessageConfig> provideMessageLookupDao() {
return dbShardingBundle.createParentObjectDao(StoredMessageConfig.class);
}

@Singleton
@Provides
public LookupDao<StoredWorkflowTemplate> provideWorkflowTemplateLookupDao() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import io.appform.statesman.engine.ActionTemplateStore;
import io.appform.statesman.engine.ProviderSelector;
import io.appform.statesman.engine.TransitionStore;
import io.appform.statesman.engine.WorkflowProvider;
import io.appform.statesman.engine.*;
import io.appform.statesman.engine.action.ActionExecutor;
import io.appform.statesman.engine.action.ActionExecutorImpl;
import io.appform.statesman.engine.action.ActionRegistry;
Expand All @@ -30,6 +27,9 @@
import io.appform.statesman.server.dao.action.ActionTemplateStoreCommand;
import io.appform.statesman.server.dao.callback.CallbackTemplateProvider;
import io.appform.statesman.server.dao.callback.CallbackTemplateProviderCommand;
import io.appform.statesman.server.dao.message.IMessageConstructor;
import io.appform.statesman.server.dao.message.MessageConfigStoreCommand;
import io.appform.statesman.server.dao.message.MessageConstructor;
import io.appform.statesman.server.dao.transition.TransitionStoreCommand;
import io.appform.statesman.server.dao.workflow.WorkflowProviderCommand;
import io.appform.statesman.server.droppedcalldetector.DroppedCallDetector;
Expand All @@ -46,6 +46,7 @@ public class StatesmanModule extends AbstractModule {
@Override
protected void configure() {
bind(ActionTemplateStore.class).to(ActionTemplateStoreCommand.class);
bind(MessageConfigStore.class).to(MessageConfigStoreCommand.class);
bind(TransitionStore.class).to(TransitionStoreCommand.class);
bind(WorkflowProvider.class).to(WorkflowProviderCommand.class);
bind(CallbackTemplateProvider.class).to(CallbackTemplateProviderCommand.class);
Expand All @@ -58,6 +59,7 @@ protected void configure() {
.annotatedWith(Names.named("foxtrotEventSender"))
.to(FoxtrotEventSender.class);
bind(IdExtractor.class).to(CompoundIdExtractor.class);
bind(IMessageConstructor.class).to(MessageConstructor.class);
}

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.appform.statesman.engine.ActionTemplateStore;
import io.appform.statesman.engine.MessageConfigStore;
import io.appform.statesman.engine.TransitionStore;
import io.appform.statesman.engine.WorkflowProvider;
import io.appform.statesman.model.MessageConfig;
import io.appform.statesman.model.StateTransition;
import io.appform.statesman.model.WorkflowTemplate;
import io.appform.statesman.model.action.template.ActionTemplate;
import io.appform.statesman.server.dao.message.IMessageConstructor;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -33,14 +36,20 @@ public class TemplateResource {
private final ActionTemplateStore actionTemplateStore;
private final TransitionStore transitionStore;
private final WorkflowProvider workflowProvider;
private final MessageConfigStore messageConfigStore;
private final IMessageConstructor messageConstrutor;

@Inject
public TemplateResource(final ActionTemplateStore actionTemplateStore,
final TransitionStore transitionStore,
final WorkflowProvider workflowProvider) {
final WorkflowProvider workflowProvider,
final MessageConfigStore messageConfigStore,
final IMessageConstructor messageConstrutor) {
this.actionTemplateStore = actionTemplateStore;
this.transitionStore = transitionStore;
this.workflowProvider = workflowProvider;
this.messageConfigStore = messageConfigStore;
this.messageConstrutor = messageConstrutor;
}


Expand Down Expand Up @@ -222,4 +231,32 @@ public Response updateAction(@Valid ActionTemplate actionTemplate) {
.build();
}

@POST
@Timed
@Path("/messageconfig")
@ApiOperation("Create Message Config")
public Response createMessageConfig(@Valid MessageConfig messageConfig) {
Optional<MessageConfig> messageConfigOptional = messageConfigStore.create(messageConfig);
if (!messageConfigOptional.isPresent()) {
return Response.serverError()
.build();
}
return Response.ok()
.entity(messageConfigOptional.get())
.build();
}

@GET
@Timed
@Path("/messageconfig/{messageId}")
@ApiOperation("Get message config")
public Response getAllStateTransitions(@PathParam("messageId") String messageId,
@DefaultValue("default") @QueryParam("language") String language,
@DefaultValue("default") @QueryParam("state") String state) {

return Response.ok()
.entity(messageConstrutor.constructMessage(messageId,language,state))
.build();
}

}
Loading