diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsScheduledConfig.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsScheduledConfig.java deleted file mode 100644 index 833a49b8..00000000 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsScheduledConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.finos.springbot.teams; - -import java.util.concurrent.TimeUnit; - -import org.finos.springbot.teams.handlers.TeamsResponseHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - - -@EnableScheduling -public class TeamsScheduledConfig { - - @Autowired - private TeamsResponseHandler handler; - - @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS) - //Task to run after a fixed delay. - //the duration between the end of last execution and the start of next execution is fixed - public void scheduleRetryMessage() { - handler.retryMessage(); - } -} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java index 49143031..f344ec23 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java @@ -13,8 +13,8 @@ import org.finos.springbot.teams.conversations.TeamsConversationsConfig; import org.finos.springbot.teams.form.TeamsFormConverter; import org.finos.springbot.teams.form.TeamsFormDeserializerModule; -import org.finos.springbot.teams.handlers.InMemoryMessageRetryHandler; -import org.finos.springbot.teams.handlers.MessageRetryHandler; +import org.finos.springbot.teams.handlers.ActivityHandler; +import org.finos.springbot.teams.handlers.SimpleActivityHandler; import org.finos.springbot.teams.handlers.TeamsResponseHandler; import org.finos.springbot.teams.history.StateStorageBasedTeamsHistory; import org.finos.springbot.teams.history.StorageIDResponseHandler; @@ -75,8 +75,7 @@ ThymeleafEngineConfig.class, AdaptiveCardConverterConfig.class, ThymeleafConverterConfig.class, - TeamsConversationsConfig.class, - TeamsScheduledConfig.class + TeamsConversationsConfig.class }) @Profile("teams") public class TeamsWorkflowConfig { @@ -129,16 +128,23 @@ public TeamsResponseHandler teamsResponseHandler( AdaptiveCardTemplateProvider formTemplater, ThymeleafTemplateProvider displayTemplater, TeamsStateStorage th, - TeamsConversations tc, - MessageRetryHandler mr) { + ActivityHandler ah) { return new TeamsResponseHandler( null, // attachment handler markupTemplater, formTemplater, displayTemplater, th, - tc, - mr); + ah); + } + + /** + If you want to include retry logic for activities, override this bean and return an instance of InMemoryRetryingActivityHandler + */ + @Bean + @ConditionalOnMissingBean + public ActivityHandler activityHandler(TeamsConversations tc) { + return new SimpleActivityHandler(tc); } @Bean @@ -230,10 +236,5 @@ public void setResourceLoaderClassLoader() { resourceLoader.setClassLoader(this.getClass().getClassLoader()); } - @Bean - @ConditionalOnMissingBean - public MessageRetryHandler messageRetryHandler() { - return new InMemoryMessageRetryHandler(); - } } \ No newline at end of file diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/ActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/ActivityHandler.java new file mode 100644 index 00000000..f2227429 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/ActivityHandler.java @@ -0,0 +1,13 @@ +package org.finos.springbot.teams.handlers; + +import java.util.concurrent.CompletableFuture; + +import org.finos.springbot.teams.content.TeamsAddressable; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +public interface ActivityHandler { + + public CompletableFuture handleActivity(Activity activity, TeamsAddressable to); +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/InMemoryMessageRetryHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/InMemoryMessageRetryHandler.java deleted file mode 100644 index e275f01d..00000000 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/InMemoryMessageRetryHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.finos.springbot.teams.handlers; - -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class InMemoryMessageRetryHandler implements MessageRetryHandler { - - private static final Logger LOG = LoggerFactory.getLogger(InMemoryMessageRetryHandler.class); - - Queue queue = new ConcurrentLinkedQueue<>(); - - @Override - public void add(MessageRetry t) { - queue.add(t); - } - - @Override - public Optional get() { - MessageRetry q; - if ((q = queue.peek()) != null) { - LocalDateTime time = q.getCurrentTime().plusSeconds(q.getRetryAfter()); - if (LocalDateTime.now().isAfter(time)) { // retry now - queue.remove(q); - return Optional.of(q); - }else { - LOG.info("Message not retied, as retry after time {} is not getter than or equal to current time {}", time, LocalDateTime.now()); - } - } - - return Optional.empty(); - } - -} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetry.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetry.java deleted file mode 100644 index d30a775f..00000000 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetry.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.finos.springbot.teams.handlers; - -import java.time.LocalDateTime; - -import org.finos.springbot.workflow.response.Response; - -public class MessageRetry { - - private Response response; - private int retryCount; - private int retryAfter; - private LocalDateTime currentTime; - - public MessageRetry(Response response, int retryCount, int retryAfter) { - super(); - this.response = response; - this.retryCount = retryCount; - this.retryAfter = retryAfter; - currentTime = LocalDateTime.now(); - } - - public Response getResponse() { - return response; - } - - public void setResponse(Response response) { - this.response = response; - } - - public int getRetryCount() { - return retryCount; - } - - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } - - public int getRetryAfter() { - return retryAfter; - } - - public void setRetryAfter(int retryAfter) { - this.retryAfter = retryAfter; - } - - - public LocalDateTime getCurrentTime() { - return currentTime; - } - - public void setCurrentTime(LocalDateTime currentTime) { - this.currentTime = currentTime; - } - - @Override - public String toString() { - return "MessageRetry [response=" + response + ", retryCount=" + retryCount + ", retryAfter=" + retryAfter - + ", localDate=" + currentTime + "]"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((currentTime == null) ? 0 : currentTime.hashCode()); - result = prime * result + ((response == null) ? 0 : response.hashCode()); - result = prime * result + retryAfter; - result = prime * result + retryCount; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - MessageRetry other = (MessageRetry) obj; - if (currentTime == null) { - if (other.currentTime != null) - return false; - } else if (!currentTime.equals(other.currentTime)) - return false; - if (response == null) { - if (other.response != null) - return false; - } else if (!response.equals(other.response)) - return false; - if (retryAfter != other.retryAfter) - return false; - if (retryCount != other.retryCount) - return false; - return true; - } - - -} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetryHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetryHandler.java deleted file mode 100644 index a1718b36..00000000 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/MessageRetryHandler.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.finos.springbot.teams.handlers; - -import java.util.Optional; - -public interface MessageRetryHandler { - - public void add(MessageRetry t); - - public Optional get(); - -} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleActivityHandler.java new file mode 100644 index 00000000..6f32ee14 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleActivityHandler.java @@ -0,0 +1,24 @@ +package org.finos.springbot.teams.handlers; + +import java.util.concurrent.CompletableFuture; + +import org.finos.springbot.teams.content.TeamsAddressable; +import org.finos.springbot.teams.conversations.TeamsConversations; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +public class SimpleActivityHandler implements ActivityHandler { + + private TeamsConversations tc; + + public SimpleActivityHandler(TeamsConversations tc) { + this.tc = tc; + } + + @Override + public CompletableFuture handleActivity(Activity activity, TeamsAddressable to) { + return tc.handleActivity(activity, to); + } + +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java index 2a8ed171..ebc15b4d 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java @@ -3,20 +3,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.BiFunction; -import org.apache.commons.lang3.StringUtils; import org.finos.springbot.teams.TeamsException; import org.finos.springbot.teams.content.TeamsAddressable; -import org.finos.springbot.teams.conversations.TeamsConversations; import org.finos.springbot.teams.history.StorageIDResponseHandler; import org.finos.springbot.teams.history.TeamsHistory; import org.finos.springbot.teams.response.templating.EntityMarkupTemplateProvider; import org.finos.springbot.teams.response.templating.MarkupAndEntities; import org.finos.springbot.teams.state.TeamsStateStorage; +import org.finos.springbot.teams.templating.adaptivecard.AdaptiveCardPassthrough; import org.finos.springbot.teams.templating.adaptivecard.AdaptiveCardTemplateProvider; import org.finos.springbot.teams.templating.thymeleaf.ThymeleafTemplateProvider; import org.finos.springbot.workflow.annotations.WorkMode; @@ -31,7 +29,6 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.http.HttpStatus; import org.springframework.util.ErrorHandler; import com.fasterxml.jackson.core.JsonProcessingException; @@ -45,14 +42,12 @@ import com.microsoft.bot.schema.ResourceResponse; import com.microsoft.bot.schema.TextFormatTypes; -import okhttp3.ResponseBody; - public class TeamsResponseHandler implements ResponseHandler, ApplicationContextAware { private static final Logger LOG = LoggerFactory.getLogger(TeamsResponseHandler.class); - private static final int RETRY_COUNT = 3; - private static final int INIT_RETRY_COUNT = 0; + + protected AttachmentHandler attachmentHandler; protected ApplicationContext ctx; @@ -61,8 +56,7 @@ public class TeamsResponseHandler implements ResponseHandler, ApplicationContext protected AdaptiveCardTemplateProvider workTemplater; protected ThymeleafTemplateProvider displayTemplater; protected TeamsStateStorage teamsState; - protected TeamsConversations teamsConversations; - protected MessageRetryHandler messageRetryHandler; + protected ActivityHandler ah; public TeamsResponseHandler( AttachmentHandler attachmentHandler, @@ -70,15 +64,13 @@ public TeamsResponseHandler( AdaptiveCardTemplateProvider workTemplater, ThymeleafTemplateProvider displayTemplater, TeamsStateStorage th, - TeamsConversations tc, - MessageRetryHandler mr) { + ActivityHandler ah) { this.attachmentHandler = attachmentHandler; this.messageTemplater = messageTemplater; this.workTemplater = workTemplater; this.displayTemplater = displayTemplater; this.teamsState = th; - this.teamsConversations = tc; - this.messageRetryHandler = mr; + this.ah = ah; } protected void initErrorHandler() { @@ -91,10 +83,6 @@ enum TemplateType { ADAPTIVE_CARD, THYMELEAF, BOTH }; @Override public void accept(Response t) { - sendResponse(t, INIT_RETRY_COUNT); - } - - private void sendResponse(Response t, int retryCount) { if (t.getAddress() instanceof TeamsAddressable) { TeamsAddressable ta = (TeamsAddressable) t.getAddress(); @@ -111,7 +99,7 @@ private void sendResponse(Response t, int retryCount) { } sendXMLResponse(content, attachment, ta, entities, mr.getData()) - .handle(handleErrorAndStorage(content, ta, mr.getData(), t, ++retryCount)); + .handle(handleErrorAndStorage(content, ta, mr.getData(), t)); } else if (t instanceof WorkResponse) { WorkResponse wr = (WorkResponse) t; @@ -120,15 +108,14 @@ private void sendResponse(Response t, int retryCount) { if (tt == TemplateType.ADAPTIVE_CARD) { JsonNode cardJson = workTemplater.template(wr); sendCardResponse(cardJson, ta, wr.getData()) - .handle(handleErrorAndStorage(cardJson, ta, wr.getData(), t, ++retryCount)); - ; + .handle(handleErrorAndStorage(cardJson, ta, wr.getData(), t)); } else { MarkupAndEntities mae = displayTemplater.template(wr); String content = mae.getContents(); List entities = mae.getEntities(); sendXMLResponse(content, null, ta, entities, wr.getData()) .handle(handleButtonsIfNeeded(tt, wr)) - .handle(handleErrorAndStorage(content, ta, wr.getData(), t, ++retryCount)); + .handle(handleErrorAndStorage(content, ta, wr.getData(), t)); } } @@ -138,11 +125,12 @@ private void sendResponse(Response t, int retryCount) { } } + protected TemplateType getTemplateType(WorkResponse wr) { TemplateType tt; if (displayTemplater.hasTemplate(wr)) { tt = TemplateType.THYMELEAF; - } else if (workTemplater.hasTemplate(wr)) { + } else if (workTemplater.hasTemplate(wr) || AdaptiveCardPassthrough.isAdaptiveCard(wr)) { tt = TemplateType.ADAPTIVE_CARD; } else if (wr.getMode() == WorkMode.EDIT) { tt = TemplateType.ADAPTIVE_CARD; @@ -160,7 +148,7 @@ protected CompletableFuture sendXMLResponse(String xml, Object out.setEntities(entities); out.setTextFormat(TextFormatTypes.XML); out.setText(xml); - return teamsConversations.handleActivity(out, address); + return ah.handleActivity(out, address); } private BiFunction handleButtonsIfNeeded(TemplateType tt, WorkResponse wr) { @@ -174,7 +162,7 @@ private BiFunction handle JsonNode expandedJson = workTemplater.applyTemplate(buttonsJson, wr); return sendCardResponse(expandedJson, (TeamsAddressable) wr.getAddress(), wr.getData()).get(); } else { - return null; + return rr; } } else { @@ -191,34 +179,10 @@ private BiFunction handle }; } - private boolean insertIntoQueue(Response t, int retryCount, Throwable e) { - if (e instanceof CompletionException - && ((CompletionException) e).getCause() instanceof ErrorResponseException) { - ErrorResponseException ere = (ErrorResponseException) ((CompletionException) e).getCause(); - retrofit2.Response response = ere.response(); - if (response.code() == HttpStatus.TOO_MANY_REQUESTS.value() && retryCount <= RETRY_COUNT) { - String retryAfter = response.headers().get("Retry-After"); - - int retryAfterInt = 1;//initiate to 1 sec - if(StringUtils.isNumeric(retryAfter)) { - retryAfterInt = Integer.parseInt(retryAfter); - } - - messageRetryHandler.add(new MessageRetry(t, retryCount, retryAfterInt)); - - return true; - } - } - - return false; - } - - private BiFunction handleErrorAndStorage(Object out, TeamsAddressable address, Map data, Response t, int retryCount) { + private BiFunction handleErrorAndStorage(Object out, TeamsAddressable address, Map data, Response t) { return (rr, e) -> { - if (e != null) { - boolean success = insertIntoQueue(t, retryCount, e); - if(!success) { - LOG.error(e.getMessage()); + if (e != null) { + LOG.error("Error message for stream id {} , message: {} ", address.getKey() , e.getMessage()); if (out instanceof ObjectNode){ try { LOG.error("json:\n"+new ObjectMapper().writeValueAsString(out)); @@ -229,23 +193,22 @@ private BiFunction handle } initErrorHandler(); - eh.handleError(e); - } - } else { + eh.handleError(e); + } else if(rr != null) { performStorage(address, data, teamsState); } return null; }; } - + protected CompletableFuture sendCardResponse(JsonNode json, TeamsAddressable address, Map data) throws Exception { Activity out = Activity.createMessageActivity(); Attachment body = new Attachment(); body.setContentType("application/vnd.microsoft.card.adaptive"); body.setContent(json); out.getAttachments().add(body); - return teamsConversations.handleActivity(out, address); + return ah.handleActivity(out, address); } public static void performStorage(TeamsAddressable address, Map data, TeamsStateStorage teamsState) { @@ -270,17 +233,6 @@ public static Map createStorageTags(Map data, Te return out; } - public void retryMessage() { - int messageCount = 0; - - Optional opt; - while ((opt = messageRetryHandler.get()).isPresent()) { - messageCount++; - this.sendResponse(opt.get().getResponse(), opt.get().getRetryCount()); - } - - LOG.info("Retry message queue {}" , messageCount == 0 ? "is empty" : "has messages, count: " + messageCount); - } @Override diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/AbstractRetryingActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/AbstractRetryingActivityHandler.java new file mode 100644 index 00000000..77710205 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/AbstractRetryingActivityHandler.java @@ -0,0 +1,63 @@ +package org.finos.springbot.teams.handlers.retry; + + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import org.apache.commons.lang3.StringUtils; +import org.finos.springbot.teams.conversations.TeamsConversations; +import org.finos.springbot.teams.handlers.ActivityHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import com.microsoft.bot.connector.rest.ErrorResponseException; + +import okhttp3.ResponseBody; + +public abstract class AbstractRetryingActivityHandler implements ActivityHandler { + + protected static final Logger LOG = LoggerFactory.getLogger(AbstractRetryingActivityHandler.class); + + @Value("${teams.retry.count:3}") + protected long teamsRetryCount = 3; + + protected TeamsConversations tc; + + public AbstractRetryingActivityHandler(TeamsConversations tc) { + this.tc = tc; + } + + protected boolean isTooManyRequest(Throwable e) { + if (e instanceof CompletionException + && ((CompletionException) e).getCause() instanceof ErrorResponseException) { + ErrorResponseException ere = (ErrorResponseException) ((CompletionException) e).getCause(); + retrofit2.Response response = ere.response(); + return (response.code() == HttpStatus.TOO_MANY_REQUESTS.value()); + } else { + return false; + } + } + + protected int getRetryAfter(CompletionException e) { + ErrorResponseException ere = (ErrorResponseException) ((CompletionException) e).getCause(); + retrofit2.Response response = ere.response(); + String retryAfter = response.headers().get("Retry-After"); + + int retryAfterInt = 1;// initiate to 1 sec + if (StringUtils.isNumeric(retryAfter)) { + retryAfterInt = Integer.parseInt(retryAfter); + } + + return retryAfterInt; + } + + protected static CompletableFuture failed(Throwable error) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(error); + return future; + } + + +} \ No newline at end of file diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandler.java new file mode 100644 index 00000000..6467390a --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandler.java @@ -0,0 +1,49 @@ +package org.finos.springbot.teams.handlers.retry; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.finos.springbot.teams.content.TeamsAddressable; +import org.finos.springbot.teams.conversations.TeamsConversations; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +public class InMemoryRetryingActivityHandler extends AbstractRetryingActivityHandler { + + static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(10); + + public InMemoryRetryingActivityHandler(TeamsConversations tc) { + super(tc); + } + + @Override + public CompletableFuture handleActivity(Activity activity, TeamsAddressable to) { + CompletableFuture f = tc.handleActivity(activity, to); + for (int i = 0; i < teamsRetryCount; i++) { + f = f.thenApply(CompletableFuture::completedFuture).exceptionally(t -> { + if (isTooManyRequest(t)) { + int ra = getRetryAfter((CompletionException) t); + Executor afterRetryTime = createDelayedExecutor(ra, TimeUnit.SECONDS); + return CompletableFuture.supplyAsync(() -> null, afterRetryTime) + .thenCompose(m -> tc.handleActivity(activity, to)); + } else { + return failed(t); + } + + }).thenCompose(Function.identity()); + } + return f; + } + + + private Executor createDelayedExecutor(long delay, TimeUnit unit) { + return r -> SCHEDULER.schedule(r, delay, unit); + } + +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/FileStateStorage.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/FileStateStorage.java index c386fce9..7e5c1279 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/FileStateStorage.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/FileStateStorage.java @@ -33,8 +33,6 @@ public class FileStateStorage extends AbstractStateStorage { - //protected static final Logger LOG = LoggerFactory.getLogger(FileStateStorage.class); - final static String DATA_FOLDER = "data"; final static String TAG_INDEX_FOLDER = "tag_index"; final static String FILE_EXT = ".txt"; @@ -53,17 +51,16 @@ public FileStateStorage(EntityJsonConverter ejc, String filePath) { @Override public void store(String file, Map tags, Map data) { - if ((tags != null) && (tags.size() > 0)) { + String addressable = getAddressable(file); String storageId = getStorage(file); try { + Path path = checkAndCreateFolder(this.filePath + addressable); - Path dataPath = checkAndCreateFolder(path.toString() + File.separator + DATA_FOLDER); - Path tagPath = checkAndCreateFolder(path.toString() + File.separator + TAG_INDEX_FOLDER); - + Path dataPath = checkAndCreateFolder(path.toString() + File.separator + DATA_FOLDER); + Path tagPath = checkAndCreateFolder(path.toString() + File.separator + TAG_INDEX_FOLDER); createStorageFile(dataPath, storageId, data); - createTagIndexFile(tags, storageId, tagPath); } catch (IOException e) { throw new TeamsException("Error while creating or getting folder " + e); @@ -100,6 +97,7 @@ private void createTagIndexFile(Map tags, String storageId, Path @Override public Optional> retrieve(String file) { String addressable = getAddressable(file); + String storageId = getStorage(file); Optional optData = readFile( @@ -108,15 +106,16 @@ public Optional> retrieve(String file) { if (optData.isPresent()) { return Optional.ofNullable(ejc.readValue(optData.get())); } else { + return Optional.empty(); } } @Override public Iterable> retrieve(List tags, boolean singleResultOnly) { - List tagFolders = getAllTagIndex(tags);// get all tag_index files - List files = extractDataFileName(tagFolders); + List tagFiles = getAllTagIndex(tags); // get all tag_index files + List dataFiles = getDataFiles(tagFiles); Optional ftOpt = tags.stream() .filter(t -> t.key.equals(StateStorageBasedTeamsConversations.ADDRESSABLE_INFO) @@ -125,27 +124,40 @@ public Iterable> retrieve(List tags, boolean singleR // if search for addressable-info like room information or user information if (ftOpt.isPresent()) { - - List> out = files.stream() + List> out = dataFiles.stream() .map(f -> ejc.readValue(readFile(f.getAbsolutePath()).orElse(""))) .filter(filterAddressableTypeFiles()).collect(Collectors.toList()); - if ((singleResultOnly) && (files.size() > 0)) { + if ((singleResultOnly) && (dataFiles.size() > 0)) { out = out.subList(0, 1); } return out; } else { - if ((singleResultOnly) && (files.size() > 0)) { - files = files.subList(0, 1); + if ((singleResultOnly) && (dataFiles.size() > 0)) { + dataFiles = dataFiles.subList(0, 1); } - return files.stream().map(f -> ejc.readValue(readFile(f.getAbsolutePath()).orElse(""))) + return dataFiles.stream().map(f -> ejc.readValue(readFile(f.getAbsolutePath()).orElse(""))) .collect(Collectors.toList()); } } - private List extractDataFileName(List tagFolders) { - return tagFolders.stream().map(t -> getDataFiles(t)).flatMap(f -> f.stream()).distinct() - .collect(Collectors.toList()); + private List tagIndexFileIntersection(List tagFolders) { + List intersection = new ArrayList<>(); + for (int i = 0; i < tagFolders.size(); ++i) { + List tagFolderInsideFiles = getTagIndexFiles(tagFolders.get(i)); + if (i == 0) { + intersection.addAll(tagFolderInsideFiles); + } else { + intersection = intersection.stream().filter(containsTagFile(tagFolderInsideFiles)) + .collect(Collectors.toList()); + } + + } + return intersection; + } + + private Predicate containsTagFile(List files) { + return f -> files.stream().filter(file -> file.getName().equals(f.getName())).findAny().isPresent(); } private List getAllTagIndex(List filter) { @@ -180,7 +192,6 @@ private void getAllTagIndex(File node, List fileList, List tags) { } } - /** * Get tag-index files for given tag * @@ -192,45 +203,44 @@ private List filteredTagsFiles(List tags, File tagFolder) { List subNote = Arrays.asList(tagFolder.list()); Map tagMap = tags.stream().collect(Collectors.toMap(f -> getAzurePath(f.key), f -> f)); - List files = subNote.stream().map(s -> { + List tagFiles = subNote.stream().map(s -> { Filter value = tagMap.get(s); if (!Objects.isNull(value)) { File file = new File(tagFolder, getAzurePath(value.key)); List fileList = Arrays.asList(file.list()); - for (String n : fileList) { - List subSubNote = Arrays.asList(n); - List list = checkEntity(value, subSubNote); - if (!list.isEmpty()) { + for (String name : fileList) { + boolean check = checkEntity(value, name); + if (check) { tagMap.remove(s); - return list.stream().map(l -> new File(file, l)).collect(Collectors.toList()); + return new File(file, name); } } } return null; - }).filter(f -> !Objects.isNull(f)).flatMap(f -> f.stream()).collect(Collectors.toList()); + }).filter(Objects::nonNull).collect(Collectors.toList()); + + tagFiles = tagIndexFileIntersection(tagFiles); if (tagMap.isEmpty()) { - return files; + return tagFiles; } else { return Collections.emptyList(); } } - private List checkEntity(Filter f, List subSubNote) { - return subSubNote.stream().filter(s -> { - int cmp = getAzurePath(f.value).compareTo(s); + private boolean checkEntity(Filter f, String name) { + int cmp = getAzurePath(f.value).compareTo(name); - if (f.operator.contains("=") && (cmp == 0)) { - return true; - } - if (f.operator.contains(">") && (cmp < 0)) { - return true; - } - if (f.operator.contains("<") && (cmp > 0)) { - return true; - } - return false; - }).collect(Collectors.toList()); + if (f.operator.contains("=") && (cmp == 0)) { + return true; + } + if (f.operator.contains(">") && (cmp < 0)) { + return true; + } + if (f.operator.contains("<") && (cmp > 0)) { + return true; + } + return false; } private Predicate filterAddressableTypeFiles() { @@ -252,29 +262,29 @@ private Predicate filterAddressableTypeFiles() { * @param tagPath * @return */ - - private List getDataFiles(File tagPath) { - - String addressableId = tagPath.getParentFile().getParentFile().getParentFile().getName(); + private List getTagIndexFiles(File tagPath) { try (Stream stream = Files.list(tagPath.toPath())) { Set paths = stream.filter(file -> !Files.isDirectory(file)).collect(Collectors.toSet()); - List files = paths.stream().map(f -> new File(f.toString())) + return paths.stream().map(f -> new File(f.toString())) .sorted(Collections.reverseOrder(Comparator.comparingLong(File::lastModified))) .collect(Collectors.toList()); - - List list = files.stream().map(f -> new File(this.filePath + File.separator + addressableId - + File.separator + DATA_FOLDER + File.separator + f.getName())).collect(Collectors.toList()); - - return list; - } catch (IOException e) { throw new TeamsException("Error while retriving data files " + e); } } + private List getDataFiles(List tagFiles) { + return tagFiles.stream().map(f -> { + String addressableId = f.getParentFile().getParentFile().getParentFile().getParentFile().getName(); + return new File(this.filePath + File.separator + addressableId + File.separator + DATA_FOLDER + + File.separator + f.getName()); + }).collect(Collectors.toList()); + + } + private String getAddressable(String file) { Optional> split = splitString(file); if (split.isPresent()) { @@ -294,13 +304,18 @@ private String getStorage(String file) { private Optional> splitString(String s) { if (s.contains("/")) { String[] data = s.split("/"); - if (data.length > 1) { + if (data.length == 0) { + return Optional.empty(); + } else if (data.length == 2) { return Optional.of(Arrays.asList(data)); + } else { + throw new UnsupportedOperationException("Can't handle multiple paths with file state storage: "+s); } } return Optional.empty(); } - + + private String getAzureTag(String s) { return s.replaceAll("[^0-9a-zA-Z]", "_"); } @@ -319,6 +334,5 @@ private String getAzurePath(String s) { } return path; } - } diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardPassthrough.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardPassthrough.java new file mode 100644 index 00000000..40f6278c --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardPassthrough.java @@ -0,0 +1,33 @@ +package org.finos.springbot.teams.templating.adaptivecard; + +import org.finos.springbot.workflow.response.WorkResponse; + +import com.fasterxml.jackson.databind.JsonNode; + +public class AdaptiveCardPassthrough { + + private final static String ADAPTIVE_CARD = "AdaptiveCard"; + private final static String ADAPTIVE_CARD_TYPE = "type"; + + private final JsonNode jsonNode; + + public AdaptiveCardPassthrough(JsonNode jsonNode) { + this.jsonNode = jsonNode; + } + + public JsonNode getJsonNode() { + return jsonNode; + } + + public static boolean isAdaptiveCard(WorkResponse wr) { + if (wr.getFormObject() instanceof AdaptiveCardPassthrough) { + AdaptiveCardPassthrough passthrough = (AdaptiveCardPassthrough) wr.getFormObject(); + JsonNode node = passthrough.getJsonNode(); + JsonNode adaptiveNode = node.get(ADAPTIVE_CARD_TYPE); + return adaptiveNode.asText().equals(ADAPTIVE_CARD); + } + + return false; + } + +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardTemplateProvider.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardTemplateProvider.java index 7ca0b16b..ba8ed4a6 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardTemplateProvider.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/templating/adaptivecard/AdaptiveCardTemplateProvider.java @@ -8,6 +8,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.finos.springbot.teams.handlers.TeamsResponseHandler; import org.finos.springbot.teams.templating.MatcherUtil; import org.finos.springbot.workflow.annotations.WorkMode; import org.finos.springbot.workflow.form.ButtonList; @@ -15,6 +16,8 @@ import org.finos.springbot.workflow.response.templating.AbstractResourceTemplateProvider; import org.finos.springbot.workflow.templating.Mode; import org.finos.springbot.workflow.templating.WorkTemplater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.ResourceLoader; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -27,6 +30,8 @@ public class AdaptiveCardTemplateProvider extends AbstractResourceTemplateProvider { + private static final Logger LOG = LoggerFactory.getLogger(AdaptiveCardTemplateProvider.class); + public static final String FORMID_KEY = "formid"; private final WorkTemplater formConverter; @@ -58,6 +63,12 @@ public JsonNode template(WorkResponse t) { // in this case, just provide the button template return formConverter.convert(null, Mode.DISPLAY_WITH_BUTTONS); + } else if (AdaptiveCardPassthrough.isAdaptiveCard(t)) { + AdaptiveCardPassthrough passthrough = (AdaptiveCardPassthrough) t.getFormObject(); + JsonNode template = passthrough.getJsonNode(); + LOG.info("JsonNode Template: \n" + template.toPrettyString()); + + return template; } else { return super.template(t); } @@ -65,7 +76,6 @@ public JsonNode template(WorkResponse t) { - @Override protected JsonNode getDefaultTemplate(WorkResponse r) { JsonNode insert; diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandlerTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandlerTest.java new file mode 100644 index 00000000..16a750dd --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/InMemoryRetryingActivityHandlerTest.java @@ -0,0 +1,136 @@ +package org.finos.springbot.teams.handlers.retry; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.finos.springbot.teams.content.TeamsChannel; +import org.finos.springbot.teams.conversations.TeamsConversations; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.microsoft.bot.connector.rest.ErrorResponseException; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.ResponseBody; +import retrofit2.Response; + +@ActiveProfiles("teams") +@ExtendWith(SpringExtension.class) +public class InMemoryRetryingActivityHandlerTest { + + @MockBean + TeamsConversations conv; + + int go = 0; + int passEvery = 3; + + Set allUsedThreads = new HashSet<>(); + + @BeforeEach + public void mockSetup() { + go = 0; + allUsedThreads.clear(); + ArgumentCaptor msg = ArgumentCaptor.forClass(Activity.class); + + Mockito.when(conv.handleActivity(msg.capture(), Mockito.any())).thenAnswer(a -> { + allUsedThreads.add(Thread.currentThread()); + System.out.println("Trying "+go+" with thread "+Thread.currentThread()); + ResourceResponse arg1 = new ResourceResponse("done"); + + if (go % passEvery != (passEvery -1)) { + // fails + ResponseBody out = ResponseBody.create("{}", MediaType.get("application/json")); + Response r = Response.error(out, + new okhttp3.Response.Builder() + .code(HttpStatus.TOO_MANY_REQUESTS.value()) + .message("Response.error()").addHeader("Retry-After", "2") + .protocol(Protocol.HTTP_1_1) + .request(new Request.Builder().url("http://localhost/").build()).build()); + go++; + return failed(new ErrorResponseException("Failed", r)); + } else { + go++; + return CompletableFuture.completedFuture(arg1); + } + }); + } + + public static CompletableFuture failed(Throwable error) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(error); + return future; + } + + TeamsChannel dummyChat1 = new TeamsChannel("dummy_id_1", "dummy_name"); + + @Test + public void testRetryWorks() throws InterruptedException, ExecutionException { + long now = System.currentTimeMillis(); + + InMemoryRetryingActivityHandler retry = new InMemoryRetryingActivityHandler(conv); + + CompletableFuture cf = retry.handleActivity(new Activity("dummy"), dummyChat1); + + ResourceResponse rr = cf.get(); + + Assertions.assertEquals("done", rr.getId()); + long doneTime = System.currentTimeMillis(); + + Assertions.assertTrue(doneTime - now > 2000); + Assertions.assertEquals(3, go); + + } + + @Test + public void testRetryGivesUp() throws InterruptedException, ExecutionException { + Assertions.assertThrows(Exception.class, () -> { + passEvery = 1000; // never succeeds + long now = System.currentTimeMillis(); + + InMemoryRetryingActivityHandler retry = new InMemoryRetryingActivityHandler(conv); + + CompletableFuture cf = retry.handleActivity(new Activity("dummy"), dummyChat1); + + ResourceResponse rr = cf.get(); + }); + } + + @Test + public void testWorksFirstTime() throws InterruptedException, ExecutionException { + passEvery = 1; // always succeeds + long now = System.currentTimeMillis(); + + InMemoryRetryingActivityHandler retry = new InMemoryRetryingActivityHandler(conv); + + CompletableFuture cf = retry.handleActivity(new Activity("dummy"), dummyChat1); + + ResourceResponse rr = cf.get(); + Assertions.assertEquals("done", rr.getId()); + } + + @Test + public void testMultipleTimes() throws Exception { + for (int i = 0; i <5; i++) { + InMemoryRetryingActivityHandler retry = new InMemoryRetryingActivityHandler(conv); + CompletableFuture cf = retry.handleActivity(new Activity("dummy"), dummyChat1); + ResourceResponse rr = cf.get(); + } + + System.out.println(allUsedThreads.size()); + } + +} \ No newline at end of file diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/SimpleActivityHandlerTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/SimpleActivityHandlerTest.java new file mode 100644 index 00000000..e56f26b4 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/handlers/retry/SimpleActivityHandlerTest.java @@ -0,0 +1,31 @@ +package org.finos.springbot.teams.handlers.retry; + +import org.finos.springbot.teams.content.TeamsChannel; +import org.finos.springbot.teams.conversations.TeamsConversations; +import org.finos.springbot.teams.handlers.SimpleActivityHandler; +import org.finos.springbot.workflow.data.DataHandlerConfig; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; + +import com.microsoft.bot.schema.Activity; + +@SpringBootTest(classes = { + DataHandlerConfig.class, + }) +public class SimpleActivityHandlerTest { + + @Mock + TeamsConversations tc; + + @InjectMocks + private SimpleActivityHandler handler = new SimpleActivityHandler(tc); + + @Test + public void testHandleActivity() { + Activity activity = Mockito.mock(Activity.class); + handler.handleActivity(activity , new TeamsChannel()); + } +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AbstractStateStorageTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AbstractStateStorageTest.java index 42fd5a61..a2f48f17 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AbstractStateStorageTest.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AbstractStateStorageTest.java @@ -72,6 +72,40 @@ public void testStoreWithTagDates() throws IOException { Assertions.assertEquals(2, hoover(tss.retrieve(tagList4, false)).size()); } + @Test + public void testSlashStoreWithMultipleDirectories() throws IOException { + Map somedata = Collections.singletonMap("a", "b"); + Map tagsForTheFile = new HashMap(); + tagsForTheFile.put("addressable", "one"); + tagsForTheFile.put("object1", "tag"); + + Map tagsForTheFileA = new HashMap(); + tagsForTheFileA.put("addressable", "one"); + tagsForTheFileA.put("object2", "tag"); + + Map tagsForTheFileB = new HashMap(); + tagsForTheFileB.put("addressable", "two"); + tagsForTheFileB.put("object2", "tag"); + + List tagList1 = Arrays.asList( + new Filter("addressable", "one", "="), + new Filter("object1", "tag", "=") + ); + + List tagList2 = Arrays.asList( + new Filter("addressable", "two", "="), + new Filter("object2", "tag", "=") + ); + + tss.store("thefile", tagsForTheFile, somedata); + //tss.store("thefile/thefile", tagsForTheFile, somedata); // this won't work + tss.store("thefile/a", tagsForTheFileA, somedata); + tss.store("thefile/b", tagsForTheFileB, somedata); + + Assertions.assertEquals(1, hoover(tss.retrieve(tagList1, false)).size()); + Assertions.assertEquals(1, hoover(tss.retrieve(tagList2, false)).size()); + } + public List> hoover(Iterable> iterable) { List> result = StreamSupport.stream(iterable.spliterator(), false) .collect(Collectors.toList()); diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AzureBlobStateStorageTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AzureBlobStateStorageTest.java index 47a2a072..14c50680 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AzureBlobStateStorageTest.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/AzureBlobStateStorageTest.java @@ -58,6 +58,8 @@ public void setup() { BlobClient theFileBlobClient; BlobClient theOtherFileBlobClient; BlobClient nonFileBlobClient; + BlobClient theFileABlobClient; + BlobClient theFileBBlobClient; private BlobServiceClient createBlobServiceClientMock() { bsc = Mockito.mock(BlobServiceClient.class); @@ -65,7 +67,8 @@ private BlobServiceClient createBlobServiceClientMock() { theFileBlobClient = Mockito.mock(BlobClient.class); theOtherFileBlobClient = Mockito.mock(BlobClient.class); nonFileBlobClient = Mockito.mock(BlobClient.class); - + theFileABlobClient = Mockito.mock(BlobClient.class); + theFileBBlobClient = Mockito.mock(BlobClient.class); Mockito.when(bsc.getBlobContainerClient("test")) .thenReturn(bcc); @@ -75,7 +78,8 @@ private BlobServiceClient createBlobServiceClientMock() { Mockito.when(bcc.getBlobClient("thefile")).thenReturn(theFileBlobClient); Mockito.when(bcc.getBlobClient("theotherfile")).thenReturn(theOtherFileBlobClient); Mockito.when(bcc.getBlobClient("nonfile")).thenReturn(nonFileBlobClient); - + Mockito.when(bcc.getBlobClient("thefile/a")).thenReturn(theFileABlobClient); + Mockito.when(bcc.getBlobClient("thefile/b")).thenReturn(theFileBBlobClient); return bsc; } @@ -118,6 +122,15 @@ private void allowReadingFromTheFile() throws IOException { private void allowReadingFromTheOtherFile() throws IOException { Mockito.when(theOtherFileBlobClient.openInputStream()).thenAnswer((a) -> setupBlobInputStream()); } + + private void allowReadingFromTheFileA() throws IOException { + Mockito.when(theFileABlobClient.openInputStream()).thenAnswer((a) -> setupBlobInputStream()); + } + + private void allowReadingFromTheFileB() throws IOException { + Mockito.when(theFileBBlobClient.openInputStream()).thenAnswer((a) -> setupBlobInputStream()); + } + private BlobInputStream setupBlobInputStream() throws IOException { BlobInputStream bis = Mockito.mock(BlobInputStream.class); @@ -144,6 +157,16 @@ private void allowWritingToTheOtherFile() { mockBlobOutputStream(theOtherFileBlobClient); } + private void allowWritingToTheSlashFile() { + mockBlobOutputStream(theFileABlobClient); + } + + private void allowWritingToTheOtherSlashFile() { + mockBlobOutputStream(theFileBBlobClient); + } + + + private void mockBlobOutputStream(BlobClient bc) { Mockito.doAnswer(new Answer() { public Void answer(InvocationOnMock a) { @@ -176,6 +199,21 @@ public void testStoreWithTagDates() throws IOException { super.testStoreWithTagDates(); } - + @Override + @Test + public void testSlashStoreWithMultipleDirectories() throws IOException { + allowWritingToTheSlashFile(); + allowWritingToTheOtherSlashFile(); + allowReadingFromTheFileA(); + allowReadingFromTheFileB(); + + Map> queries = new HashMap<>(); + queries.put("@container='test' AND addressable='one' AND object1='tag'", Arrays.asList("thefile/a")); + queries.put("@container='test' AND addressable='two' AND object2='tag'", Arrays.asList("thefile/b")); + + setupBlobSearch(queries); + + super.testSlashStoreWithMultipleDirectories(); + } } diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/FileStateStorageTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/FileStateStorageTest.java index 6e6f6bda..a802cf37 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/FileStateStorageTest.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/state/FileStateStorageTest.java @@ -5,13 +5,18 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.apache.commons.io.FileUtils; import org.finos.springbot.teams.MockTeamsConfiguration; import org.finos.springbot.workflow.data.DataHandlerConfig; import org.finos.springbot.workflow.data.EntityJsonConverter; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -37,6 +42,17 @@ public void setup() throws IOException { this.tss = new FileStateStorage(ejc, tmpdir); } + @Test + public void testCantStoreMultipleNestedDirectories() throws IOException { + Map somedata = Collections.singletonMap("a", "b"); + + Map tagsForTheFileB = new HashMap(); + tagsForTheFileB.put("addressable", "two"); + tagsForTheFileB.put("object2", "tag"); + + Assertions.assertThrows(UnsupportedOperationException.class, () -> tss.store("thefile/c/b", tagsForTheFileB, somedata)); + } + @AfterEach public void cleanUp() throws IOException { Path path = Paths.get(tmpdir);