diff --git a/statesman-engine/src/main/java/io/appform/statesman/engine/MessageConfigStore.java b/statesman-engine/src/main/java/io/appform/statesman/engine/MessageConfigStore.java new file mode 100644 index 00000000..12a46c11 --- /dev/null +++ b/statesman-engine/src/main/java/io/appform/statesman/engine/MessageConfigStore.java @@ -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 create(MessageConfig messageConfig); + + Optional get(String messageConfigId); +} diff --git a/statesman-model/src/main/java/io/appform/statesman/model/MessageConfig.java b/statesman-model/src/main/java/io/appform/statesman/model/MessageConfig.java new file mode 100644 index 00000000..840fe600 --- /dev/null +++ b/statesman-model/src/main/java/io/appform/statesman/model/MessageConfig.java @@ -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; +} diff --git a/statesman-server/db/schema.sql b/statesman-server/db/schema.sql index c8dd7674..b8ec70e4 100644 --- a/statesman-server/db/schema.sql +++ b/statesman-server/db/schema.sql @@ -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; diff --git a/statesman-server/src/main/java/io/appform/statesman/server/dao/message/IMessageConstructor.java b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/IMessageConstructor.java new file mode 100644 index 00000000..755c40d4 --- /dev/null +++ b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/IMessageConstructor.java @@ -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); +} diff --git a/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConfigStoreCommand.java b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConfigStoreCommand.java new file mode 100644 index 00000000..408b665c --- /dev/null +++ b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConfigStoreCommand.java @@ -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 messageLookupDao; + private final LoadingCache> messageConfigCache; + + @Inject + public MessageConfigStoreCommand(LookupDao messageLookupDao) { + this.messageLookupDao = messageLookupDao; + messageConfigCache = Caffeine.newBuilder() + .maximumSize(1_000) + .expireAfterWrite(120, TimeUnit.MINUTES) + .refreshAfterWrite(15, TimeUnit.MINUTES) + .build(key -> { + log.debug("Loading data for action for key: {}", key); + return getFromDb(key); + }); + } + + private Optional 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 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 get(String messageConfigId) { + try { + return messageConfigCache.get(messageConfigId); + } catch (Exception e) { + throw StatesmanError.propagate(e, ResponseCode.DAO_ERROR); + } + } +} diff --git a/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConstructor.java b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConstructor.java new file mode 100644 index 00000000..d62bc0a4 --- /dev/null +++ b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/MessageConstructor.java @@ -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 optionalMessageConfig = messageConfigStoreCommand.get(messageId); + if(!optionalMessageConfig.isPresent()){ + throw new StatesmanError("No message config found", ResponseCode.NO_PROVIDER_FOUND); + } + + MessageConfig storeOutput = optionalMessageConfig.get(); + 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(); + } + } +} + + diff --git a/statesman-server/src/main/java/io/appform/statesman/server/dao/message/StoredMessageConfig.java b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/StoredMessageConfig.java new file mode 100644 index 00000000..9c97a81d --- /dev/null +++ b/statesman-server/src/main/java/io/appform/statesman/server/dao/message/StoredMessageConfig.java @@ -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 { + + @Id + @Column(name = "message_id") + @LookupKey + private String messageId; + + @Column(name = "message_config_body") + private byte[] messageConfigBody; +} diff --git a/statesman-server/src/main/java/io/appform/statesman/server/module/DBModule.java b/statesman-server/src/main/java/io/appform/statesman/server/module/DBModule.java index 3fbef826..f5d3f8ed 100644 --- a/statesman-server/src/main/java/io/appform/statesman/server/module/DBModule.java +++ b/statesman-server/src/main/java/io/appform/statesman/server/module/DBModule.java @@ -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; @@ -34,6 +35,12 @@ public LookupDao provideActionTemplateLookupDao() { return dbShardingBundle.createParentObjectDao(StoredActionTemplate.class); } + @Singleton + @Provides + public LookupDao provideMessageLookupDao() { + return dbShardingBundle.createParentObjectDao(StoredMessageConfig.class); + } + @Singleton @Provides public LookupDao provideWorkflowTemplateLookupDao() { diff --git a/statesman-server/src/main/java/io/appform/statesman/server/module/StatesmanModule.java b/statesman-server/src/main/java/io/appform/statesman/server/module/StatesmanModule.java index dc423916..1dafc7c9 100644 --- a/statesman-server/src/main/java/io/appform/statesman/server/module/StatesmanModule.java +++ b/statesman-server/src/main/java/io/appform/statesman/server/module/StatesmanModule.java @@ -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; @@ -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; @@ -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); @@ -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 diff --git a/statesman-server/src/main/java/io/appform/statesman/server/resources/TemplateResource.java b/statesman-server/src/main/java/io/appform/statesman/server/resources/TemplateResource.java index 97fa6f97..b967412a 100644 --- a/statesman-server/src/main/java/io/appform/statesman/server/resources/TemplateResource.java +++ b/statesman-server/src/main/java/io/appform/statesman/server/resources/TemplateResource.java @@ -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; @@ -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; } @@ -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 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(); + } + } diff --git a/statesman-server/src/main/java/io/appform/statesman/server/utils/MapperUtils.java b/statesman-server/src/main/java/io/appform/statesman/server/utils/MapperUtils.java index 6a941bb6..4e054598 100644 --- a/statesman-server/src/main/java/io/appform/statesman/server/utils/MapperUtils.java +++ b/statesman-server/src/main/java/io/appform/statesman/server/utils/MapperUtils.java @@ -2,11 +2,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.statesman.model.exception.ResponseCode; import io.appform.statesman.model.exception.StatesmanError; import javax.annotation.Nullable; +import java.io.IOException; public class MapperUtils { private static ObjectMapper objectMapper; @@ -33,6 +35,7 @@ public static T deserialize( byte[] data, TypeReference typeReference) { return deserialize(objectMapper, data, typeReference); } + @Nullable public static T deserialize(ObjectMapper mapper, byte[] data, Class valueType) { try { @@ -109,4 +112,19 @@ public static byte[] serialize(ObjectMapper mapper, Object data) { } } + + @Nullable + public static JsonNode readTree(byte[] data) { + try { + if (data == null) { + return null; + } + return objectMapper.readTree(data); + } catch (JsonProcessingException e) { + throw StatesmanError.propagate(e, ResponseCode.JSON_ERROR); + + } catch (IOException e) { + throw StatesmanError.propagate(e, ResponseCode.JSON_ERROR); + } + } } diff --git a/statesman-server/src/main/java/io/appform/statesman/server/utils/WorkflowUtils.java b/statesman-server/src/main/java/io/appform/statesman/server/utils/WorkflowUtils.java index 79a1d9e4..f56d15d9 100644 --- a/statesman-server/src/main/java/io/appform/statesman/server/utils/WorkflowUtils.java +++ b/statesman-server/src/main/java/io/appform/statesman/server/utils/WorkflowUtils.java @@ -7,11 +7,13 @@ import io.appform.statesman.model.*; import io.appform.statesman.model.action.template.*; import io.appform.statesman.server.dao.action.StoredActionTemplate; +import io.appform.statesman.server.dao.message.StoredMessageConfig; import io.appform.statesman.server.dao.transition.StoredStateTransition; import io.appform.statesman.server.dao.workflow.StoredWorkflowInstance; import io.appform.statesman.server.dao.workflow.StoredWorkflowTemplate; import lombok.extern.slf4j.Slf4j; +import java.io.IOException; import java.util.List; import java.util.UUID; @@ -109,4 +111,15 @@ public static StoredActionTemplate toDao(ActionTemplate actionTemplate) { .data(MapperUtils.serialize(actionTemplate)) .build(); } + + public static StoredMessageConfig toDao(MessageConfig messageConfig) { + try { + return StoredMessageConfig.builder() + .messageId(messageConfig.getMessageId()) + .messageConfigBody(messageConfig.getMessageBody().binaryValue()) + .build(); + } catch (IOException e) { + return null; + } + } }