diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java index 05425255..0ba7ca40 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java @@ -8,6 +8,7 @@ import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ConversationReferenceHelper; import com.microsoft.bot.schema.ResourceResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.Function; @@ -81,7 +82,7 @@ public abstract class BotAdapter { * the receiving channel assigned to the activities. * {@link TurnContext#onSendActivities(SendActivitiesHandler)} */ - public abstract CompletableFuture sendActivities(TurnContext context, Activity[] activities); + public abstract CompletableFuture sendActivities(TurnContext context, List activities); /** * When overridden in a derived class, replaces an existing activity in the diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java index dc683f96..646ff88e 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java @@ -279,7 +279,7 @@ public class BotFrameworkAdapter extends BotAdapter { */ @SuppressWarnings("checkstyle:EmptyBlock") @Override - public CompletableFuture sendActivities(TurnContext context, Activity[] activities) { + public CompletableFuture sendActivities(TurnContext context, List activities) { if (context == null) { throw new IllegalArgumentException("context"); } @@ -288,20 +288,20 @@ public class BotFrameworkAdapter extends BotAdapter { throw new IllegalArgumentException("activities"); } - if (activities.length == 0) { + if (activities.size() == 0) { throw new IllegalArgumentException("Expecting one or more activities, but the array was empty."); } return CompletableFuture.supplyAsync(() -> { - ResourceResponse[] responses = new ResourceResponse[activities.length]; + ResourceResponse[] responses = new ResourceResponse[activities.size()]; /* * NOTE: we're using for here (vs. foreach) because we want to simultaneously index into the * activities array to get the activity to process as well as use that index to assign * the response to the responses array and this is the most cost effective way to do that. */ - for (int index = 0; index < activities.length; index++) { - Activity activity = activities[index]; + for (int index = 0; index < activities.size(); index++) { + Activity activity = activities.get(index); ResourceResponse response = null; if (activity.isType(ActivityTypes.DELAY)) { diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java index 3d4960a8..7887c02a 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java @@ -5,8 +5,10 @@ package com.microsoft.bot.builder; import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.InputHints; import com.microsoft.bot.schema.ResourceResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -50,7 +52,7 @@ public class DelegatingTurnContext implements TurnContext { } @Override - public CompletableFuture sendActivity(String textReplyToSend, String speak, String inputHint) { + public CompletableFuture sendActivity(String textReplyToSend, String speak, InputHints inputHint) { return innerTurnContext.sendActivity(textReplyToSend, speak, inputHint); } @@ -60,7 +62,7 @@ public class DelegatingTurnContext implements TurnContext { } @Override - public CompletableFuture sendActivities(Activity[] activities) { + public CompletableFuture sendActivities(List activities) { return innerTurnContext.sendActivities(activities); } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java index 8a2ecaa2..8b950275 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java @@ -4,8 +4,10 @@ package com.microsoft.bot.builder; import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.InputHints; import com.microsoft.bot.schema.ResourceResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -117,7 +119,7 @@ public interface TurnContext { * rate, volume, pronunciation, and pitch, specify {@code speak} in * Speech Synthesis Markup Language (SSML) format.

*/ - CompletableFuture sendActivity(String textReplyToSend, String speak, String inputHint); + CompletableFuture sendActivity(String textReplyToSend, String speak, InputHints inputHint); /** * Sends an activity to the sender of the incoming activity. @@ -139,12 +141,12 @@ public interface TurnContext { * an array of {@link ResourceResponse} objects containing the IDs that * the receiving channel assigned to the activities. */ - CompletableFuture sendActivities(Activity[] activities); + CompletableFuture sendActivities(List activities); /** * Replaces an existing activity. * - * @param activity New replacement activity. + * @param withActivity New replacement activity. * @return A task that represents the work queued to execute. * If the activity is successfully sent, the task result contains * a {@link ResourceResponse} object containing the ID that the receiving @@ -152,7 +154,7 @@ public interface TurnContext { *

Before calling this, set the ID of the replacement activity to the ID * of the activity to replace.

*/ - CompletableFuture updateActivity(Activity activity); + CompletableFuture updateActivity(Activity withActivity); /** * Deletes an existing activity. @@ -178,7 +180,7 @@ public interface TurnContext { * @param handler The handler to add to the context object. * @return The updated context object. * When the context's {@link #sendActivity(Activity)} - * or {@link #sendActivities(Activity[])} methods are called, + * or {@link #sendActivities(List)} methods are called, * the adapter calls the registered handlers in the order in which they were * added to the context object. */ diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java index 889c5f37..d1951a5d 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java @@ -1,133 +1,80 @@ -package com.microsoft.bot.builder; - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +package com.microsoft.bot.builder; + import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.InputHints; import com.microsoft.bot.schema.ResourceResponse; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; - -import static com.microsoft.bot.schema.ActivityTypes.MESSAGE; -import static com.microsoft.bot.schema.ActivityTypes.TRACE; -import static java.util.stream.Collectors.toList; +import java.util.stream.Collectors; /** * Provides context for a turn of a bot. * Context provides information needed to process an incoming activity. * The context object is created by a {@link BotAdapter} and persists for the * length of the turn. - * {@linkalso Bot} - * {@linkalso Middleware} + * {@link Bot} + * {@link Middleware} */ public class TurnContextImpl implements TurnContext, AutoCloseable { + /** + * The bot adapter that created this context object. + */ private final BotAdapter adapter; + + /** + * The activity associated with this turn; or null when processing a proactive message. + */ private final Activity activity; + private final List onSendActivities = new ArrayList(); private final List onUpdateActivity = new ArrayList(); private final List onDeleteActivity = new ArrayList(); + + /** + * The services registered on this context object. + */ private final TurnContextStateCollection turnState; + + /** + * Indicates whether at least one response was sent for the current turn. + */ private Boolean responded = false; /** * Creates a context object. * - * @param adapter The adapter creating the context. - * @param activity The incoming activity for the turn; + * @param withAdapter The adapter creating the context. + * @param withActivity The incoming activity for the turn; * or {@code null} for a turn for a proactive message. * @throws IllegalArgumentException {@code activity} or * {@code adapter} is {@code null}. * For use by bot adapter implementations only. */ - public TurnContextImpl(BotAdapter adapter, Activity activity) { - if (adapter == null) + public TurnContextImpl(BotAdapter withAdapter, Activity withActivity) { + if (withAdapter == null) { throw new IllegalArgumentException("adapter"); - this.adapter = adapter; - if (activity == null) + } + adapter = withAdapter; + + if (withActivity == null) { throw new IllegalArgumentException("activity"); - this.activity = activity; + } + activity = withActivity; turnState = new TurnContextStateCollection(); } - /** - * Creates a conversation reference from an activity. - * - * @param activity The activity. - * @return A conversation reference for the conversation that contains the activity. - * @throws IllegalArgumentException {@code activity} is {@code null}. - */ - public static ConversationReference getConversationReference(Activity activity) { - BotAssert.activityNotNull(activity); - - ConversationReference r = new ConversationReference() {{ - setActivityId(activity.getId()); - setUser(activity.getFrom()); - setBot(activity.getRecipient()); - setConversation(activity.getConversation()); - setChannelId(activity.getChannelId()); - setServiceUrl(activity.getServiceUrl()); - }}; - - return r; - } - - /** - * Updates an activity with the delivery information from an existing - * conversation reference. - * - * @param activity The activity to update. - * @param reference The conversation reference. - */ - public static Activity applyConversationReference(Activity activity, ConversationReference reference) { - return applyConversationReference(activity, reference, false); - } - - /** - * Updates an activity with the delivery information from an existing - * conversation reference. - * - * @param activity The activity to update. - * @param reference The conversation reference. - * @param isIncoming (Optional) {@code true} to treat the activity as an - * incoming activity, where the bot is the recipient; otherwaire {@code false}. - * Default is {@code false}, and the activity will show the bot as the sender. - * Call {@link #getConversationReference(Activity)} on an incoming - * activity to get a conversation reference that you can then use to update an - * outgoing activity with the correct delivery information. - *

The {@link #sendActivity(Activity)} and {@link #sendActivities(Activity[])} - * methods do this for you.

- */ - public static Activity applyConversationReference(Activity activity, - ConversationReference reference, - boolean isIncoming) { - - activity.setChannelId(reference.getChannelId()); - activity.setServiceUrl(reference.getServiceUrl()); - activity.setConversation(reference.getConversation()); - - if (isIncoming) { - activity.setFrom(reference.getUser()); - activity.setRecipient(reference.getBot()); - if (reference.getActivityId() != null) - activity.setId(reference.getActivityId()); - } else { // Outgoing - activity.setFrom(reference.getBot()); - activity.setRecipient(reference.getUser()); - if (reference.getActivityId() != null) - activity.setReplyToId(reference.getActivityId()); - } - return activity; - } - /** * Adds a response handler for send activity operations. * @@ -135,16 +82,17 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { * @return The updated context object. * @throws IllegalArgumentException {@code handler} is {@code null}. * When the context's {@link #sendActivity(Activity)} - * or {@link #sendActivities(Activity[])} methods are called, + * or {@link #sendActivities(List)} methods are called, * the adapter calls the registered handlers in the order in which they were * added to the context object. */ @Override public TurnContext onSendActivities(SendActivitiesHandler handler) { - if (handler == null) + if (handler == null) { throw new IllegalArgumentException("handler"); + } - this.onSendActivities.add(handler); + onSendActivities.add(handler); return this; } @@ -160,10 +108,11 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { */ @Override public TurnContext onUpdateActivity(UpdateActivityHandler handler) { - if (handler == null) + if (handler == null) { throw new IllegalArgumentException("handler"); + } - this.onUpdateActivity.add(handler); + onUpdateActivity.add(handler); return this; } @@ -179,10 +128,11 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { */ @Override public TurnContext onDeleteActivity(DeleteActivityHandler handler) { - if (handler == null) + if (handler == null) { throw new IllegalArgumentException("handler"); + } - this.onDeleteActivity.add(handler); + onDeleteActivity.add(handler); return this; } @@ -213,58 +163,90 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { * Indicates whether at least one response was sent for the current turn. * * @return {@code true} if at least one response was sent for the current turn. - * @throws IllegalArgumentException You attempted to set the value to {@code false}. */ @Override public boolean getResponded() { - return this.responded; - } - - private void setResponded(boolean responded) { - if (responded == false) { - throw new IllegalArgumentException("TurnContext: cannot set 'responded' to a value of 'false'."); - } - this.responded = true; + return responded; } /** * Sends a message activity to the sender of the incoming activity. * + *

If the activity is successfully sent, the task result contains + * a {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity.

+ * + *

See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}.

+ * * @param textReplyToSend The text of the message to send. * @return A task that represents the work queued to execute. * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace. - * If the activity is successfully sent, the task result contains - * a {@link ResourceResponse} object containing the ID that the receiving - * channel assigned to the activity. - *

See the channel's documentation for limits imposed upon the contents of - * {@code textReplyToSend}.

- *

To control various characteristics of your bot's speech such as voice, - * rate, volume, pronunciation, and pitch, specify {@code speak} in - * Speech Synthesis Markup Language (SSML) format.

*/ @Override public CompletableFuture sendActivity(String textReplyToSend) { return sendActivity(textReplyToSend, null, null); } + /** + * Sends a message activity to the sender of the incoming activity. + * + *

If the activity is successfully sent, the task result contains + * a {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity.

+ * + *

See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}.

+ * + * @param textReplyToSend The text of the message to send. + * @param speak To control various characteristics of your bot's speech such as voice + * rate, volume, pronunciation, and pitch, specify Speech Synthesis Markup + * Language (SSML) format. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace. + */ @Override public CompletableFuture sendActivity(String textReplyToSend, String speak) { return sendActivity(textReplyToSend, speak, null); } + /** + * Sends a message activity to the sender of the incoming activity. + * + *

If the activity is successfully sent, the task result contains + * a {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity.

+ * + *

See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}.

+ * + * @param textReplyToSend The text of the message to send. + * @param speak To control various characteristics of your bot's speech such as voice + * rate, volume, pronunciation, and pitch, specify Speech Synthesis Markup + * Language (SSML) format. + * @param inputHint (Optional) Input hint. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace. + */ @Override - public CompletableFuture sendActivity(String textReplyToSend, String speak, String inputHint) { - if (StringUtils.isEmpty(textReplyToSend)) + public CompletableFuture sendActivity(String textReplyToSend, + String speak, + InputHints inputHint) { + if (StringUtils.isEmpty(textReplyToSend)) { throw new IllegalArgumentException("textReplyToSend"); + } - Activity activityToSend = new Activity(MESSAGE) {{ + Activity activityToSend = new Activity(ActivityTypes.MESSAGE) {{ setText(textReplyToSend); }}; - if (speak != null) - activityToSend.setSpeak(speak); - if (StringUtils.isNotEmpty(inputHint)) - activityToSend.setInputHint(InputHints.fromString(inputHint)); + if (StringUtils.isNotEmpty(speak)) { + activityToSend.setSpeak(speak); + } + + if (inputHint != null) { + activityToSend.setInputHint(inputHint); + } return sendActivity(activityToSend); } @@ -281,15 +263,14 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { */ @Override public CompletableFuture sendActivity(Activity activity) { - if (activity == null) { - throw new IllegalArgumentException("activity"); - } + BotAssert.activityNotNull(activity); - Activity[] activities = {activity}; - return sendActivities(activities) + return sendActivities(Collections.singletonList(activity)) .thenApply(resourceResponses -> { if (resourceResponses == null || resourceResponses.length == 0) { - return null; + // It's possible an interceptor prevented the activity from having been sent. + // Just return an empty response in that case. + return new ResourceResponse(); } return resourceResponses[0]; }); @@ -305,201 +286,104 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { * the receiving channel assigned to the activities. */ @Override - public CompletableFuture sendActivities(Activity[] activities) { - // Bind the relevant Conversation Reference properties, such as URLs and - // ChannelId's, to the activities we're about to send. - ConversationReference cr = getConversationReference(this.activity); - for (Activity a : activities) { - applyConversationReference(a, cr); + public CompletableFuture sendActivities(List activities) { + if (activities == null || activities.size() == 0) { + throw new IllegalArgumentException("activities"); } - // Convert the IActivities to Activities. - List activityArray = Arrays.stream(activities).map(input -> input).collect(toList()); + // Bind the relevant Conversation Reference properties, such as URLs and + // ChannelId's, to the activities we're about to send. + ConversationReference cr = activity.getConversationReference(); - // Create the list used by the recursive methods. - List activityList = new ArrayList(activityArray); + // Buffer the incoming activities into a List since we allow the set to be manipulated by the callbacks + // Bind the relevant Conversation Reference properties, such as URLs and + // ChannelId's, to the activity we're about to send + List bufferedActivities = activities.stream() + .map(a -> a.applyConversationReference(cr)).collect(Collectors.toList()); - Supplier> actuallySendStuff = () -> { - // Send from the list, which may have been manipulated via the event handlers. - // Note that 'responses' was captured from the root of the call, and will be - // returned to the original caller. - return getAdapter().sendActivities(this, activityList.toArray(new Activity[activityList.size()])) - .thenApply(responses -> { - if (responses != null && responses.length == activityList.size()) { - // stitch up activity ids - for (int i = 0; i < responses.length; i++) { - ResourceResponse response = responses[i]; - Activity activity = activityList.get(i); - activity.setId(response.getId()); - } - } + if (onSendActivities.size() == 0) { + return sendActivitiesThroughAdapter(bufferedActivities); + } - // Are the any non-trace activities to send? - // The thinking here is that a Trace event isn't user relevant data - // so the "Responded" flag should not be set by Trace messages being - // sent out. - if (activityList.stream().anyMatch((a) -> a.getType() == TRACE)) { - this.setResponded(true); - } - return responses; - }); - }; + return sendActivitiesThroughCallbackPipeline(bufferedActivities, 0); + } - List act_list = new ArrayList<>(activityList); - return sendActivitiesInternal(act_list, onSendActivities.iterator(), actuallySendStuff); + private CompletableFuture sendActivitiesThroughAdapter(List activities) { + return adapter.sendActivities(this, activities) + .thenApply(responses -> { + boolean sentNonTraceActivity = false; + + for (int index = 0; index < responses.length; index++) { + Activity activity = activities.get(index); + activity.setId(responses[index].getId()); + sentNonTraceActivity |= !activity.isType(ActivityTypes.TRACE); + } + + if (sentNonTraceActivity) { + responded = true; + } + + return responses; + }); + } + + private CompletableFuture sendActivitiesThroughCallbackPipeline(List activities, + int nextCallbackIndex) { + + if (nextCallbackIndex == onSendActivities.size()) { + return sendActivitiesThroughAdapter(activities); + } + + return onSendActivities.get(nextCallbackIndex).invoke(this, + activities, () -> sendActivitiesThroughCallbackPipeline(activities, nextCallbackIndex + 1)); } /** * Replaces an existing activity. * - * @param activity New replacement activity. + * @param withActivity New replacement activity. * @return A task that represents the work queued to execute. - * @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the response contained additional information. + * @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the + * response contained additional information. */ @Override - public CompletableFuture updateActivity(Activity activity) { - Supplier> ActuallyUpdateStuff = () -> { - return getAdapter().updateActivity(this, activity); - }; - - return updateActivityInternal(activity, onUpdateActivity.iterator(), ActuallyUpdateStuff); - } - - /** - * Deletes an existing activity. - * - * @param activityId The ID of the activity to delete. - * @return A task that represents the work queued to execute. - * @throws Exception The HTTP operation failed and the response contained additional information. - */ - public CompletableFuture deleteActivity(String activityId) { - if (StringUtils.isWhitespace(activityId) || activityId == null) { - throw new IllegalArgumentException("activityId"); - } - - ConversationReference cr = getConversationReference(getActivity()); - cr.setActivityId(activityId); - - Supplier> ActuallyDeleteStuff = () -> - getAdapter().deleteActivity(this, cr); - - return deleteActivityInternal(cr, onDeleteActivity.iterator(), ActuallyDeleteStuff); - } - - /** - * Deletes an existing activity. - * - * @param conversationReference The conversation containing the activity to delete. - * @return A task that represents the work queued to execute. - * @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the response contained additional information. - * The conversation reference's {@link ConversationReference#getActivityId} - * indicates the activity in the conversation to delete. - */ - @Override - public CompletableFuture deleteActivity(ConversationReference conversationReference) { - if (conversationReference == null) - throw new IllegalArgumentException("conversationReference"); - - Supplier> ActuallyDeleteStuff = () -> - getAdapter().deleteActivity(this, conversationReference); - - return deleteActivityInternal(conversationReference, onDeleteActivity.iterator(), ActuallyDeleteStuff); - } - - private CompletableFuture sendActivitiesInternal( - List activities, - Iterator sendHandlers, - Supplier> callAtBottom) { - - if (activities == null) { - throw new IllegalArgumentException("activities"); - } - if (sendHandlers == null) { - throw new IllegalArgumentException("sendHandlers"); - } - - if (!sendHandlers.hasNext()) { // No middleware to run. - if (callAtBottom != null) { - return callAtBottom.get(); - } - return CompletableFuture.completedFuture(new ResourceResponse[0]); - } - - // Default to "No more Middleware after this". - Supplier> next = () -> { - // Remove the first item from the list of middleware to call, - // so that the next call just has the remaining items to worry about. - //Iterable remaining = sendHandlers.Skip(1); - //Iterator remaining = sendHandlers.iterator(); - if (sendHandlers.hasNext()) - sendHandlers.next(); - return sendActivitiesInternal(activities, sendHandlers, callAtBottom); - }; - - // Grab the current middleware, which is the 1st element in the array, and execute it - SendActivitiesHandler caller = sendHandlers.next(); - return caller.invoke(this, activities, next); - } - - // private async Task UpdateActivityInternal(Activity activity, - // IEnumerable updateHandlers, - // Func> callAtBottom) - // { - // BotAssert.ActivityNotNull(activity); - // if (updateHandlers == null) - // throw new ArgumentException(nameof(updateHandlers)); - // - // if (updateHandlers.Count() == 0) // No middleware to run. - // { - // if (callAtBottom != null) - // { - // return await callAtBottom(); - // } - // - // return null; - // } - // - // /** - // */ Default to "No more Middleware after this". - // */ - // async Task next() - // { - // /** - // */ Remove the first item from the list of middleware to call, - // */ so that the next call just has the remaining items to worry about. - // */ - // IEnumerable remaining = updateHandlers.Skip(1); - // var result = await UpdateActivityInternal(activity, remaining, callAtBottom).ConfigureAwait(false); - // activity.Id = result.Id; - // return result; - // } - // - // /** - // */ Grab the current middleware, which is the 1st element in the array, and execute it - // */ - // UpdateActivityHandler toCall = updateHandlers.First(); - // return await toCall(this, activity, next); - // } - private CompletableFuture updateActivityInternal(Activity activity, - Iterator updateHandlers, - Supplier> callAtBottom) { + public CompletableFuture updateActivity(Activity withActivity) { BotAssert.activityNotNull(activity); - if (updateHandlers == null) - throw new IllegalArgumentException("updateHandlers"); - if (false == updateHandlers.hasNext()) { // No middleware to run. + ConversationReference conversationReference = activity.getConversationReference(); + withActivity.applyConversationReference(conversationReference); + + Supplier> actuallyUpdateStuff = + () -> getAdapter().updateActivity(this, withActivity); + + return updateActivityInternal(withActivity, onUpdateActivity.iterator(), actuallyUpdateStuff); + } + + private CompletableFuture updateActivityInternal( + Activity activity, + Iterator updateHandlers, + Supplier> callAtBottom) { + + BotAssert.activityNotNull(activity); + if (updateHandlers == null) { + throw new IllegalArgumentException("updateHandlers"); + } + + // No middleware to run. + if (!updateHandlers.hasNext()) { if (callAtBottom != null) { return callAtBottom.get(); } - return null; + return CompletableFuture.completedFuture(null); } // Default to "No more Middleware after this". Supplier> next = () -> { // Remove the first item from the list of middleware to call, // so that the next call just has the remaining items to worry about. - if (updateHandlers.hasNext()) + if (updateHandlers.hasNext()) { updateHandlers.next(); + } return updateActivityInternal(activity, updateHandlers, callAtBottom) .thenApply(resourceResponse -> { @@ -513,14 +397,57 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { return toCall.invoke(this, activity, next); } + /** + * Deletes an existing activity. + * + * @param activityId The ID of the activity to delete. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture deleteActivity(String activityId) { + if (StringUtils.isWhitespace(activityId) || StringUtils.isEmpty(activityId)) { + throw new IllegalArgumentException("activityId"); + } + + ConversationReference cr = activity.getConversationReference(); + cr.setActivityId(activityId); + + Supplier> actuallyDeleteStuff = () -> + getAdapter().deleteActivity(this, cr); + + return deleteActivityInternal(cr, onDeleteActivity.iterator(), actuallyDeleteStuff); + } + + /** + * Deletes an existing activity. + * + * The conversation reference's {@link ConversationReference#getActivityId} + * indicates the activity in the conversation to delete. + * + * @param conversationReference The conversation containing the activity to delete. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture deleteActivity(ConversationReference conversationReference) { + if (conversationReference == null) { + throw new IllegalArgumentException("conversationReference"); + } + + Supplier> actuallyDeleteStuff = () -> + getAdapter().deleteActivity(this, conversationReference); + + return deleteActivityInternal(conversationReference, onDeleteActivity.iterator(), actuallyDeleteStuff); + } + private CompletableFuture deleteActivityInternal(ConversationReference cr, Iterator deleteHandlers, Supplier> callAtBottom) { BotAssert.conversationReferenceNotNull(cr); - if (deleteHandlers == null) + if (deleteHandlers == null) { throw new IllegalArgumentException("deleteHandlers"); + } - if (!deleteHandlers.hasNext()) { // No middleware to run. + // No middleware to run. + if (!deleteHandlers.hasNext()) { if (callAtBottom != null) { return callAtBottom.get(); } @@ -531,10 +458,9 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { Supplier> next = () -> { // Remove the first item from the list of middleware to call, // so that the next call just has the remaining items to worry about. - - //Iterator remaining = (deleteHandlers.hasNext()) ? deleteHandlers.next() : null; - if (deleteHandlers.hasNext()) + if (deleteHandlers.hasNext()) { deleteHandlers.next(); + } return deleteActivityInternal(cr, deleteHandlers, callAtBottom); }; @@ -544,6 +470,16 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { return toCall.invoke(this, cr, next); } + @Override + public void finalize() { + try { + close(); + } catch (Exception e) { + + } + } + + @Override public void close() throws Exception { turnState.close(); } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java index 1c3c8d9b..b5649d7d 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java @@ -3,6 +3,8 @@ package com.microsoft.bot.builder; +import com.microsoft.bot.connector.ConnectorClient; + import java.util.HashMap; import java.util.Map; @@ -71,6 +73,9 @@ public class TurnContextStateCollection extends HashMap implemen public void close() throws Exception { for (Map.Entry entry : entrySet()) { if (entry.getValue() instanceof AutoCloseable) { + if (entry.getValue() instanceof ConnectorClient) { + continue; + } ((AutoCloseable) entry.getValue()).close(); } } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java index 04469225..c4a6baae 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java @@ -279,7 +279,7 @@ public class ActivityHandlerTests { private static class NotImplementedAdapter extends BotAdapter { @Override - public CompletableFuture sendActivities(TurnContext context, Activity[] activities) { + public CompletableFuture sendActivities(TurnContext context, List activities) { throw new NotImplementedException(); } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java index 4a9cfcbe..1e9183a1 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Test; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -28,7 +29,7 @@ public class BotAdapterTests { @Test public void PassResourceResponsesThrough() { - Consumer validateResponse = (activities) -> { + Consumer> validateResponse = (activities) -> { // no need to do anything. }; diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java index e584025b..cdd273bc 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java @@ -14,20 +14,20 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public class SimpleAdapter extends BotAdapter { - private Consumer callOnSend = null; + private Consumer> callOnSend = null; private Consumer callOnUpdate = null; private Consumer callOnDelete = null; // Callback Function but doesn't need to be. Avoid java legacy type erasure - public SimpleAdapter(Consumer callOnSend) { + public SimpleAdapter(Consumer> callOnSend) { this(callOnSend, null, null); } - public SimpleAdapter(Consumer callOnSend, Consumer callOnUpdate) { + public SimpleAdapter(Consumer> callOnSend, Consumer callOnUpdate) { this(callOnSend, callOnUpdate, null); } - public SimpleAdapter(Consumer callOnSend, Consumer callOnUpdate, Consumer callOnDelete) { + public SimpleAdapter(Consumer> callOnSend, Consumer callOnUpdate, Consumer callOnDelete) { this.callOnSend = callOnSend; this.callOnUpdate = callOnUpdate; this.callOnDelete = callOnDelete; @@ -39,9 +39,9 @@ public class SimpleAdapter extends BotAdapter { @Override - public CompletableFuture sendActivities(TurnContext context, Activity[] activities) { + public CompletableFuture sendActivities(TurnContext context, List activities) { Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", activities); - Assert.assertTrue("SimpleAdapter.sendActivities: empty activities array.", activities.length > 0); + Assert.assertTrue("SimpleAdapter.sendActivities: empty activities array.", activities.size() > 0); if (this.callOnSend != null) this.callOnSend.accept(activities); @@ -67,7 +67,7 @@ public class SimpleAdapter extends BotAdapter { Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", reference); if (callOnDelete != null) this.callOnDelete.accept(reference); - return null; + return CompletableFuture.completedFuture(null); } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java index 2a79e582..e449f119 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java @@ -3,510 +3,615 @@ package com.microsoft.bot.builder; -//[TestClass] -//[TestCategory("Middleware")] -//public class TurnContextTests extends BotConnectorTestBase { +import com.microsoft.azure.AzureClient; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.rest.RestClient; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + public class TurnContextTests { -/* - @Test - public CompletableFuture ConstructorNullAdapter() - { - //TurnContext c = new TurnContext(null, new Activity()); - //Assert.Fail("Should Fail due to null Adapter"); + @Test(expected = IllegalArgumentException.class) + public void ConstructorNullAdapter() { + new TurnContextImpl(null, new Activity(ActivityTypes.MESSAGE)); + Assert.fail("Should Fail due to null Adapter"); + } + + @Test(expected = IllegalArgumentException.class) + public void ConstructorNullActivity() { + new TurnContextImpl(new TestAdapter(), null); + Assert.fail("Should Fail due to null Activity"); } @Test - public CompletableFuture ConstructorNullActivity() - { - //TestAdapter a = new TestAdapter(); - //TurnContext c = new TurnContext(a, null); - //Assert.Fail("Should Fail due to null Activty"); + public void Constructor() { + new TurnContextImpl(new TestAdapter(), new Activity(ActivityTypes.MESSAGE)); } - [TestMethod] - public async Task Constructor() - { - TurnContext c = new TurnContext(new TestAdapter(), new Activity()); - Assert.IsNotNull(c); + @Test + public void CacheValueUsingSetAndGet() { + TestAdapter adapter = new TestAdapter(); + new TestFlow(adapter, (turnContext -> { + switch (turnContext.getActivity().getText()) { + case "count": + return turnContext.sendActivity(turnContext.getActivity().createReply("one")) + .thenCompose(resourceResponse -> turnContext.sendActivity(turnContext.getActivity().createReply("two"))) + .thenCompose(resourceResponse -> turnContext.sendActivity(turnContext.getActivity().createReply("two"))) + .thenApply(resourceResponse -> null); + + case "ignore": + break; + + case "TestResponded": + if (turnContext.getResponded()) { + throw new RuntimeException("Responded is true"); + } + + return turnContext.sendActivity(turnContext.getActivity().createReply("one")) + .thenApply(resourceResponse -> { + if (!turnContext.getResponded()) { + throw new RuntimeException("Responded is false"); + } + return null; + }); + + default: + return turnContext.sendActivity(turnContext.getActivity().createReply("echo:" + turnContext.getActivity().getText())) + .thenApply(resourceResponse -> null); + } + + return CompletableFuture.completedFuture(null); + })) + .send("TestResponded") + .startTest(); } - [TestMethod] - public async Task RespondedIsFalse() - { - TurnContext c = new TurnContext(new TestAdapter(), new Activity()); - Assert.IsFalse(c.Responded); + @Test(expected = IllegalArgumentException.class) + public void GetThrowsOnNullKey() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), new Activity(ActivityTypes.MESSAGE)); + Object o = c.getTurnState().get((String)null); } - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public async Task UnableToSetRespondedToFalse() - { - TurnContext c = new TurnContext(new TestAdapter(), new Activity()) - { - Responded = false // should throw - }; - Assert.Fail("Should have thrown"); + @Test + public void GetReturnsNullOnEmptyKey() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), new Activity(ActivityTypes.MESSAGE)); + Object service = c.getTurnState().get(""); + Assert.assertNull("Should not have found a service under an empty key", service); } - [TestMethod] - public async Task CacheValueUsingSetAndGet() - { - var adapter = new TestAdapter(); - await new TestFlow(adapter, MyBotLogic) - .Send("TestResponded") - .StartTest(); + @Test + public void GetReturnsNullWithUnknownKey() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), new Activity(ActivityTypes.MESSAGE)); + Object service = c.getTurnState().get("test"); + Assert.assertNull("Should not have found a service with unknown key", service); } - [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] - public async Task GetThrowsOnNullKey() - { - TurnContext c = new TurnContext(new SimpleAdapter(), new Activity()); - c.Services.Get(null); + @Test + public void CacheValueUsingGetAndSet() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), new Activity(ActivityTypes.MESSAGE)); + + c.getTurnState().add("bar", "foo"); + String result = c.getTurnState().get("bar"); + + Assert.assertEquals("foo", result); } - [TestMethod] - public async Task GetReturnsNullOnEmptyKey() - { - TurnContext c = new TurnContext(new SimpleAdapter(), new Activity()); - object service = c.Services.Get(string.Empty); // empty key - Assert.IsNull(service, "Should not have found a service under an empty key"); + @Test + public void CacheValueUsingGetAndSetGenericWithTypeAsKeyName() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), new Activity(ActivityTypes.MESSAGE)); + + c.getTurnState().add("foo"); + String result = c.getTurnState().get(String.class); + + Assert.assertEquals("foo", result); } - - [TestMethod] - public async Task GetReturnsNullWithUnknownKey() - { - TurnContext c = new TurnContext(new SimpleAdapter(), new Activity()); - object o = c.Services.Get("test"); - Assert.IsNull(o); + @Test + public void RequestIsSet() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), TestMessage.Message()); + Assert.assertEquals("1234", c.getActivity().getId()); } - [TestMethod] - public async Task CacheValueUsingGetAndSet() - { - TurnContext c = new TurnContext(new SimpleAdapter(), new Activity()); - - c.Services.Add("bar", "foo"); - var result = c.Services.Get("bar"); - - Assert.AreEqual("foo", result); - } - [TestMethod] - public async Task CacheValueUsingGetAndSetGenericWithTypeAsKeyName() - { - TurnContext c = new TurnContext(new SimpleAdapter(), new Activity()); - - c.Services.Add("foo"); - string result = c.Services.Get(); - - Assert.AreEqual("foo", result); - } - - [TestMethod] - public async Task RequestIsSet() - { - TurnContext c = new TurnContext(new SimpleAdapter(), TestMessage.Message()); - Assert.IsTrue(c.Activity.Id == "1234"); - } - - [TestMethod] - public async Task SendAndSetResponded() - { + @Test + public void SendAndSetResponded() { SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); - Assert.IsFalse(c.Responded); - var response = await c.SendActivity(TestMessage.Message("testtest")); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); + ResourceResponse response = c.sendActivity(TestMessage.Message("testtest")).join(); - Assert.IsTrue(c.Responded); - Assert.IsTrue(response.Id == "testtest"); + Assert.assertTrue(c.getResponded()); + Assert.assertEquals("testtest", response.getId()); } - [TestMethod] - public async Task SendBatchOfActivities() - { + @Test + public void SendBatchOfActivities() { SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); - Assert.IsFalse(c.Responded); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); - var message1 = TestMessage.Message("message1"); - var message2 = TestMessage.Message("message2"); + Activity message1 = TestMessage.Message("message1"); + Activity message2 = TestMessage.Message("message2"); - var response = await c.SendActivities(new Activity[] { message1, message2 } ); + ResourceResponse[] response = c.sendActivities(Arrays.asList(message1, message2)).join(); - Assert.IsTrue(c.Responded); - Assert.IsTrue(response.Length == 2); - Assert.IsTrue(response[0].Id == "message1"); - Assert.IsTrue(response[1].Id == "message2"); + Assert.assertTrue(c.getResponded()); + Assert.assertEquals(2, response.length); + Assert.assertEquals("message1", response[0].getId()); + Assert.assertEquals("message2", response[1].getId()); } - [TestMethod] - public async Task SendAndSetRespondedUsingMessageActivity() - { + @Test + public void SendAndSetRespondedUsingIMessageActivity() { SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); - Assert.IsFalse(c.Responded); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); - MessageActivity msg = TestMessage.Message().AsMessageActivity(); - await c.SendActivity(msg); - Assert.IsTrue(c.Responded); + Activity msg = TestMessage.Message(); + c.sendActivity(msg).join(); + Assert.assertTrue(c.getResponded()); } - [TestMethod] - public async Task TraceActivitiesDoNoSetResponded() - { + @Test + public void TraceActivitiesDoNoSetResponded() { SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); - Assert.IsFalse(c.Responded); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); // Send a Trace Activity, and make sure responded is NOT set. - ITraceActivity trace = Activity.CreateTraceActivity("trace"); - await c.SendActivity(trace); - Assert.IsFalse(c.Responded); + Activity trace = Activity.createTraceActivity("trace"); + c.sendActivity(trace).join(); + Assert.assertFalse(c.getResponded()); // Just to sanity check everything, send a Message and verify the // responded flag IS set. - MessageActivity msg = TestMessage.Message().AsMessageActivity(); - await c.SendActivity(msg); - Assert.IsTrue(c.Responded); + Activity msg = TestMessage.Message(); + c.sendActivity(msg).join(); + Assert.assertTrue(c.getResponded()); } - [TestMethod] - public async Task SendOneActivityToAdapter() - { - bool foundActivity = false; + @Test + public void SendOneActivityToAdapter() { + boolean[] foundActivity = new boolean[]{ false }; - void ValidateResponses(Activity[] activities) - { - Assert.IsTrue(activities.Count() == 1, "Incorrect Count"); - Assert.IsTrue(activities[0].Id == "1234"); - foundActivity = true; - } - - SimpleAdapter a = new SimpleAdapter(ValidateResponses); - TurnContext c = new TurnContext(a, new Activity()); - await c.SendActivity(TestMessage.Message()); - Assert.IsTrue(foundActivity); - } - - [TestMethod] - public async Task CallOnSendBeforeDelivery() - { - SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); - - int count = 0; - c.OnSendActivities(async (context, activities, next) => - { - Assert.IsNotNull(activities, "Null Array passed in"); - count = activities.Count(); - return await next(); + SimpleAdapter a = new SimpleAdapter((activities) -> { + Assert.assertTrue("Incorrect Count", activities.size() == 1); + Assert.assertEquals("1234", activities.get(0).getId()); + foundActivity[0] = true; }); - await c.SendActivity(TestMessage.Message()); - - Assert.IsTrue(count == 1); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + c.sendActivity(TestMessage.Message()).join(); + Assert.assertTrue(foundActivity[0]); } - [TestMethod] - public async Task AllowInterceptionOfDeliveryOnSend() - { - bool responsesSent = false; - void ValidateResponses(Activity[] activities) - { - responsesSent = true; - Assert.Fail("Should not be called. Interceptor did not work"); - } + @Test + public void CallOnSendBeforeDelivery() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); - SimpleAdapter a = new SimpleAdapter(ValidateResponses); - TurnContext c = new TurnContext(a, new Activity()); + int[] count = new int[] {0}; + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + count[0] = activities.size(); + return next.get(); + })); + + c.sendActivity(TestMessage.Message()).join(); + + Assert.assertEquals(1, count[0]); + } + + @Test + public void AllowInterceptionOfDeliveryOnSend() { + boolean[] responsesSent = new boolean[]{ false }; + + SimpleAdapter a = new SimpleAdapter((activities) -> { + responsesSent[0] = true; + Assert.fail("Should not be called. Interceptor did not work"); + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + int[] count = new int[] {0}; + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + count[0] = activities.size(); - int count = 0; - c.OnSendActivities(async (context, activities, next) => - { - Assert.IsNotNull(activities, "Null Array passed in"); - count = activities.Count(); // Do not call next. - return null; + return CompletableFuture.completedFuture(null); + })); + + c.sendActivity(TestMessage.Message()).join(); + Assert.assertEquals(1, count[0]); + Assert.assertFalse("Responses made it to the adapter.", responsesSent[0]); + } + + @Test + public void InterceptAndMutateOnSend() { + boolean[] foundIt = new boolean[]{ false }; + + SimpleAdapter a = new SimpleAdapter((activities) -> { + Assert.assertNotNull(activities); + Assert.assertTrue(activities.size() == 1); + Assert.assertEquals("changed", activities.get(0).getId()); + foundIt[0] = true; }); - await c.SendActivity(TestMessage.Message()); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); - Assert.IsTrue(count == 1); - Assert.IsFalse(responsesSent, "Responses made it to the adapter."); + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + Assert.assertTrue(activities.size() == 1); + Assert.assertEquals("1234", activities.get(0).getId()); + activities.get(0).setId("changed"); + return next.get(); + })); + + c.sendActivity(TestMessage.Message()).join(); + Assert.assertTrue(foundIt[0]); } - [TestMethod] - public async Task InterceptAndMutateOnSend() - { - bool foundIt = false; - void ValidateResponses(Activity[] activities) - { - Assert.IsNotNull(activities); - Assert.IsTrue(activities.Length == 1); - Assert.IsTrue(activities[0].Id == "changed"); - foundIt = true; - } + @Test + public void UpdateOneActivityToAdapter() { + boolean[] foundActivity = new boolean[]{ false }; - SimpleAdapter a = new SimpleAdapter(ValidateResponses); - TurnContext c = new TurnContext(a, new Activity()); - - c.OnSendActivities(async (context, activities, next) => - { - Assert.IsNotNull(activities, "Null Array passed in"); - Assert.IsTrue(activities.Count() == 1); - Assert.IsTrue(activities[0].Id == "1234", "Unknown Id Passed In"); - activities[0].Id = "changed"; - return await next(); + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("test", activity.getId()); + foundActivity[0] = true; }); - await c.SendActivity(TestMessage.Message()); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); - // Intercepted the message, changed it, and sent it on to the Adapter - Assert.IsTrue(foundIt); + ResourceResponse updateResult = c.updateActivity(TestMessage.Message("test")).join(); + Assert.assertTrue(foundActivity[0]); + Assert.assertEquals("test", updateResult.getId()); } - [TestMethod] - public async Task UpdateOneActivityToAdapter() - { - bool foundActivity = false; + @Test + public void UpdateActivityWithMessageFactory() { + final String ACTIVITY_ID = "activity ID"; + final String CONVERSATION_ID = "conversation ID"; - void ValidateUpdate(Activity activity) - { - Assert.IsNotNull(activity); - Assert.IsTrue(activity.Id == "test"); - foundActivity = true; - } + boolean[] foundActivity = new boolean[]{ false }; - SimpleAdapter a = new SimpleAdapter(ValidateUpdate); - TurnContext c = new TurnContext(a, new Activity()); - - var message = TestMessage.Message("test"); - var updateResult = await c.UpdateActivity(message); - - Assert.IsTrue(foundActivity); - Assert.IsTrue(updateResult.Id == "test"); - } - - [TestMethod] - public async Task CallOnUpdateBeforeDelivery() - { - bool foundActivity = false; - - void ValidateUpdate(Activity activity) - { - Assert.IsNotNull(activity); - Assert.IsTrue(activity.Id == "1234"); - foundActivity = true; - } - - SimpleAdapter a = new SimpleAdapter(ValidateUpdate); - TurnContext c = new TurnContext(a, new Activity()); - - bool wasCalled = false; - c.OnUpdateActivity(async (context, activity, next) => - { - Assert.IsNotNull(activity, "Null activity passed in"); - wasCalled = true; - return await next(); + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals(ACTIVITY_ID, activity.getId()); + Assert.assertEquals(CONVERSATION_ID, activity.getConversation().getId()); + foundActivity[0] = true; }); - await c.UpdateActivity(TestMessage.Message()); - Assert.IsTrue(wasCalled); - Assert.IsTrue(foundActivity); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE) {{ + setConversation(new ConversationAccount(CONVERSATION_ID)); + }}); + + Activity message = MessageFactory.text("test text"); + message.setId(ACTIVITY_ID); + + ResourceResponse updateResult = c.updateActivity(message).join(); + + Assert.assertTrue(foundActivity[0]); + Assert.assertEquals(ACTIVITY_ID, updateResult.getId()); } - [TestMethod] - public async Task InterceptOnUpdate() - { - bool adapterCalled = false; - void ValidateUpdate(Activity activity) - { - adapterCalled = true; - Assert.Fail("Should not be called."); - } + @Test + public void CallOnUpdateBeforeDelivery() { + boolean[] activityDelivered = new boolean[]{ false }; - SimpleAdapter a = new SimpleAdapter(ValidateUpdate); - TurnContext c = new TurnContext(a, new Activity()); + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("1234", activity.getId()); + activityDelivered[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + boolean[] wasCalled = new boolean[]{ false }; + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + Assert.assertFalse(activityDelivered[0]); + wasCalled[0] = true; + return next.get(); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertTrue(activityDelivered[0]); + } + + @Test + public void InterceptOnUpdate() { + boolean[] activityDelivered = new boolean[]{ false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + activityDelivered[0] = true; + Assert.fail("Should not be called."); + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + boolean[] wasCalled = new boolean[]{ false }; + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + wasCalled[0] = true; - bool wasCalled = false; - c.OnUpdateActivity(async (context, activity, next) => - { - Assert.IsNotNull(activity, "Null activity passed in"); - wasCalled = true; // Do Not Call Next - return null; + return CompletableFuture.completedFuture(null); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertFalse(activityDelivered[0]); + } + + @Test + public void InterceptAndMutateOnUpdate() { + boolean[] activityDelivered = new boolean[]{ false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertEquals("mutated", activity.getId()); + activityDelivered[0] = true; }); - await c.UpdateActivity(TestMessage.Message()); - Assert.IsTrue(wasCalled); // Interceptor was called - Assert.IsFalse(adapterCalled); // Adapter was not + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("1234", activity.getId()); + activity.setId("mutated"); + return next.get(); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(activityDelivered[0]); } - [TestMethod] - public async Task InterceptAndMutateOnUpdate() - { - bool adapterCalled = false; - void ValidateUpdate(Activity activity) - { - Assert.IsTrue(activity.Id == "mutated"); - adapterCalled = true; - } + @Test + public void DeleteOneActivityToAdapter() { + boolean[] activityDeleted = new boolean[]{ false }; - SimpleAdapter a = new SimpleAdapter(ValidateUpdate); - TurnContext c = new TurnContext(a, new Activity()); - - c.OnUpdateActivity(async (context, activity, next) => - { - Assert.IsNotNull(activity, "Null activity passed in"); - Assert.IsTrue(activity.Id == "1234"); - activity.Id = "mutated"; - return await next(); + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("12345", reference.getActivityId()); + activityDeleted[0] = true; }); - await c.UpdateActivity(TestMessage.Message()); - Assert.IsTrue(adapterCalled); // Adapter was not + TurnContext c = new TurnContextImpl(a, TestMessage.Message()); + + c.deleteActivity("12345"); + Assert.assertTrue(activityDeleted[0]); } - [TestMethod] - public async Task DeleteOneActivityToAdapter() - { - bool deleteCalled = false; + @Test + public void DeleteConversationReferenceToAdapter() { + boolean[] activityDeleted = new boolean[]{ false }; - void ValidateDelete(ConversationReference r) - { - Assert.IsNotNull(r); - Assert.IsTrue(r.ActivityId == "12345"); - deleteCalled = true; - } + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("12345", reference.getActivityId()); + activityDeleted[0] = true; + }); - SimpleAdapter a = new SimpleAdapter(ValidateDelete); - TurnContext c = new TurnContext(a, TestMessage.Message()); - await c.DeleteActivity("12345"); - Assert.IsTrue(deleteCalled); + TurnContext c = new TurnContextImpl(a, TestMessage.Message()); + + ConversationReference reference = new ConversationReference() {{ + setActivityId("12345"); + }}; + + c.deleteActivity(reference); + Assert.assertTrue(activityDeleted[0]); } - [TestMethod] - public async Task DeleteConversationReferenceToAdapter() - { - bool deleteCalled = false; + @Test + public void InterceptOnDelete() { + boolean[] activityDeleted = new boolean[]{ false }; - void ValidateDelete(ConversationReference r) - { - Assert.IsNotNull(r); - Assert.IsTrue(r.ActivityId == "12345"); - deleteCalled = true; - } + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + activityDeleted[0] = true; + Assert.fail("Should not be called."); + }); - SimpleAdapter a = new SimpleAdapter(ValidateDelete); - TurnContext c = new TurnContext(a, TestMessage.Message()); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); - var reference = new ConversationReference("12345"); + boolean[] wasCalled = new boolean[]{ false }; + c.onDeleteActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + wasCalled[0] = true; - await c.DeleteActivity(reference); - Assert.IsTrue(deleteCalled); - } - - [TestMethod] - public async Task InterceptOnDelete() - { - bool adapterCalled = false; - - void ValidateDelete(ConversationReference r) - { - adapterCalled = true; - Assert.Fail("Should not be called."); - } - - SimpleAdapter a = new SimpleAdapter(ValidateDelete); - TurnContext c = new TurnContext(a, new Activity()); - - bool wasCalled = false; - c.OnDeleteActivity(async (context, convRef, next) => - { - Assert.IsNotNull(convRef, "Null activity passed in"); - wasCalled = true; // Do Not Call Next - }); + return CompletableFuture.completedFuture(null); + })); - await c.DeleteActivity("1234"); - Assert.IsTrue(wasCalled); // Interceptor was called - Assert.IsFalse(adapterCalled); // Adapter was not + c.deleteActivity("1234").join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertFalse(activityDeleted[0]); } - [TestMethod] - public async Task InterceptAndMutateOnDelete() - { - bool adapterCalled = false; + @Test + public void DeleteWithNoOnDeleteHandlers() { + boolean[] activityDeleted = new boolean[]{ false }; - void ValidateDelete(ConversationReference r) - { - Assert.IsTrue(r.ActivityId == "mutated"); - adapterCalled = true; - } - - SimpleAdapter a = new SimpleAdapter(ValidateDelete); - TurnContext c = new TurnContext(a, new Activity()); - - c.OnDeleteActivity(async (context, convRef, next) => - { - Assert.IsNotNull(convRef, "Null activity passed in"); - Assert.IsTrue(convRef.ActivityId == "1234", "Incorrect Activity Id"); - convRef.ActivityId = "mutated"; - await next(); + SimpleAdapter a = new SimpleAdapter(null, null, (activity) -> { + activityDeleted[0] = true; }); - await c.DeleteActivity("1234"); - Assert.IsTrue(adapterCalled); // Adapter was called + valided the change + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.deleteActivity("1234").join(); + + Assert.assertTrue(activityDeleted[0]); } - [TestMethod] - public async Task ThrowExceptionInOnSend() - { + @Test + public void InterceptAndMutateOnDelete() { + boolean[] activityDeleted = new boolean[]{ false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("mutated", reference.getActivityId()); + activityDeleted[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onDeleteActivity(((context, reference, next) -> { + Assert.assertNotNull(reference); + Assert.assertEquals("1234", reference.getActivityId()); + reference.setActivityId("mutated"); + return next.get(); + })); + + c.deleteActivity("1234").join(); + + Assert.assertTrue(activityDeleted[0]); + } + + @Test + public void ThrowExceptionInOnSend() { SimpleAdapter a = new SimpleAdapter(); - TurnContext c = new TurnContext(a, new Activity()); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); - c.OnSendActivities(async (context, activities, next) => - { - throw new Exception("test"); - }); + c.onSendActivities(((context, activities, next) -> { + CompletableFuture result = new CompletableFuture(); + result.completeExceptionally(new RuntimeException("test")); + return result; + })); - try - { - await c.SendActivity(TestMessage.Message()); - Assert.Fail("Should not get here"); - } - catch(Exception ex) - { - Assert.IsTrue(ex.Message == "test"); + try { + c.sendActivity(TestMessage.Message()).join(); + Assert.fail("ThrowExceptionInOnSend have thrown"); + } catch(CompletionException e) { + Assert.assertEquals("test", e.getCause().getMessage()); } } - public async Task MyBotLogic(TurnContext context) - { - switch (context.Activity.AsMessageActivity().Text) - { - case "count": - await context.SendActivity(context.Activity.CreateReply("one")); - await context.SendActivity(context.Activity.CreateReply("two")); - await context.SendActivity(context.Activity.CreateReply("three")); - break; - case "ignore": - break; - case "TestResponded": - if (context.Responded == true) - throw new InvalidOperationException("Responded Is True"); + @Test + public void TurnContextStateNoDispose() { + ConnectorClient connector = new ConnectorClientThrowExceptionOnDispose(); + Assert.assertTrue(connector instanceof AutoCloseable); - await context.SendActivity(context.Activity.CreateReply("one")); + TurnContextStateCollection stateCollection = new TurnContextStateCollection(); + stateCollection.add("connector", connector); - if (context.Responded == false) - throw new InvalidOperationException("Responded Is True"); - break; - default: - await context.SendActivity( - context.Activity.CreateReply($"echo:{context.Activity.Text}")); - break; + try { + stateCollection.close(); + } catch(Throwable t) { + Assert.fail("Should not have thrown"); + } + } + + @Test + public void TurnContextStateDisposeNonConnectorClient() { + TrackDisposed disposableObject1 = new TrackDisposed(); + TrackDisposed disposableObject2 = new TrackDisposed(); + TrackDisposed disposableObject3 = new TrackDisposed(); + Assert.assertFalse(disposableObject1.disposed); + Assert.assertFalse(disposableObject2.disposed); + Assert.assertFalse(disposableObject3.disposed); + + ConnectorClient connector = new ConnectorClientThrowExceptionOnDispose(); + + TurnContextStateCollection stateCollection = new TurnContextStateCollection(); + stateCollection.add("disposable1", disposableObject1); + stateCollection.add("disposable2", disposableObject2); + stateCollection.add("disposable3", disposableObject3); + stateCollection.add("connector", connector); + + try { + stateCollection.close(); + } catch(Throwable t) { + Assert.fail("Should not have thrown"); + } + + Assert.assertTrue(disposableObject1.disposed); + Assert.assertTrue(disposableObject2.disposed); + Assert.assertTrue(disposableObject3.disposed); + } + + private static class TrackDisposed implements AutoCloseable { + public boolean disposed = false; + + @Override + public void close() throws Exception { + disposed = true; + } + } + + private static class ConnectorClientThrowExceptionOnDispose implements ConnectorClient { + + @Override + public RestClient getRestClient() { + return null; + } + + @Override + public AzureClient getAzureClient() { + return null; + } + + @Override + public String getUserAgent() { + return null; + } + + @Override + public String getAcceptLanguage() { + return null; + } + + @Override + public void setAcceptLanguage(String acceptLanguage) { + + } + + @Override + public int getLongRunningOperationRetryTimeout() { + return 0; + } + + @Override + public void setLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) { + + } + + @Override + public boolean getGenerateClientRequestId() { + return false; + } + + @Override + public void setGenerateClientRequestId(boolean generateClientRequestId) { + + } + + @Override + public Attachments getAttachments() { + return null; + } + + @Override + public Conversations getConversations() { + return null; + } + + @Override + public void close() throws Exception { + throw new RuntimeException("Should not close"); } } -*/ } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java index b7f1055d..12830c21 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java @@ -91,7 +91,7 @@ public class TestAdapter extends BotAdapter { } @Override - public CompletableFuture sendActivities(TurnContext context, Activity[] activities) { + public CompletableFuture sendActivities(TurnContext context, List activities) { List responses = new LinkedList(); for (Activity activity : activities) { @@ -104,7 +104,7 @@ public class TestAdapter extends BotAdapter { responses.add(new ResourceResponse(activity.getId())); // This is simulating DELAY - System.out.println(String.format("TestAdapter:SendActivities(tid:%s):Count:%s", Thread.currentThread().getId(), activities.length)); + System.out.println(String.format("TestAdapter:SendActivities(tid:%s):Count:%s", Thread.currentThread().getId(), activities.size())); for (Activity act : activities) { System.out.printf(":--------\n: To:%s\n", act.getRecipient().getName()); System.out.printf(": From:%s\n", (act.getFrom() == null) ? "No from set" : act.getFrom().getName()); diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java index 3d83f7f3..91f859af 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java @@ -16,7 +16,7 @@ import com.microsoft.rest.RestClient; /** * The interface for ConnectorClient class. */ -public interface ConnectorClient { +public interface ConnectorClient extends AutoCloseable { /** * Gets the REST client. * @@ -94,4 +94,9 @@ public interface ConnectorClient { * @return the Conversations object. */ Conversations getConversations(); + + @Override + default void close() throws Exception { + + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java index e6e22b25..19cb28d7 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java @@ -9,7 +9,10 @@ package com.microsoft.bot.connector.rest; import com.microsoft.azure.AzureClient; import com.microsoft.azure.AzureResponseBuilder; import com.microsoft.azure.AzureServiceClient; -import com.microsoft.bot.connector.*; +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.UserAgent; import com.microsoft.rest.credentials.ServiceClientCredentials; import com.microsoft.rest.RestClient; import com.microsoft.rest.retry.RetryStrategy; diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/Activity.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/Activity.java index 2141dbdc..c7d664c6 100644 --- a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/Activity.java +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/Activity.java @@ -313,11 +313,21 @@ public class Activity { /** * Create a TRACE type Activity. * - * @param withName Name of the operation - * @param withValueType valueType if helpful to identify the value schema (default is value.GetType().Name) - * @param withValue The content for this trace operation. - * @param withLabel A descriptive label for this trace operation. + * @param withName Name of the operation */ + public static Activity createTraceActivity(String withName) { + return createTraceActivity(withName, null, null, null); + } + + + /** + * Create a TRACE type Activity. + * + * @param withName Name of the operation + * @param withValueType valueType if helpful to identify the value schema (default is value.GetType().Name) + * @param withValue The content for this trace operation. + * @param withLabel A descriptive label for this trace operation. + */ public static Activity createTraceActivity(String withName, String withValueType, Object withValue, @@ -325,7 +335,11 @@ public class Activity { return new Activity(ActivityTypes.TRACE) {{ setName(withName); setLabel(withLabel); - setValueType((withValueType == null) ? withValue.getClass().getTypeName() : withValueType); + if (withValue != null) { + setValueType((withValueType == null) ? withValue.getClass().getTypeName() : withValueType); + } else { + setValueType(withValueType); + } setValue(withValue); }}; } diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ActivityTypes.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ActivityTypes.java index a2227784..ca36a07e 100644 --- a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ActivityTypes.java +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ActivityTypes.java @@ -6,9 +6,6 @@ package com.microsoft.bot.schema; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** * Defines values for ActivityTypes. */