diff --git a/Generator/generator-botbuilder-java/generators/app/templates/app.java b/Generator/generator-botbuilder-java/generators/app/templates/app.java index 65584b463..c182c1422 100644 --- a/Generator/generator-botbuilder-java/generators/app/templates/app.java +++ b/Generator/generator-botbuilder-java/generators/app/templates/app.java @@ -10,7 +10,7 @@ import com.microsoft.bot.connector.customizations.CredentialProviderImpl; import com.microsoft.bot.connector.customizations.JwtTokenValidation; import com.microsoft.bot.connector.customizations.MicrosoftAppCredentials; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.ConnectorClientImpl; import com.microsoft.bot.schema.models.Activity; import com.microsoft.bot.schema.models.ActivityTypes; import com.microsoft.bot.schema.models.ResourceResponse; diff --git a/etc/bot-checkstyle.xml b/etc/bot-checkstyle.xml new file mode 100644 index 000000000..55fa68e98 --- /dev/null +++ b/etc/bot-checkstyle.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 2c850e05b..bbb50bc4e 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 @@ -1,779 +1,780 @@ -package com.microsoft.bot.builder; - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.bot.connector.ConnectorClient; -import com.microsoft.bot.connector.Conversations; -import com.microsoft.bot.connector.authentication.*; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.connector.implementation.ConversationsImpl; -import com.microsoft.bot.schema.ActivityImpl; -import com.microsoft.bot.schema.models.*; -import com.microsoft.rest.retry.RetryStrategy; -import org.apache.commons.lang3.StringUtils; -import sun.net.www.http.HttpClient; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.function.Function; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -/** - * A bot adapter that can connect a bot to a service endpoint. - * The bot adapter encapsulates authentication processes and sends - * activities to and receives activities from the Bot Connector Service. When your - * bot receives an activity, the adapter creates a context object, passes it to your - * bot's application logic, and sends responses back to the user's channel. - *

Use {@link Use(Middleware)} to add {@link Middleware} objects - * to your adapter’s middleware collection. The adapter processes and directs - * incoming activities in through the bot middleware pipeline to your bot’s logic - * and then back out again. As each activity flows in and out of the bot, each piece - * of middleware can inspect or act upon the activity, both before and after the bot - * logic runs.

- *

- * {@linkalso TurnContext} - * {@linkalso Activity} - * {@linkalso Bot} - * {@linkalso Middleware} - */ -public class BotFrameworkAdapter extends BotAdapter { - private final CredentialProvider _credentialProvider; - - private final RetryStrategy connectorClientRetryStrategy; - private Map appCredentialMap = new HashMap(); - - private final String InvokeReponseKey = "BotFrameworkAdapter.InvokeResponse"; - private boolean isEmulatingOAuthCards = false; - - /** - * Initializes a new instance of the {@link BotFrameworkAdapter} class, - * using a credential provider. - * - * @param credentialProvider The credential provider. - * @param connectorClientRetryStrategy Retry strategy for retrying HTTP operations. - * @param httpClient The HTTP client. - * @param middleware The middleware to initially add to the adapter. - * @throws IllegalArgumentException {@code credentialProvider} is {@code null}. - * Use a {@link MiddlewareSet} object to add multiple middleware - * components in the conustructor. Use the {@link Use(Middleware)} method to - * add additional middleware to the adapter after construction. - */ - public BotFrameworkAdapter(CredentialProvider credentialProvider) { - this(credentialProvider, null, null, null); - } - - public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy) { - this(credentialProvider, connectorClientRetryStrategy, null, null); - } - - public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient) { - this(credentialProvider, connectorClientRetryStrategy, httpClient, null); - } - - public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient, Middleware middleware) { - if (credentialProvider == null) - throw new IllegalArgumentException("credentialProvider"); - _credentialProvider = credentialProvider; - //_httpClient = httpClient ?? new HttpClient(); - this.connectorClientRetryStrategy = connectorClientRetryStrategy; - - if (middleware != null) { - this.Use(middleware); - } - } - - /** - * Sends a proactive message from the bot to a conversation. - * - * @param botAppId The application ID of the bot. This is the appId returned by Portal registration, and is - * generally found in the "MicrosoftAppId" parameter in appSettings.json. - * @param reference A reference to the conversation to continue. - * @param callback The method to call for the resulting bot turn. - * @return A task that represents the work queued to execute. - * @throws IllegalArgumentException {@code botAppId}, {@code reference}, or - * {@code callback} is {@code null}. - * Call this method to proactively send a message to a conversation. - * Most channels require a user to initaiate a conversation with a bot - * before the bot can send activities to the user. - *

This method registers the following.services().for the turn. - * {@link ConnectorClient}, the channel connector client to use this turn. - *

- *

- * This overload differers from the Node implementation by requiring the BotId to be - * passed in. The .Net code allows multiple bots to be hosted in a single adapter which - * isn't something supported by Node. - *

- *

- * {@linkalso ProcessActivity(String, Activity, Func { TurnContext, Task })} - * {@linkalso BotAdapter.RunPipeline(TurnContext, Func { TurnContext, Task } } - */ - @Override - public void ContinueConversation(String botAppId, ConversationReference reference, Consumer callback) throws Exception { - if (StringUtils.isEmpty(botAppId)) - throw new IllegalArgumentException("botAppId"); - - if (reference == null) - throw new IllegalArgumentException("reference"); - - if (callback == null) - throw new IllegalArgumentException("callback"); - - try (TurnContextImpl context = new TurnContextImpl(this, new ConversationReferenceHelper(reference).GetPostToBotMessage())) { - // Hand craft Claims Identity. - HashMap claims = new HashMap(); - claims.put(AuthenticationConstants.AudienceClaim, botAppId); - claims.put(AuthenticationConstants.AppIdClaim, botAppId); - ClaimsIdentityImpl claimsIdentity = new ClaimsIdentityImpl("ExternalBearer", claims); - - context.getServices().Add("BotIdentity", claimsIdentity); - - ConnectorClient connectorClient = this.CreateConnectorClientAsync(reference.serviceUrl(), claimsIdentity).join(); - context.getServices().Add("ConnectorClient", connectorClient); - RunPipeline(context, callback); - } - return; - } - - /** - * Initializes a new instance of the {@link BotFrameworkAdapter} class, - * using an application ID and secret. - * - * @param appId The application ID of the bot. - * @param appPassword The application secret for the bot. - * @param connectorClientRetryStrategy Retry policy for retrying HTTP operations. - * @param httpClient The HTTP client. - * @param middleware The middleware to initially add to the adapter. - * Use a {@link MiddlewareSet} object to add multiple middleware - * components in the conustructor. Use the {@link Use(Middleware)} method to - * add additional middleware to the adapter after construction. - */ - public BotFrameworkAdapter(String appId, String appPassword) { - this(appId, appPassword, null, null, null); - } - - public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy) { - this(appId, appPassword, connectorClientRetryStrategy, null, null); - } - - public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient) { - this(appId, appPassword, connectorClientRetryStrategy, httpClient, null); - } - - public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient, Middleware middleware) { - this(new SimpleCredentialProvider(appId, appPassword), connectorClientRetryStrategy, httpClient, middleware); - } - - /** - * Adds middleware to the adapter's pipeline. - * - * @param middleware The middleware to add. - * @return The updated adapter object. - * Middleware is added to the adapter at initialization time. - * For each turn, the adapter calls middleware in the order in which you added it. - */ - - public BotFrameworkAdapter Use(Middleware middleware) { - super._middlewareSet.Use(middleware); - return this; - } - - /** - * Creates a turn context and runs the middleware pipeline for an incoming activity. - * - * @param authHeader The HTTP authentication header of the request. - * @param activity The incoming activity. - * @param callback The code to run at the end of the adapter's middleware - * pipeline. - * @return A task that represents the work queued to execute. If the activity type - * was 'Invoke' and the corresponding key (channelId + activityId) was found - * then an InvokeResponse is returned, otherwise null is returned. - * @throws IllegalArgumentException {@code activity} is {@code null}. - * @throws UnauthorizedAccessException authentication failed. - * Call this method to reactively send a message to a conversation. - *

This method registers the following.services().for the turn. - * {@link ConnectorClient}, the channel connector client to use this turn. - *

- *

- * {@linkalso ContinueConversation(String, ConversationReference, Func { TurnContext, Task })} - * {@linkalso BotAdapter.RunPipeline(TurnContext, Func { TurnContext, Task })} - */ - public CompletableFuture ProcessActivity(String authHeader, ActivityImpl activity, Function callback) throws Exception { - BotAssert.ActivityNotNull(activity); - - //ClaimsIdentity claimsIdentity = await(JwtTokenValidation.validateAuthHeader(activity, authHeader, _credentialProvider)); - - //return completedFuture(await(ProcessActivity(claimsIdentity, activity, callback))); - return completedFuture(null); - } - - public CompletableFuture ProcessActivity(ClaimsIdentity identity, ActivityImpl activity, Consumer callback) throws Exception { - BotAssert.ActivityNotNull(activity); - - try (TurnContextImpl context = new TurnContextImpl(this, activity)) { - context.getServices().Add("BotIdentity", identity); - - ConnectorClient connectorClient = this.CreateConnectorClientAsync(activity.serviceUrl(), identity).join(); - // TODO: Verify key that C# uses - context.getServices().Add("ConnectorClient", connectorClient); - - super.RunPipeline(context, callback); - - // Handle Invoke scenarios, which deviate from the request/response model in that - // the Bot will return a specific body and return code. - if (activity.type() == ActivityTypes.INVOKE) { - Activity invokeResponse = context.getServices().Get(InvokeReponseKey); - if (invokeResponse == null) { - // ToDo: Trace Here - throw new IllegalStateException("Bot failed to return a valid 'invokeResponse' activity."); - } else { - return completedFuture((InvokeResponse) invokeResponse.value()); - } - } - - // For all non-invoke scenarios, the HTTP layers above don't have to mess - // withthe Body and return codes. - return null; - } - } - - /** - * Sends activities to the conversation. - * - * @param context The context object for the turn. - * @param activities The activities to send. - * @return A task that represents the work queued to execute. - * If the activities are successfully sent, the task result contains - * an array of {@link ResourceResponse} objects containing the IDs that - * the receiving channel assigned to the activities. - * {@linkalso TurnContext.OnSendActivities(SendActivitiesHandler)} - */ - public ResourceResponse[] SendActivities(TurnContext context, Activity[] activities) throws InterruptedException { - if (context == null) { - throw new IllegalArgumentException("context"); - } - - if (activities == null) { - throw new IllegalArgumentException("activities"); - } - - if (activities.length == 0) { - throw new IllegalArgumentException("Expecting one or more activities, but the array was empty."); - } - - ResourceResponse[] responses = new ResourceResponse[activities.length]; - - /* - * 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]; - ResourceResponse response = new ResourceResponse(); - - if (activity.type().toString().equals("delay")) { - // The Activity Schema doesn't have a delay type build in, so it's simulated - // here in the Bot. This matches the behavior in the Node connector. - int delayMs = (int) activity.value(); - Thread.sleep(delayMs); - //await(Task.Delay(delayMs)); - // No need to create a response. One will be created below. - } else if (activity.type().toString().equals("invokeResponse")) // Aligning name with Node - { - context.getServices().Add(InvokeReponseKey, activity); - // No need to create a response. One will be created below. - } else if (activity.type() == ActivityTypes.TRACE && !activity.channelId().equals("emulator")) { - // if it is a Trace activity we only send to the channel if it's the emulator. - } else if (!StringUtils.isEmpty(activity.replyToId())) { - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - response = connectorClient.conversations().replyToActivity(activity.conversation().id(), activity.id(), activity); - } else { - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - response = connectorClient.conversations().sendToConversation(activity.conversation().id(), activity); - } - - // If No response is set, then defult to a "simple" response. This can't really be done - // above, as there are cases where the ReplyTo/SendTo methods will also return null - // (See below) so the check has to happen here. - - // Note: In addition to the Invoke / Delay / Activity cases, this code also applies - // with Skype and Teams with regards to typing events. When sending a typing event in - // these channels they do not return a RequestResponse which causes the bot to blow up. - // https://github.com/Microsoft/botbuilder-dotnet/issues/460 - // bug report : https://github.com/Microsoft/botbuilder-dotnet/issues/465 - if (response == null) { - response = new ResourceResponse().withId((activity.id() == null) ? "" : activity.id()); - } - - responses[index] = response; - } - - return responses; - } - - /** - * Replaces an existing activity in the conversation. - * - * @param context The context object for the turn. - * @param activity 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 - * channel assigned to the activity. - *

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

- * {@linkalso TurnContext.OnUpdateActivity(UpdateActivityHandler)} - */ - @Override - public ResourceResponse UpdateActivity(TurnContext context, Activity activity) { - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - // TODO String conversationId, String activityId, Activity activity) - return connectorClient.conversations().updateActivity(activity.conversation().id(), activity.id(), activity); - } - - /** - * Deletes an existing activity in the conversation. - * - * @param context The context object for the turn. - * @param reference Conversation reference for the activity to delete. - * @return A task that represents the work queued to execute. - * The {@link ConversationReference.ActivityId} of the conversation - * reference identifies the activity to delete. - * {@linkalso TurnContext.OnDeleteActivity(DeleteActivityHandler)} - */ - public void DeleteActivity(TurnContext context, ConversationReference reference) { - ConnectorClientImpl connectorClient = context.getServices().Get("ConnectorClient"); - try { - connectorClient.conversations().deleteConversationMemberFuture(reference.conversation().id(), reference.activityId()).join(); - } catch (ExecutionException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Failed deleting activity (%s)", e.toString())); - } catch (InterruptedException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Failed deleting activity (%s)", e.toString())); - } - return; - } - - /** - * Deletes a member from the current conversation - * - * @param context The context object for the turn. - * @param memberId ID of the member to delete from the conversation - * @return - */ - public void DeleteConversationMember(TurnContextImpl context, String memberId) { - if (context.getActivity().conversation() == null) - throw new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation"); - - if (StringUtils.isEmpty(context.getActivity().conversation().id())) - throw new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation.id"); - - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - - String conversationId = context.getActivity().conversation().id(); - - // TODO: - //await (connectorClient.conversations().DeleteConversationMemberAsync(conversationId, memberId)); - return; - } - - /** - * Lists the members of a given activity. - * - * @param context The context object for the turn. - * @param activityId (Optional) Activity ID to enumerate. If not specified the current activities ID will be used. - * @return List of Members of the activity - */ - public CompletableFuture> GetActivityMembers(TurnContextImpl context) { - return GetActivityMembers(context, null); - } - - public CompletableFuture> GetActivityMembers(TurnContextImpl context, String activityId) { - // If no activity was passed in, use the current activity. - if (activityId == null) - activityId = context.getActivity().id(); - - if (context.getActivity().conversation() == null) - throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation"); - - if (StringUtils.isEmpty((context.getActivity().conversation().id()))) - throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id"); - - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - String conversationId = context.getActivity().conversation().id(); - - // TODO: - //List accounts = await(connectorClient.conversations().GetActivityMembersAsync(conversationId, activityId)); - - return completedFuture(null); - } - - /** - * Lists the members of the current conversation. - * - * @param context The context object for the turn. - * @return List of Members of the current conversation - */ - public CompletableFuture> GetConversationMembers(TurnContextImpl context) { - if (context.getActivity().conversation() == null) - throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation"); - - if (StringUtils.isEmpty(context.getActivity().conversation().id())) - throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id"); - - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - String conversationId = context.getActivity().conversation().id(); - - // TODO - //List accounts = await(connectorClient.conversations().getConversationMembersAsync(conversationId)); - return completedFuture(null); - } - - /** - * Lists the Conversations in which this bot has participated for a given channel server. The - * channel server returns results in pages and each page will include a `continuationToken` - * that can be used to fetch the next page of results from the server. - * - * @param serviceUrl The URL of the channel server to query. This can be retrieved - * from `context.activity.serviceUrl`. - * @param credentials The credentials needed for the Bot to connect to the.services(). - * @param continuationToken (Optional) token used to fetch the next page of results - * from the channel server. This should be left as `null` to retrieve the first page - * of results. - * @return List of Members of the current conversation - *

- * This overload may be called from outside the context of a conversation, as only the - * Bot's ServiceUrl and credentials are required. - */ - public CompletableFuture GetConversations(String serviceUrl, MicrosoftAppCredentials credentials) throws MalformedURLException, URISyntaxException { - return GetConversations(serviceUrl, credentials, null); - } - - public CompletableFuture GetConversations(String serviceUrl, MicrosoftAppCredentials credentials, String continuationToken) throws MalformedURLException, URISyntaxException { - if (StringUtils.isEmpty(serviceUrl)) - throw new IllegalArgumentException("serviceUrl"); - - if (credentials == null) - throw new IllegalArgumentException("credentials"); - - ConnectorClient connectorClient = this.CreateConnectorClient(serviceUrl, credentials); - // TODO - //ConversationsResult results = await(connectorClient.conversations().getConversationsAsync(continuationToken)); - return completedFuture(null); - } - - /** - * Lists the Conversations in which this bot has participated for a given channel server. The - * channel server returns results in pages and each page will include a `continuationToken` - * that can be used to fetch the next page of results from the server. - * - * @param context The context object for the turn. - * @param continuationToken (Optional) token used to fetch the next page of results - * from the channel server. This should be left as `null` to retrieve the first page - * of results. - * @return List of Members of the current conversation - *

- * This overload may be called during standard Activity processing, at which point the Bot's - * service URL and credentials that are part of the current activity processing pipeline - * will be used. - */ - public CompletableFuture GetConversations(TurnContextImpl context) { - return GetConversations(context, null); - } - - public CompletableFuture GetConversations(TurnContextImpl context, String continuationToken) { - ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); - // TODO - //ConversationsResult results = await(connectorClient.conversations().getConversationsAsync()); - return completedFuture(null); - } - - - /** - * Attempts to retrieve the token for a user that's in a login flow. - * - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. - * @param magicCode (Optional) Optional user entered code to validate. - * @return Token Response - */ - public CompletableFuture GetUserToken(TurnContextImpl context, String connectionName, String magicCode) { - BotAssert.ContextNotNull(context); - if (context.getActivity().from() == null || StringUtils.isEmpty(context.getActivity().from().id())) - throw new IllegalArgumentException("BotFrameworkAdapter.GetuserToken(): missing from or from.id"); - - if (StringUtils.isEmpty(connectionName)) - throw new IllegalArgumentException("connectionName"); - - //OAuthClient client = this.CreateOAuthApiClient(context); - //return await(client.GetUserTokenAsync(context.getActivity().from().id(), connectionName, magicCode)); - return completedFuture(null); - } - - /** - * Get the raw signin link to be sent to the user for signin for a connection name. - * - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. - * @return - */ - public CompletableFuture GetOauthSignInLink(TurnContextImpl context, String connectionName) { - BotAssert.ContextNotNull(context); - if (StringUtils.isEmpty(connectionName)) - throw new IllegalArgumentException("connectionName"); - - //OAuthClient client = this.CreateOAuthApiClient(context); - //return await(client.GetSignInLinkAsync(context.getActivity(), connectionName)); - return completedFuture(null); - } - - /** - * Signs the user out with the token server. - * - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. - * @return - */ - public CompletableFuture SignOutUser(TurnContextImpl context, String connectionName) { - BotAssert.ContextNotNull(context); - if (StringUtils.isEmpty(connectionName)) - throw new IllegalArgumentException("connectionName"); - - //OAuthClient client = this.CreateOAuthApiClient(context); - //await(client.SignOutUserAsync(context.Activity.From.Id, connectionName)); - return completedFuture(null); - } - - /** - * Creates a conversation on the specified channel. - * - * @param channelId The ID for the channel. - * @param serviceUrl The channel's service URL endpoint. - * @param credentials The application credentials for the bot. - * @param conversationParameters The conversation information to use to - * create the conversation. - * @param callback The method to call for the resulting bot turn. - * @return A task that represents the work queued to execute. - * To start a conversation, your bot must know its account information - * and the user's account information on that channel. - * Most channels only support initiating a direct message (non-group) conversation. - *

The adapter attempts to create a new conversation on the channel, and - * then sends a {@code conversationUpdate} activity through its middleware pipeline - * to the {@code callback} method.

- *

If the conversation is established with the - * specified users, the ID of the activity's {@link Activity.Conversation} - * will contain the ID of the new conversation.

- */ - public CompletableFuture CreateConversation(String channelId, String serviceUrl, MicrosoftAppCredentials - credentials, ConversationParameters conversationParameters, Consumer callback) throws Exception { - // Validate serviceUrl - can throw - URI uri = new URI(serviceUrl); - return CompletableFuture.runAsync(() -> { - ConnectorClient connectorClient = null; - try { - connectorClient = this.CreateConnectorClient(serviceUrl, credentials); - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad serviceUrl: %s", serviceUrl)); - } catch (URISyntaxException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad serviceUrl: %s", serviceUrl)); - } - - Conversations conv = connectorClient.conversations(); - List results = null; - if (conv instanceof ConversationsImpl) { - ConversationsImpl convImpl = (ConversationsImpl) conv; - results = convImpl.CreateConversationAsync(conversationParameters).join(); - } else { - results = new ArrayList(); - results.add(conv.createConversation(conversationParameters)); - } - if (results.size() == 1) { - - ConversationResourceResponse result = results.get(0); - // Create a conversation update activity to represent the result. - - ConversationUpdateActivity conversationUpdate = (ConversationUpdateActivity) MessageActivity.CreateConversationUpdateActivity() - .withChannelId(channelId) - .withTopicName(conversationParameters.topicName()) - .withServiceUrl(serviceUrl) - .withMembersAdded(conversationParameters.members()) - .withId((result.activityId() != null) ? result.activityId() : UUID.randomUUID().toString()) - .withConversation(new ConversationAccount().withId(result.id())) - .withRecipient(conversationParameters.bot()); - - try (TurnContextImpl context = new TurnContextImpl(this, conversationUpdate)) { - try { - this.RunPipeline(context, callback); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Running pipeline failed : %s", e)); - } - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Turn Context Error: %s", e)); - } - } else { - // Should never happen - throw new RuntimeException(String.format("Conversations create issue - returned %d conversations", results.size())); - } - }); - - } - - protected CompletableFuture TrySetEmulatingOAuthCards(TurnContext turnContext) { - if (!isEmulatingOAuthCards && - turnContext.getActivity().channelId().equals("emulator") && - (_credentialProvider.isAuthenticationDisabledAsync().join())) { - isEmulatingOAuthCards = true; - } - return completedFuture(isEmulatingOAuthCards); - - } - - protected OAuthClient CreateOAuthApiClient(TurnContext context) throws MalformedURLException, URISyntaxException { - ConnectorClientImpl client = context.getServices().Get("ConnectorClient"); - if (client == null) { - throw new IllegalArgumentException("CreateOAuthApiClient: OAuth requires a valid ConnectorClient instance"); - } - if (isEmulatingOAuthCards) { - return new OAuthClient(client, context.getActivity().serviceUrl()); - } - return new OAuthClient(client, AuthenticationConstants.OAuthUrl); - } - - /** - * Creates the connector client asynchronous. - * - * @param serviceUrl The service URL. - * @param claimsIdentity The claims identity. - * @return ConnectorClient instance. - * @throws UnsupportedOperationException ClaimsIdemtity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off. - */ - private CompletableFuture CreateConnectorClientAsync(String serviceUrl, ClaimsIdentity claimsIdentity) { - - return CompletableFuture.supplyAsync(() -> { - if (claimsIdentity == null) { - throw new UnsupportedOperationException("ClaimsIdentity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off."); - } - - // For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For - // unauthenticated requests we have anonymous identity provided auth is disabled. - if (claimsIdentity.claims() == null) { - try { - return CreateConnectorClient(serviceUrl); - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Invalid Service URL: %s", serviceUrl)); - } catch (URISyntaxException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Invalid Service URL: %s", serviceUrl)); - } - } - - // For Activities coming from Emulator AppId claim contains the Bot's AAD AppId. - // For anonymous requests (requests with no header) appId is not set in claims. - - Map.Entry botAppIdClaim = claimsIdentity.claims().entrySet().stream() - .filter(claim -> claim.getKey() == AuthenticationConstants.AudienceClaim) - .findFirst() - .orElse(null); - if (botAppIdClaim == null) { - botAppIdClaim = claimsIdentity.claims().entrySet().stream() - .filter(claim -> claim.getKey() == AuthenticationConstants.AppIdClaim) - .findFirst() - .orElse(null); - } - - if (botAppIdClaim != null) { - String botId = botAppIdClaim.getValue(); - MicrosoftAppCredentials appCredentials = this.GetAppCredentialsAsync(botId).join(); - try { - return this.CreateConnectorClient(serviceUrl, appCredentials); - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); - } catch (URISyntaxException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); - } - } else { - try { - return this.CreateConnectorClient(serviceUrl); - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); - } catch (URISyntaxException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); - } - } - }); - - } - - /** - * Creates the connector client. - * - * @param serviceUrl The service URL. - * @param appCredentials The application credentials for the bot. - * @return Connector client instance. - */ - private ConnectorClient CreateConnectorClient(String serviceUrl) throws MalformedURLException, URISyntaxException { - return CreateConnectorClient(serviceUrl, null); - } - - private ConnectorClient CreateConnectorClient(String serviceUrl, MicrosoftAppCredentials appCredentials) throws MalformedURLException, URISyntaxException { - ConnectorClientImpl connectorClient = null; - if (appCredentials != null) { - connectorClient = new ConnectorClientImpl(new URI(serviceUrl).toURL().toString(), appCredentials); - } - // TODO: Constructor necessary? -// else { -// -// connectorClient = new ConnectorClientImpl(new URI(serviceUrl).toURL().toString()); -// } - - if (this.connectorClientRetryStrategy != null) - connectorClient.withRestRetryStrategy(this.connectorClientRetryStrategy); - - - return connectorClient; - - } - - /** - * Gets the application credentials. App Credentials are cached so as to ensure we are not refreshing - * token everytime. - * - * @param appId The application identifier (AAD Id for the bot). - * @return App credentials. - */ - private CompletableFuture GetAppCredentialsAsync(String appId) { - CompletableFuture result = CompletableFuture.supplyAsync(() -> { - if (appId == null) { - return MicrosoftAppCredentials.Empty; - } - if (this.appCredentialMap.containsKey(appId)) - return this.appCredentialMap.get(appId); - String appPassword = this._credentialProvider.getAppPasswordAsync(appId).join(); - MicrosoftAppCredentials appCredentials = new MicrosoftAppCredentials(appId, appPassword); - this.appCredentialMap.put(appId, appCredentials); - return appCredentials; - - }); - return result; - } - -} +package com.microsoft.bot.builder; + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.connector.authentication.*; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.connector.rest.RestConversations; +import com.microsoft.bot.schema.ActivityImpl; +import com.microsoft.bot.schema.models.*; +import com.microsoft.rest.retry.RetryStrategy; +import org.apache.commons.lang3.StringUtils; +import sun.net.www.http.HttpClient; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +/** + * A bot adapter that can connect a bot to a service endpoint. + * The bot adapter encapsulates authentication processes and sends + * activities to and receives activities from the Bot Connector Service. When your + * bot receives an activity, the adapter creates a context object, passes it to your + * bot's application logic, and sends responses back to the user's channel. + *

Use {@link Use(Middleware)} to add {@link Middleware} objects + * to your adapter’s middleware collection. The adapter processes and directs + * incoming activities in through the bot middleware pipeline to your bot’s logic + * and then back out again. As each activity flows in and out of the bot, each piece + * of middleware can inspect or act upon the activity, both before and after the bot + * logic runs.

+ *

+ * {@linkalso TurnContext} + * {@linkalso Activity} + * {@linkalso Bot} + * {@linkalso Middleware} + */ +public class BotFrameworkAdapter extends BotAdapter { + private final CredentialProvider _credentialProvider; + + private final RetryStrategy connectorClientRetryStrategy; + private Map appCredentialMap = new HashMap(); + + private final String InvokeReponseKey = "BotFrameworkAdapter.InvokeResponse"; + private boolean isEmulatingOAuthCards = false; + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, + * using a credential provider. + * + * @param credentialProvider The credential provider. + * @param connectorClientRetryStrategy Retry strategy for retrying HTTP operations. + * @param httpClient The HTTP client. + * @param middleware The middleware to initially add to the adapter. + * @throws IllegalArgumentException {@code credentialProvider} is {@code null}. + * Use a {@link MiddlewareSet} object to add multiple middleware + * components in the conustructor. Use the {@link Use(Middleware)} method to + * add additional middleware to the adapter after construction. + */ + public BotFrameworkAdapter(CredentialProvider credentialProvider) { + this(credentialProvider, null, null, null); + } + + public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy) { + this(credentialProvider, connectorClientRetryStrategy, null, null); + } + + public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient) { + this(credentialProvider, connectorClientRetryStrategy, httpClient, null); + } + + public BotFrameworkAdapter(CredentialProvider credentialProvider, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient, Middleware middleware) { + if (credentialProvider == null) + throw new IllegalArgumentException("credentialProvider"); + _credentialProvider = credentialProvider; + //_httpClient = httpClient ?? new HttpClient(); + this.connectorClientRetryStrategy = connectorClientRetryStrategy; + + if (middleware != null) { + this.Use(middleware); + } + } + + /** + * Sends a proactive message from the bot to a conversation. + * + * @param botAppId The application ID of the bot. This is the appId returned by Portal registration, and is + * generally found in the "MicrosoftAppId" parameter in appSettings.json. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code botAppId}, {@code reference}, or + * {@code callback} is {@code null}. + * Call this method to proactively send a message to a conversation. + * Most channels require a user to initaiate a conversation with a bot + * before the bot can send activities to the user. + *

This method registers the following.services().for the turn. + * {@link ConnectorClient}, the channel connector client to use this turn. + *

+ *

+ * This overload differers from the Node implementation by requiring the BotId to be + * passed in. The .Net code allows multiple bots to be hosted in a single adapter which + * isn't something supported by Node. + *

+ *

+ * {@linkalso ProcessActivity(String, Activity, Func { TurnContext, Task })} + * {@linkalso BotAdapter.RunPipeline(TurnContext, Func { TurnContext, Task } } + */ + @Override + public void ContinueConversation(String botAppId, ConversationReference reference, Consumer callback) throws Exception { + if (StringUtils.isEmpty(botAppId)) + throw new IllegalArgumentException("botAppId"); + + if (reference == null) + throw new IllegalArgumentException("reference"); + + if (callback == null) + throw new IllegalArgumentException("callback"); + + try (TurnContextImpl context = new TurnContextImpl(this, new ConversationReferenceHelper(reference).GetPostToBotMessage())) { + // Hand craft Claims Identity. + HashMap claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId); + claims.put(AuthenticationConstants.APPID_CLAIM, botAppId); + ClaimsIdentity claimsIdentity = new ClaimsIdentity("ExternalBearer", claims); + + context.getServices().Add("BotIdentity", claimsIdentity); + + ConnectorClient connectorClient = this.CreateConnectorClientAsync(reference.serviceUrl(), claimsIdentity).join(); + context.getServices().Add("ConnectorClient", connectorClient); + RunPipeline(context, callback); + } + return; + } + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, + * using an application ID and secret. + * + * @param appId The application ID of the bot. + * @param appPassword The application secret for the bot. + * @param connectorClientRetryStrategy Retry policy for retrying HTTP operations. + * @param httpClient The HTTP client. + * @param middleware The middleware to initially add to the adapter. + * Use a {@link MiddlewareSet} object to add multiple middleware + * components in the conustructor. Use the {@link Use(Middleware)} method to + * add additional middleware to the adapter after construction. + */ + public BotFrameworkAdapter(String appId, String appPassword) { + this(appId, appPassword, null, null, null); + } + + public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy) { + this(appId, appPassword, connectorClientRetryStrategy, null, null); + } + + public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient) { + this(appId, appPassword, connectorClientRetryStrategy, httpClient, null); + } + + public BotFrameworkAdapter(String appId, String appPassword, RetryStrategy connectorClientRetryStrategy, HttpClient httpClient, Middleware middleware) { + this(new SimpleCredentialProvider(appId, appPassword), connectorClientRetryStrategy, httpClient, middleware); + } + + /** + * Adds middleware to the adapter's pipeline. + * + * @param middleware The middleware to add. + * @return The updated adapter object. + * Middleware is added to the adapter at initialization time. + * For each turn, the adapter calls middleware in the order in which you added it. + */ + + public BotFrameworkAdapter Use(Middleware middleware) { + super._middlewareSet.Use(middleware); + return this; + } + + /** + * Creates a turn context and runs the middleware pipeline for an incoming activity. + * + * @param authHeader The HTTP authentication header of the request. + * @param activity The incoming activity. + * @param callback The code to run at the end of the adapter's middleware + * pipeline. + * @return A task that represents the work queued to execute. If the activity type + * was 'Invoke' and the corresponding key (channelId + activityId) was found + * then an InvokeResponse is returned, otherwise null is returned. + * @throws IllegalArgumentException {@code activity} is {@code null}. + * @throws UnauthorizedAccessException authentication failed. + * Call this method to reactively send a message to a conversation. + *

This method registers the following.services().for the turn. + * {@link ConnectorClient}, the channel connector client to use this turn. + *

+ *

+ * {@linkalso ContinueConversation(String, ConversationReference, Func { TurnContext, Task })} + * {@linkalso BotAdapter.RunPipeline(TurnContext, Func { TurnContext, Task })} + */ + public CompletableFuture ProcessActivity(String authHeader, ActivityImpl activity, Function callback) throws Exception { + BotAssert.ActivityNotNull(activity); + + //ClaimsIdentity claimsIdentity = await(JwtTokenValidation.validateAuthHeader(activity, authHeader, _credentialProvider)); + + //return completedFuture(await(ProcessActivity(claimsIdentity, activity, callback))); + return completedFuture(null); + } + + public CompletableFuture ProcessActivity(ClaimsIdentity identity, ActivityImpl activity, Consumer callback) throws Exception { + BotAssert.ActivityNotNull(activity); + + try (TurnContextImpl context = new TurnContextImpl(this, activity)) { + context.getServices().Add("BotIdentity", identity); + + ConnectorClient connectorClient = this.CreateConnectorClientAsync(activity.serviceUrl(), identity).join(); + // TODO: Verify key that C# uses + context.getServices().Add("ConnectorClient", connectorClient); + + super.RunPipeline(context, callback); + + // Handle Invoke scenarios, which deviate from the request/response model in that + // the Bot will return a specific body and return code. + if (activity.type() == ActivityTypes.INVOKE) { + Activity invokeResponse = context.getServices().Get(InvokeReponseKey); + if (invokeResponse == null) { + // ToDo: Trace Here + throw new IllegalStateException("Bot failed to return a valid 'invokeResponse' activity."); + } else { + return completedFuture((InvokeResponse) invokeResponse.value()); + } + } + + // For all non-invoke scenarios, the HTTP layers above don't have to mess + // withthe Body and return codes. + return null; + } + } + + /** + * Sends activities to the conversation. + * + * @param context The context object for the turn. + * @param activities The activities to send. + * @return A task that represents the work queued to execute. + * If the activities are successfully sent, the task result contains + * an array of {@link ResourceResponse} objects containing the IDs that + * the receiving channel assigned to the activities. + * {@linkalso TurnContext.OnSendActivities(SendActivitiesHandler)} + */ + public ResourceResponse[] SendActivities(TurnContext context, Activity[] activities) throws InterruptedException { + if (context == null) { + throw new IllegalArgumentException("context"); + } + + if (activities == null) { + throw new IllegalArgumentException("activities"); + } + + if (activities.length == 0) { + throw new IllegalArgumentException("Expecting one or more activities, but the array was empty."); + } + + ResourceResponse[] responses = new ResourceResponse[activities.length]; + + /* + * 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]; + ResourceResponse response = new ResourceResponse(); + + if (activity.type().toString().equals("delay")) { + // The Activity Schema doesn't have a delay type build in, so it's simulated + // here in the Bot. This matches the behavior in the Node connector. + int delayMs = (int) activity.value(); + Thread.sleep(delayMs); + //await(Task.Delay(delayMs)); + // No need to create a response. One will be created below. + } else if (activity.type().toString().equals("invokeResponse")) // Aligning name with Node + { + context.getServices().Add(InvokeReponseKey, activity); + // No need to create a response. One will be created below. + } else if (activity.type() == ActivityTypes.TRACE && !activity.channelId().equals("emulator")) { + // if it is a Trace activity we only send to the channel if it's the emulator. + } else if (!StringUtils.isEmpty(activity.replyToId())) { + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + response = connectorClient.conversations().replyToActivity(activity.conversation().id(), activity.id(), activity); + } else { + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + response = connectorClient.conversations().sendToConversation(activity.conversation().id(), activity); + } + + // If No response is set, then defult to a "simple" response. This can't really be done + // above, as there are cases where the ReplyTo/SendTo methods will also return null + // (See below) so the check has to happen here. + + // Note: In addition to the Invoke / Delay / Activity cases, this code also applies + // with Skype and Teams with regards to typing events. When sending a typing event in + // these channels they do not return a RequestResponse which causes the bot to blow up. + // https://github.com/Microsoft/botbuilder-dotnet/issues/460 + // bug report : https://github.com/Microsoft/botbuilder-dotnet/issues/465 + if (response == null) { + response = new ResourceResponse().withId((activity.id() == null) ? "" : activity.id()); + } + + responses[index] = response; + } + + return responses; + } + + /** + * Replaces an existing activity in the conversation. + * + * @param context The context object for the turn. + * @param activity 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 + * channel assigned to the activity. + *

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

+ * {@linkalso TurnContext.OnUpdateActivity(UpdateActivityHandler)} + */ + @Override + public ResourceResponse UpdateActivity(TurnContext context, Activity activity) { + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + // TODO String conversationId, String activityId, Activity activity) + return connectorClient.conversations().updateActivity(activity.conversation().id(), activity.id(), activity); + } + + /** + * Deletes an existing activity in the conversation. + * + * @param context The context object for the turn. + * @param reference Conversation reference for the activity to delete. + * @return A task that represents the work queued to execute. + * The {@link ConversationReference.ActivityId} of the conversation + * reference identifies the activity to delete. + * {@linkalso TurnContext.OnDeleteActivity(DeleteActivityHandler)} + */ + public void DeleteActivity(TurnContext context, ConversationReference reference) { + RestConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + try { + connectorClient.conversations().deleteConversationMemberFuture(reference.conversation().id(), reference.activityId()).join(); + } catch (ExecutionException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Failed deleting activity (%s)", e.toString())); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Failed deleting activity (%s)", e.toString())); + } + return; + } + + /** + * Deletes a member from the current conversation + * + * @param context The context object for the turn. + * @param memberId ID of the member to delete from the conversation + * @return + */ + public void DeleteConversationMember(TurnContextImpl context, String memberId) { + if (context.getActivity().conversation() == null) + throw new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation"); + + if (StringUtils.isEmpty(context.getActivity().conversation().id())) + throw new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation.id"); + + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + + String conversationId = context.getActivity().conversation().id(); + + // TODO: + //await (connectorClient.conversations().DeleteConversationMemberAsync(conversationId, memberId)); + return; + } + + /** + * Lists the members of a given activity. + * + * @param context The context object for the turn. + * @param activityId (Optional) Activity ID to enumerate. If not specified the current activities ID will be used. + * @return List of Members of the activity + */ + public CompletableFuture> GetActivityMembers(TurnContextImpl context) { + return GetActivityMembers(context, null); + } + + public CompletableFuture> GetActivityMembers(TurnContextImpl context, String activityId) { + // If no activity was passed in, use the current activity. + if (activityId == null) + activityId = context.getActivity().id(); + + if (context.getActivity().conversation() == null) + throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation"); + + if (StringUtils.isEmpty((context.getActivity().conversation().id()))) + throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id"); + + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + String conversationId = context.getActivity().conversation().id(); + + // TODO: + //List accounts = await(connectorClient.conversations().GetActivityMembersAsync(conversationId, activityId)); + + return completedFuture(null); + } + + /** + * Lists the members of the current conversation. + * + * @param context The context object for the turn. + * @return List of Members of the current conversation + */ + public CompletableFuture> GetConversationMembers(TurnContextImpl context) { + if (context.getActivity().conversation() == null) + throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation"); + + if (StringUtils.isEmpty(context.getActivity().conversation().id())) + throw new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id"); + + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + String conversationId = context.getActivity().conversation().id(); + + // TODO + //List accounts = await(connectorClient.conversations().getConversationMembersAsync(conversationId)); + return completedFuture(null); + } + + /** + * Lists the Conversations in which this bot has participated for a given channel server. The + * channel server returns results in pages and each page will include a `continuationToken` + * that can be used to fetch the next page of results from the server. + * + * @param serviceUrl The URL of the channel server to query. This can be retrieved + * from `context.activity.serviceUrl`. + * @param credentials The credentials needed for the Bot to connect to the.services(). + * @param continuationToken (Optional) token used to fetch the next page of results + * from the channel server. This should be left as `null` to retrieve the first page + * of results. + * @return List of Members of the current conversation + *

+ * This overload may be called from outside the context of a conversation, as only the + * Bot's ServiceUrl and credentials are required. + */ + public CompletableFuture GetConversations(String serviceUrl, MicrosoftAppCredentials credentials) throws MalformedURLException, URISyntaxException { + return GetConversations(serviceUrl, credentials, null); + } + + public CompletableFuture GetConversations(String serviceUrl, MicrosoftAppCredentials credentials, String continuationToken) throws MalformedURLException, URISyntaxException { + if (StringUtils.isEmpty(serviceUrl)) + throw new IllegalArgumentException("serviceUrl"); + + if (credentials == null) + throw new IllegalArgumentException("credentials"); + + ConnectorClient connectorClient = this.CreateConnectorClient(serviceUrl, credentials); + // TODO + //ConversationsResult results = await(connectorClient.conversations().getConversationsAsync(continuationToken)); + return completedFuture(null); + } + + /** + * Lists the Conversations in which this bot has participated for a given channel server. The + * channel server returns results in pages and each page will include a `continuationToken` + * that can be used to fetch the next page of results from the server. + * + * @param context The context object for the turn. + * @param continuationToken (Optional) token used to fetch the next page of results + * from the channel server. This should be left as `null` to retrieve the first page + * of results. + * @return List of Members of the current conversation + *

+ * This overload may be called during standard Activity processing, at which point the Bot's + * service URL and credentials that are part of the current activity processing pipeline + * will be used. + */ + public CompletableFuture GetConversations(TurnContextImpl context) { + return GetConversations(context, null); + } + + public CompletableFuture GetConversations(TurnContextImpl context, String continuationToken) { + ConnectorClient connectorClient = context.getServices().Get("ConnectorClient"); + // TODO + //ConversationsResult results = await(connectorClient.conversations().getConversationsAsync()); + return completedFuture(null); + } + + + /** + * Attempts to retrieve the token for a user that's in a login flow. + * + * @param context Context for the current turn of conversation with the user. + * @param connectionName Name of the auth connection to use. + * @param magicCode (Optional) Optional user entered code to validate. + * @return Token Response + */ + public CompletableFuture GetUserToken(TurnContextImpl context, String connectionName, String magicCode) { + BotAssert.ContextNotNull(context); + if (context.getActivity().from() == null || StringUtils.isEmpty(context.getActivity().from().id())) + throw new IllegalArgumentException("BotFrameworkAdapter.GetuserToken(): missing from or from.id"); + + if (StringUtils.isEmpty(connectionName)) + throw new IllegalArgumentException("connectionName"); + + //OAuthClient client = this.CreateOAuthApiClient(context); + //return await(client.GetUserTokenAsync(context.getActivity().from().id(), connectionName, magicCode)); + return completedFuture(null); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection name. + * + * @param context Context for the current turn of conversation with the user. + * @param connectionName Name of the auth connection to use. + * @return + */ + public CompletableFuture GetOauthSignInLink(TurnContextImpl context, String connectionName) { + BotAssert.ContextNotNull(context); + if (StringUtils.isEmpty(connectionName)) + throw new IllegalArgumentException("connectionName"); + + //OAuthClient client = this.CreateOAuthApiClient(context); + //return await(client.GetSignInLinkAsync(context.getActivity(), connectionName)); + return completedFuture(null); + } + + /** + * Signs the user out with the token server. + * + * @param context Context for the current turn of conversation with the user. + * @param connectionName Name of the auth connection to use. + * @return + */ + public CompletableFuture SignOutUser(TurnContextImpl context, String connectionName) { + BotAssert.ContextNotNull(context); + if (StringUtils.isEmpty(connectionName)) + throw new IllegalArgumentException("connectionName"); + + //OAuthClient client = this.CreateOAuthApiClient(context); + //await(client.SignOutUserAsync(context.Activity.From.Id, connectionName)); + return completedFuture(null); + } + + /** + * Creates a conversation on the specified channel. + * + * @param channelId The ID for the channel. + * @param serviceUrl The channel's service URL endpoint. + * @param credentials The application credentials for the bot. + * @param conversationParameters The conversation information to use to + * create the conversation. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + * To start a conversation, your bot must know its account information + * and the user's account information on that channel. + * Most channels only support initiating a direct message (non-group) conversation. + *

The adapter attempts to create a new conversation on the channel, and + * then sends a {@code conversationUpdate} activity through its middleware pipeline + * to the {@code callback} method.

+ *

If the conversation is established with the + * specified users, the ID of the activity's {@link Activity.Conversation} + * will contain the ID of the new conversation.

+ */ + public CompletableFuture CreateConversation(String channelId, String serviceUrl, MicrosoftAppCredentials + credentials, ConversationParameters conversationParameters, Consumer callback) throws Exception { + // Validate serviceUrl - can throw + URI uri = new URI(serviceUrl); + return CompletableFuture.runAsync(() -> { + ConnectorClient connectorClient = null; + try { + connectorClient = this.CreateConnectorClient(serviceUrl, credentials); + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad serviceUrl: %s", serviceUrl)); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad serviceUrl: %s", serviceUrl)); + } + + Conversations conv = connectorClient.conversations(); + List results = null; + if (conv instanceof RestConversations) { + RestConversations convImpl = (RestConversations) conv; + results = convImpl.CreateConversationAsync(conversationParameters).join(); + } else { + results = new ArrayList(); + results.add(conv.createConversation(conversationParameters)); + } + if (results.size() == 1) { + + ConversationResourceResponse result = results.get(0); + // Create a conversation update activity to represent the result. + + ConversationUpdateActivity conversationUpdate = (ConversationUpdateActivity) MessageActivity.CreateConversationUpdateActivity() + .withChannelId(channelId) + .withTopicName(conversationParameters.topicName()) + .withServiceUrl(serviceUrl) + .withMembersAdded(conversationParameters.members()) + .withId((result.activityId() != null) ? result.activityId() : UUID.randomUUID().toString()) + .withConversation(new ConversationAccount().withId(result.id())) + .withRecipient(conversationParameters.bot()); + + try (TurnContextImpl context = new TurnContextImpl(this, conversationUpdate)) { + try { + this.RunPipeline(context, callback); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Running pipeline failed : %s", e)); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Turn Context Error: %s", e)); + } + } else { + // Should never happen + throw new RuntimeException(String.format("Conversations create issue - returned %d conversations", results.size())); + } + }, ExecutorFactory.getExecutor()); + + } + + protected CompletableFuture TrySetEmulatingOAuthCards(TurnContext turnContext) { + if (!isEmulatingOAuthCards && + turnContext.getActivity().channelId().equals("emulator") && + (_credentialProvider.isAuthenticationDisabledAsync().join())) { + isEmulatingOAuthCards = true; + } + return completedFuture(isEmulatingOAuthCards); + + } + + protected OAuthClient CreateOAuthApiClient(TurnContext context) throws MalformedURLException, URISyntaxException { + RestConnectorClient client = context.getServices().Get("ConnectorClient"); + if (client == null) { + throw new IllegalArgumentException("CreateOAuthApiClient: OAuth requires a valid ConnectorClient instance"); + } + if (isEmulatingOAuthCards) { + return new OAuthClient(client, context.getActivity().serviceUrl()); + } + return new OAuthClient(client, AuthenticationConstants.OAUTH_URL); + } + + /** + * Creates the connector client asynchronous. + * + * @param serviceUrl The service URL. + * @param claimsIdentity The claims identity. + * @return ConnectorClient instance. + * @throws UnsupportedOperationException ClaimsIdemtity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off. + */ + private CompletableFuture CreateConnectorClientAsync(String serviceUrl, ClaimsIdentity claimsIdentity) { + + return CompletableFuture.supplyAsync(() -> { + if (claimsIdentity == null) { + throw new UnsupportedOperationException("ClaimsIdentity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off."); + } + + // For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For + // unauthenticated requests we have anonymous identity provided auth is disabled. + if (claimsIdentity.claims() == null) { + try { + return CreateConnectorClient(serviceUrl); + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Invalid Service URL: %s", serviceUrl)); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Invalid Service URL: %s", serviceUrl)); + } + } + + // For Activities coming from Emulator AppId claim contains the Bot's AAD AppId. + // For anonymous requests (requests with no header) appId is not set in claims. + + Map.Entry botAppIdClaim = claimsIdentity.claims().entrySet().stream() + .filter(claim -> claim.getKey() == AuthenticationConstants.AUDIENCE_CLAIM) + .findFirst() + .orElse(null); + if (botAppIdClaim == null) { + botAppIdClaim = claimsIdentity.claims().entrySet().stream() + .filter(claim -> claim.getKey() == AuthenticationConstants.APPID_CLAIM) + .findFirst() + .orElse(null); + } + + if (botAppIdClaim != null) { + String botId = botAppIdClaim.getValue(); + MicrosoftAppCredentials appCredentials = this.GetAppCredentialsAsync(botId).join(); + try { + return this.CreateConnectorClient(serviceUrl, appCredentials); + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); + } + } else { + try { + return this.CreateConnectorClient(serviceUrl); + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Bad Service URL: %s", serviceUrl)); + } + } + }, ExecutorFactory.getExecutor()); + + } + + /** + * Creates the connector client. + * + * @param serviceUrl The service URL. + * @param appCredentials The application credentials for the bot. + * @return Connector client instance. + */ + private ConnectorClient CreateConnectorClient(String serviceUrl) throws MalformedURLException, URISyntaxException { + return CreateConnectorClient(serviceUrl, null); + } + + private ConnectorClient CreateConnectorClient(String serviceUrl, MicrosoftAppCredentials appCredentials) throws MalformedURLException, URISyntaxException { + RestConnectorClient connectorClient = null; + if (appCredentials != null) { + connectorClient = new RestConnectorClient(new URI(serviceUrl).toURL().toString(), appCredentials); + } + // TODO: Constructor necessary? +// else { +// +// connectorClient = new ConnectorClientImpl(new URI(serviceUrl).toURL().toString()); +// } + + if (this.connectorClientRetryStrategy != null) + connectorClient.withRestRetryStrategy(this.connectorClientRetryStrategy); + + + return connectorClient; + + } + + /** + * Gets the application credentials. App Credentials are cached so as to ensure we are not refreshing + * token everytime. + * + * @param appId The application identifier (AAD Id for the bot). + * @return App credentials. + */ + private CompletableFuture GetAppCredentialsAsync(String appId) { + CompletableFuture result = CompletableFuture.supplyAsync(() -> { + if (appId == null) { + return MicrosoftAppCredentials.empty(); + } + if (this.appCredentialMap.containsKey(appId)) + return this.appCredentialMap.get(appId); + String appPassword = this._credentialProvider.getAppPasswordAsync(appId).join(); + MicrosoftAppCredentials appCredentials = new MicrosoftAppCredentials(appId, appPassword); + this.appCredentialMap.put(appId, appCredentials); + return appCredentials; + + }, ExecutorFactory.getExecutor()); + return result; + } + +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java index cf36ef092..6ddbde3da 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java @@ -1,310 +1,300 @@ -package com.microsoft.bot.builder; - - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - - -import com.microsoft.bot.schema.models.Activity; -import org.joda.time.DateTime; - -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinWorkerThread; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * The memory transcript store stores transcripts in volatile memory in a Dictionary. - *

- *

- * Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments. - */ -public class MemoryTranscriptStore implements TranscriptStore { - private HashMap>> channels = new HashMap>>(); - final ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() { - @Override - public ForkJoinWorkerThread newThread(ForkJoinPool pool) { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setName("TestFlow-" + worker.getPoolIndex()); - return worker; - } - }; - - final ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, false); - - - /** - * Logs an activity to the transcript. - * - * @param activity The activity to log. - * @return A CompletableFuture that represents the work queued to execute. - */ - public final void LogActivityAsync(Activity activity) { - if (activity == null) { - throw new NullPointerException("activity cannot be null for LogActivity()"); - } - - synchronized (this.channels) { - HashMap> channel; - if (!this.channels.containsKey(activity.channelId())) { - channel = new HashMap>(); - this.channels.put(activity.channelId(), channel); - } else { - channel = this.channels.get(activity.channelId()); - } - - ArrayList transcript = null; - - - if (!channel.containsKey(activity.conversation().id())) { - transcript = new ArrayList(); - channel.put(activity.conversation().id(), transcript); - } else { - transcript = channel.get(activity.conversation().id()); - } - - transcript.add(activity); - } - - } - - /** - * Gets from the store activities that match a set of criteria. - * - * @param channelId The ID of the channel the conversation is in. - * @param conversationId The ID of the conversation. - * @param continuationToken - * @return A task that represents the work queued to execute. - * If the task completes successfully, the result contains the matching activities. - */ - - public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken) { - return GetTranscriptActivitiesAsync(channelId, conversationId, continuationToken, null); - } - - /** - * Gets from the store activities that match a set of criteria. - * - * @param channelId The ID of the channel the conversation is in. - * @param conversationId The ID of the conversation. - * @return A task that represents the work queued to execute. - * If the task completes successfully, the result contains the matching activities. - */ - - public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId) { - return GetTranscriptActivitiesAsync(channelId, conversationId, null, null); - } - - /** - * Gets from the store activities that match a set of criteria. - * - * @param channelId The ID of the channel the conversation is in. - * @param conversationId The ID of the conversation. - * @param continuationToken - * @param startDate A cutoff date. Activities older than this date are not included. - * @return A task that represents the work queued to execute. - * If the task completes successfully, the result contains the matching activities. - */ - public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken, DateTime startDate) { - return CompletableFuture.supplyAsync(() -> { - if (channelId == null) { - throw new NullPointerException(String.format("missing %1$s", "channelId")); - } - - if (conversationId == null) { - throw new NullPointerException(String.format("missing %1$s", "conversationId")); - } - - PagedResult pagedResult = new PagedResult(); - synchronized (channels) { - HashMap> channel; - if (!channels.containsKey(channelId)) { - return pagedResult; - } - channel = channels.get(channelId); - ArrayList transcript; - - if (!channel.containsKey(conversationId)) { - return pagedResult; - } - transcript = channel.get(conversationId); - if (continuationToken != null) { - List items = transcript.stream() - .sorted(Comparator.comparing(Activity::timestamp)) - .filter(a -> a.timestamp().compareTo(startDate) >= 0) - .filter(skipwhile(a -> !a.id().equals(continuationToken))) - .skip(1) - .limit(20) - .collect(Collectors.toList()); - - pagedResult.items(items.toArray(new Activity[items.size()])); - - if (pagedResult.getItems().length == 20) { - pagedResult.withContinuationToken(items.get(items.size() - 1).id()); - } - } else { - List items = transcript.stream() - .sorted(Comparator.comparing(Activity::timestamp)) - .filter(a -> a.timestamp().compareTo((startDate == null) ? new DateTime(Long.MIN_VALUE) : startDate) >= 0) - .limit(20) - .collect(Collectors.toList()); - pagedResult.items(items.toArray(new Activity[items.size()])); - if (items.size() == 20) { - pagedResult.withContinuationToken(items.get(items.size() - 1).id()); - } - } - } - - return pagedResult; - - }, this.executor); - } - - /** - * Deletes conversation data from the store. - * - * @param channelId The ID of the channel the conversation is in. - * @param conversationId The ID of the conversation to delete. - * @return A task that represents the work queued to execute. - */ - public final CompletableFuture DeleteTranscriptAsync(String channelId, String conversationId) { - return CompletableFuture.runAsync(() -> { - if (channelId == null) { - throw new NullPointerException(String.format("%1$s should not be null", "channelId")); - } - - if (conversationId == null) { - throw new NullPointerException(String.format("%1$s should not be null", "conversationId")); - } - - synchronized (this.channels) { - if (!this.channels.containsKey(channelId)) { - return; - } - HashMap> channel = this.channels.get(channelId); - if (channel.containsKey(conversationId)) { - channel.remove(conversationId); - } - } - }, this.executor); - } - - /** - * Gets the conversations on a channel from the store. - * - * @param channelId The ID of the channel. - * @return A task that represents the work queued to execute. - */ - - public final CompletableFuture> ListTranscriptsAsync(String channelId) { - return ListTranscriptsAsync(channelId, null); - } - - /** - * Gets the conversations on a channel from the store. - * - * @param channelId The ID of the channel. - * @param continuationToken - * @return A task that represents the work queued to execute. - */ - - public final CompletableFuture> ListTranscriptsAsync(String channelId, String continuationToken) { - return CompletableFuture.supplyAsync(() -> { - if (channelId == null) { - throw new NullPointerException(String.format("missing %1$s", "channelId")); - } - - PagedResult pagedResult = new PagedResult(); - synchronized (channels) { - - if (!channels.containsKey(channelId)) { - return pagedResult; - } - - HashMap> channel = channels.get(channelId); - if (continuationToken != null) { - List items = channel.entrySet().stream() - .map(c -> { - OffsetDateTime offsetDateTime = null; - if (c.getValue().stream().findFirst().isPresent()) { - DateTime dt = c.getValue().stream().findFirst().get().timestamp(); - // convert to DateTime to OffsetDateTime - Instant instant = Instant.ofEpochMilli(dt.getMillis()); - ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant); - offsetDateTime = instant.atOffset(offset); - } else { - offsetDateTime = OffsetDateTime.now(); - } - return new Transcript() - .withChannelId(channelId) - .withId(c.getKey()) - .withCreated(offsetDateTime); - } - ) - .sorted(Comparator.comparing(Transcript::getCreated)) - .filter(skipwhile(c -> !c.getId().equals(continuationToken))) - .skip(1) - .limit(20) - .collect(Collectors.toList()); - pagedResult.items(items.toArray(new Transcript[items.size()])); - if (items.size() == 20) { - pagedResult.withContinuationToken(items.get(items.size() - 1).getId()); - } - } else { - - List items = channel.entrySet().stream() - .map(c -> { - OffsetDateTime offsetDateTime = null; - if (c.getValue().stream().findFirst().isPresent()) { - DateTime dt = c.getValue().stream().findFirst().get().timestamp(); - // convert to DateTime to OffsetDateTime - Instant instant = Instant.ofEpochMilli(dt.getMillis()); - ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant); - offsetDateTime = instant.atOffset(offset); - } else { - offsetDateTime = OffsetDateTime.now(); - } - return new Transcript() - .withChannelId(channelId) - .withId(c.getKey()) - .withCreated(offsetDateTime); - } - ) - .sorted(Comparator.comparing(Transcript::getCreated)) - .limit(20) - .collect(Collectors.toList()); - pagedResult.items(items.toArray(new Transcript[items.size()])); - if (items.size() == 20) { - pagedResult.withContinuationToken(items.get(items.size() - 1).getId()); - } - } - } - return pagedResult; - }, this.executor); - } - - /** - * Emulate C# SkipWhile. - * Stateful - * - * @param func1 predicate to apply - * @param type - * @return if the predicate condition is true - */ - public static Predicate skipwhile(Function func1) { - final boolean[] started = {false}; - return t -> started[0] || (started[0] = (boolean) func1.apply(t)); - } - -} \ No newline at end of file +package com.microsoft.bot.builder; + + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.schema.models.Activity; +import org.joda.time.DateTime; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * The memory transcript store stores transcripts in volatile memory in a Dictionary. + *

+ *

+ * Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments. + */ +public class MemoryTranscriptStore implements TranscriptStore { + private HashMap>> channels = new HashMap>>(); + + /** + * Logs an activity to the transcript. + * + * @param activity The activity to log. + * @return A CompletableFuture that represents the work queued to execute. + */ + public final void LogActivityAsync(Activity activity) { + if (activity == null) { + throw new NullPointerException("activity cannot be null for LogActivity()"); + } + + synchronized (this.channels) { + HashMap> channel; + if (!this.channels.containsKey(activity.channelId())) { + channel = new HashMap>(); + this.channels.put(activity.channelId(), channel); + } else { + channel = this.channels.get(activity.channelId()); + } + + ArrayList transcript = null; + + + if (!channel.containsKey(activity.conversation().id())) { + transcript = new ArrayList(); + channel.put(activity.conversation().id(), transcript); + } else { + transcript = channel.get(activity.conversation().id()); + } + + transcript.add(activity); + } + + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken + * @return A task that represents the work queued to execute. + * If the task completes successfully, the result contains the matching activities. + */ + + public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken) { + return GetTranscriptActivitiesAsync(channelId, conversationId, continuationToken, null); + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @return A task that represents the work queued to execute. + * If the task completes successfully, the result contains the matching activities. + */ + + public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId) { + return GetTranscriptActivitiesAsync(channelId, conversationId, null, null); + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken + * @param startDate A cutoff date. Activities older than this date are not included. + * @return A task that represents the work queued to execute. + * If the task completes successfully, the result contains the matching activities. + */ + public final CompletableFuture> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken, DateTime startDate) { + return CompletableFuture.supplyAsync(() -> { + if (channelId == null) { + throw new NullPointerException(String.format("missing %1$s", "channelId")); + } + + if (conversationId == null) { + throw new NullPointerException(String.format("missing %1$s", "conversationId")); + } + + PagedResult pagedResult = new PagedResult(); + synchronized (channels) { + HashMap> channel; + if (!channels.containsKey(channelId)) { + return pagedResult; + } + channel = channels.get(channelId); + ArrayList transcript; + + if (!channel.containsKey(conversationId)) { + return pagedResult; + } + transcript = channel.get(conversationId); + if (continuationToken != null) { + List items = transcript.stream() + .sorted(Comparator.comparing(Activity::timestamp)) + .filter(a -> a.timestamp().compareTo(startDate) >= 0) + .filter(skipwhile(a -> !a.id().equals(continuationToken))) + .skip(1) + .limit(20) + .collect(Collectors.toList()); + + pagedResult.items(items.toArray(new Activity[items.size()])); + + if (pagedResult.getItems().length == 20) { + pagedResult.withContinuationToken(items.get(items.size() - 1).id()); + } + } else { + List items = transcript.stream() + .sorted(Comparator.comparing(Activity::timestamp)) + .filter(a -> a.timestamp().compareTo((startDate == null) ? new DateTime(Long.MIN_VALUE) : startDate) >= 0) + .limit(20) + .collect(Collectors.toList()); + pagedResult.items(items.toArray(new Activity[items.size()])); + if (items.size() == 20) { + pagedResult.withContinuationToken(items.get(items.size() - 1).id()); + } + } + } + + return pagedResult; + + }, ExecutorFactory.getExecutor()); + } + + /** + * Deletes conversation data from the store. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation to delete. + * @return A task that represents the work queued to execute. + */ + public final CompletableFuture DeleteTranscriptAsync(String channelId, String conversationId) { + return CompletableFuture.runAsync(() -> { + if (channelId == null) { + throw new NullPointerException(String.format("%1$s should not be null", "channelId")); + } + + if (conversationId == null) { + throw new NullPointerException(String.format("%1$s should not be null", "conversationId")); + } + + synchronized (this.channels) { + if (!this.channels.containsKey(channelId)) { + return; + } + HashMap> channel = this.channels.get(channelId); + if (channel.containsKey(conversationId)) { + channel.remove(conversationId); + } + } + }, ExecutorFactory.getExecutor()); + } + + /** + * Gets the conversations on a channel from the store. + * + * @param channelId The ID of the channel. + * @return A task that represents the work queued to execute. + */ + + public final CompletableFuture> ListTranscriptsAsync(String channelId) { + return ListTranscriptsAsync(channelId, null); + } + + /** + * Gets the conversations on a channel from the store. + * + * @param channelId The ID of the channel. + * @param continuationToken + * @return A task that represents the work queued to execute. + */ + + public final CompletableFuture> ListTranscriptsAsync(String channelId, String continuationToken) { + return CompletableFuture.supplyAsync(() -> { + if (channelId == null) { + throw new NullPointerException(String.format("missing %1$s", "channelId")); + } + + PagedResult pagedResult = new PagedResult(); + synchronized (channels) { + + if (!channels.containsKey(channelId)) { + return pagedResult; + } + + HashMap> channel = channels.get(channelId); + if (continuationToken != null) { + List items = channel.entrySet().stream() + .map(c -> { + OffsetDateTime offsetDateTime = null; + if (c.getValue().stream().findFirst().isPresent()) { + DateTime dt = c.getValue().stream().findFirst().get().timestamp(); + // convert to DateTime to OffsetDateTime + Instant instant = Instant.ofEpochMilli(dt.getMillis()); + ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant); + offsetDateTime = instant.atOffset(offset); + } else { + offsetDateTime = OffsetDateTime.now(); + } + return new Transcript() + .withChannelId(channelId) + .withId(c.getKey()) + .withCreated(offsetDateTime); + } + ) + .sorted(Comparator.comparing(Transcript::getCreated)) + .filter(skipwhile(c -> !c.getId().equals(continuationToken))) + .skip(1) + .limit(20) + .collect(Collectors.toList()); + pagedResult.items(items.toArray(new Transcript[items.size()])); + if (items.size() == 20) { + pagedResult.withContinuationToken(items.get(items.size() - 1).getId()); + } + } else { + + List items = channel.entrySet().stream() + .map(c -> { + OffsetDateTime offsetDateTime = null; + if (c.getValue().stream().findFirst().isPresent()) { + DateTime dt = c.getValue().stream().findFirst().get().timestamp(); + // convert to DateTime to OffsetDateTime + Instant instant = Instant.ofEpochMilli(dt.getMillis()); + ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant); + offsetDateTime = instant.atOffset(offset); + } else { + offsetDateTime = OffsetDateTime.now(); + } + return new Transcript() + .withChannelId(channelId) + .withId(c.getKey()) + .withCreated(offsetDateTime); + } + ) + .sorted(Comparator.comparing(Transcript::getCreated)) + .limit(20) + .collect(Collectors.toList()); + pagedResult.items(items.toArray(new Transcript[items.size()])); + if (items.size() == 20) { + pagedResult.withContinuationToken(items.get(items.size() - 1).getId()); + } + } + } + return pagedResult; + }, ExecutorFactory.getExecutor()); + } + + /** + * Emulate C# SkipWhile. + * Stateful + * + * @param func1 predicate to apply + * @param type + * @return if the predicate condition is true + */ + public static Predicate skipwhile(Function func1) { + final boolean[] started = {false}; + return t -> started[0] || (started[0] = (boolean) func1.apply(t)); + } + +} 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 c955db952..315248a0e 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,615 +1,602 @@ -package com.microsoft.bot.builder; - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.bot.schema.ActivityImpl; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.ConversationReference; -import com.microsoft.bot.schema.models.InputHints; -import com.microsoft.bot.schema.models.ResourceResponse; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.*; - -import static com.microsoft.bot.schema.models.ActivityTypes.MESSAGE; -import static com.microsoft.bot.schema.models.ActivityTypes.TRACE; -import static java.util.stream.Collectors.toList; - -/** - * 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} - */ -public class TurnContextImpl implements TurnContext, AutoCloseable { - private final BotAdapter adapter; - private final ActivityImpl activity; - private Boolean responded = false; - - private final List onSendActivities = new ArrayList(); - private final List onUpdateActivity = new ArrayList(); - private final List onDeleteActivity = new ArrayList(); - - private final TurnContextServiceCollection turnServices; - ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() - { - @Override - public ForkJoinWorkerThread newThread(ForkJoinPool pool) - { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setName("TestFlow-" + worker.getPoolIndex()); - return worker; - } - }; - - ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, true); - - - - /** - * Creates a context object. - * - * @param adapter The adapter creating the context. - * @param activity 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, ActivityImpl activity) { - if (adapter == null) - throw new IllegalArgumentException("adapter"); - this.adapter = adapter; - if (activity == null) - throw new IllegalArgumentException("activity"); - this.activity = activity; - - turnServices = new TurnContextServiceCollectionImpl(); - } - - - /** - * Adds a response handler for send activity operations. - * - * @param handler The handler to add to the context object. - * @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, - * the adapter calls the registered handlers in the order in which they were - * added to the context object. - */ - public TurnContextImpl OnSendActivities(SendActivitiesHandler handler) { - if (handler == null) - throw new IllegalArgumentException("handler"); - - this.onSendActivities.add(handler); - return this; - } - - /** - * Adds a response handler for update activity operations. - * - * @param handler The handler to add to the context object. - * @return The updated context object. - * @throws IllegalArgumentException {@code handler} is {@code null}. - * When the context's {@link UpdateActivity(Activity)} is called, - * the adapter calls the registered handlers in the order in which they were - * added to the context object. - */ - public TurnContextImpl OnUpdateActivity(UpdateActivityHandler handler) { - if (handler == null) - throw new IllegalArgumentException("handler"); - - this.onUpdateActivity.add(handler); - return this; - } - - /** - * Adds a response handler for delete activity operations. - * - * @param handler The handler to add to the context object. - * @return The updated context object. - * @throws IllegalArgumentException {@code handler} is {@code null}. - * When the context's {@link DeleteActivity(string)} is called, - * the adapter calls the registered handlers in the order in which they were - * added to the context object. - */ - public TurnContextImpl OnDeleteActivity(DeleteActivityHandler handler) { - if (handler == null) - throw new IllegalArgumentException("handler"); - - this.onDeleteActivity.add(handler); - return this; - } - - /** - * Gets the bot adapter that created this context object. - */ - public BotAdapter getAdapter() { - return this.adapter; - } - - /** - * Gets the services registered on this context object. - */ - public TurnContextServiceCollection getServices() { - return this.turnServices; - } - - /** - * Gets the activity associated with this turn; or {@code null} when processing - * a proactive message. - */ - @Override - public Activity getActivity() { - return this.activity; - } - - /** - * 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}. - */ - public boolean getResponded() { - return this.responded; - } - - public void setResponded(boolean responded) { - if (responded == false) { - throw new IllegalArgumentException("TurnContext: cannot set 'responded' to a value of 'false'."); - } - this.responded = true; - } - - /** - * Sends a message activity to the sender of the incoming activity. - * - * @param textReplyToSend The text of the message to send. - * @param speak Optional, text to be spoken by your bot on a speech-enabled - * channel. - * @param inputHint Optional, indicates whether your bot is accepting, - * expecting, or ignoring user input after the message is delivered to the client. - * One of: "acceptingInput", "ignoringInput", or "expectingInput". - * Default is null. - * @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 ResourceResponse SendActivity(String textReplyToSend) throws Exception { - return SendActivity(textReplyToSend, null, null); - } - - @Override - public ResourceResponse SendActivity(String textReplyToSend, String speak) throws Exception { - return SendActivity(textReplyToSend, speak, null); - } - - @Override - public ResourceResponse SendActivity(String textReplyToSend, String speak, String inputHint) throws Exception { - if (StringUtils.isEmpty(textReplyToSend)) - throw new IllegalArgumentException("textReplyToSend"); - - ActivityImpl activityToSend = (ActivityImpl) new ActivityImpl() - .withType(MESSAGE) - .withText(textReplyToSend); - if (speak != null) - activityToSend.withSpeak(speak); - - if (StringUtils.isNotEmpty(inputHint)) - activityToSend.withInputHint(InputHints.fromString(inputHint)); - - return SendActivity(activityToSend); - } - - /** - * Sends an activity to the sender of the incoming activity. - * - * @param activity The activity to send. - * @return A task that represents the work queued to execute. - * @throws IllegalArgumentException {@code activity} is {@code null}. - * 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. - */ - @Override - public ResourceResponse SendActivity(Activity activity) throws Exception { - if (activity == null) - throw new IllegalArgumentException("activity"); - - System.out.printf("In SENDEACTIVITYASYNC:"); - System.out.flush(); - Activity[] activities = {activity}; - ResourceResponse[] responses; - try { - responses = SendActivities(activities); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("TurnContext:SendActivity fail %s", e.toString())); - } - if (responses == null || responses.length == 0) { - // It's possible an interceptor prevented the activity from having been sent. - // Just return an empty response in that case. - return null; - } else { - return responses[0]; - } - - } - - /** - * Sends a set of activities to the sender of the incoming activity. - * - * @param activities The activities to send. - * @return A task that represents the work queued to execute. - * If the activities are successfully sent, the task result contains - * an array of {@link ResourceResponse} objects containing the IDs that - * the receiving channel assigned to the activities. - */ - @Override - public ResourceResponse[] SendActivities(Activity[] activities) throws Exception { - // 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); - } - - // Convert the IActivities to Activies. - // Activity[] activityArray = Array.ConvertAll(activities, (input) => (Activity)input); - List activityArray = Arrays.stream(activities).map(input -> (Activity) input).collect(toList()); - - - // Create the list used by the recursive methods. - List activityList = new ArrayList(activityArray); - - Callable ActuallySendStuff = () -> { - // 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. - boolean sentNonTraceActivities = false; - if (!activityList.stream().anyMatch((a) -> a.type() == TRACE)) { - sentNonTraceActivities = true; - } - // 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. - ResourceResponse[] responses = new ResourceResponse[0]; - responses = this.getAdapter().SendActivities(this, activityList.toArray(new ActivityImpl[activityList.size()])); - 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.withId(response.id()); - } - } - - // If we actually sent something (that's not Trace), set the flag. - if (sentNonTraceActivities) { - this.setResponded(true); - } - return responses; - }; - - List act_list = new ArrayList<>(activityList); - return SendActivitiesInternal(act_list, onSendActivities.iterator(), ActuallySendStuff); - } - - /** - * Replaces an existing activity. - * - * @param activity New replacement activity. - * @return A task that represents the work queued to execute. - * @throws Microsoft.Bot.Schema.ErrorResponseException The HTTP operation failed and the response contained additional information. - * @throws System.AggregateException One or more exceptions occurred during the operation. - * 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. - *

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

- */ - @Override - public ResourceResponse UpdateActivity(Activity activity) throws Exception { - - - Callable ActuallyUpdateStuff = () -> { - return this.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) throws Exception { - if (StringUtils.isWhitespace(activityId) || activityId == null) - throw new IllegalArgumentException("activityId"); - - return CompletableFuture.runAsync(() -> { - ConversationReference cr = this.GetConversationReference(this.getActivity()); - cr.withActivityId(activityId); - - Runnable ActuallyDeleteStuff = () -> { - try { - this.getAdapter().DeleteActivity(this, cr); - } catch (ExecutionException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Failed to delete activity %s", e.toString())); - } catch (InterruptedException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Failed to delete activity %s", e.toString())); - } - return; - }; - - try { - DeleteActivityInternal(cr, onDeleteActivity.iterator(), ActuallyDeleteStuff); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Failed to delete activity %s", e.getMessage())); - } - return; - - }, executor); - - } - - /** - * Deletes an existing activity. - * - * @param conversationReference The conversation containing the activity to delete. - * @return A task that represents the work queued to execute. - * @throws Microsoft.Bot.Schema.ErrorResponseException The HTTP operation failed and the response contained additional information. - * The conversation reference's {@link ConversationReference.ActivityId} - * indicates the activity in the conversation to delete. - */ - public void DeleteActivity(ConversationReference conversationReference) throws Exception { - if (conversationReference == null) - throw new IllegalArgumentException("conversationReference"); - - Runnable ActuallyDeleteStuff = () -> { - try { - this.getAdapter().DeleteActivity(this, conversationReference); - return; - } catch (ExecutionException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - throw new RuntimeException("DeleteActivity failed"); - }; - - DeleteActivityInternal(conversationReference, onDeleteActivity.iterator(), ActuallyDeleteStuff); - return ; - } - - private ResourceResponse[] SendActivitiesInternal(List activities, Iterator sendHandlers, Callable callAtBottom) throws Exception { - if (activities == null) - throw new IllegalArgumentException("activities"); - if (sendHandlers == null) - throw new IllegalArgumentException("sendHandlers"); - - if (false == sendHandlers.hasNext()) { // No middleware to run. - if (callAtBottom != null) - return callAtBottom.call(); - return new ResourceResponse[0]; - } - - // Default to "No more Middleware after this". - Callable 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.handle(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 ResourceResponse UpdateActivityInternal(Activity activity, - Iterator updateHandlers, - Callable callAtBottom) throws Exception { - BotAssert.ActivityNotNull(activity); - if (updateHandlers == null) - throw new IllegalArgumentException("updateHandlers"); - - if (false == updateHandlers.hasNext()) { // No middleware to run. - if (callAtBottom != null) { - return callAtBottom.call(); - } - return null; - } - - // Default to "No more Middleware after this". - Callable 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()) - updateHandlers.next(); - ResourceResponse result = null; - try { - result = UpdateActivityInternal(activity, updateHandlers, callAtBottom); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("Error updating activity: %s", e.toString())); - } - activity.withId(result.id()); - return result; - }; - - // Grab the current middleware, which is the 1st element in the array, and execute it - UpdateActivityHandler toCall = updateHandlers.next(); - return toCall.handle(this, activity, next); - } - - - private void DeleteActivityInternal(ConversationReference cr, - Iterator deleteHandlers, - Runnable callAtBottom) throws Exception { - BotAssert.ConversationReferenceNotNull(cr); - if (deleteHandlers == null) - throw new IllegalArgumentException("deleteHandlers"); - - if (deleteHandlers.hasNext() == false) { // No middleware to run. - if (callAtBottom != null) { - callAtBottom.run(); - } - return; - } - - // Default to "No more Middleware after this". - Runnable 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()) - deleteHandlers.next(); - - - try { - DeleteActivityInternal(cr, deleteHandlers, callAtBottom); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("DeleteActivityInternal failed"); - } - return; - }; - - // Grab the current middleware, which is the 1st element in the array, and execute it. - DeleteActivityHandler toCall = deleteHandlers.next(); - toCall.handle(this, cr, next); - } - - /** - * 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() - .withActivityId(activity.id()) - .withUser(activity.from()) - .withBot(activity.recipient()) - .withConversation(activity.conversation()) - .withChannelId(activity.channelId()) - .withServiceUrl(activity.serviceUrl()); - - 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. - * @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) { - return ApplyConversationReference(activity, reference, false); - } - - public static Activity ApplyConversationReference(Activity activity, ConversationReference reference, boolean isIncoming) { - activity.withChannelId(reference.channelId()); - activity.withServiceUrl(reference.serviceUrl()); - activity.withConversation(reference.conversation()); - - if (isIncoming) { - activity.withFrom(reference.user()); - activity.withRecipient(reference.bot()); - if (reference.activityId() != null) - activity.withId(reference.activityId()); - } else { // Outgoing - activity.withFrom(reference.bot()); - activity.withRecipient(reference.user()); - if (reference.activityId() != null) - activity.withReplyToId(reference.activityId()); - } - return activity; - } - - public void close() throws Exception { - turnServices.close(); - } -} +package com.microsoft.bot.builder; + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.schema.ActivityImpl; +import com.microsoft.bot.schema.models.Activity; +import com.microsoft.bot.schema.models.ConversationReference; +import com.microsoft.bot.schema.models.InputHints; +import com.microsoft.bot.schema.models.ResourceResponse; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.*; + +import static com.microsoft.bot.schema.models.ActivityTypes.MESSAGE; +import static com.microsoft.bot.schema.models.ActivityTypes.TRACE; +import static java.util.stream.Collectors.toList; + +/** + * 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} + */ +public class TurnContextImpl implements TurnContext, AutoCloseable { + private final BotAdapter adapter; + private final ActivityImpl activity; + private Boolean responded = false; + + private final List onSendActivities = new ArrayList(); + private final List onUpdateActivity = new ArrayList(); + private final List onDeleteActivity = new ArrayList(); + + private final TurnContextServiceCollection turnServices; + + /** + * Creates a context object. + * + * @param adapter The adapter creating the context. + * @param activity 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, ActivityImpl activity) { + if (adapter == null) + throw new IllegalArgumentException("adapter"); + this.adapter = adapter; + if (activity == null) + throw new IllegalArgumentException("activity"); + this.activity = activity; + + turnServices = new TurnContextServiceCollectionImpl(); + } + + + /** + * Adds a response handler for send activity operations. + * + * @param handler The handler to add to the context object. + * @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, + * the adapter calls the registered handlers in the order in which they were + * added to the context object. + */ + public TurnContextImpl OnSendActivities(SendActivitiesHandler handler) { + if (handler == null) + throw new IllegalArgumentException("handler"); + + this.onSendActivities.add(handler); + return this; + } + + /** + * Adds a response handler for update activity operations. + * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws IllegalArgumentException {@code handler} is {@code null}. + * When the context's {@link UpdateActivity(Activity)} is called, + * the adapter calls the registered handlers in the order in which they were + * added to the context object. + */ + public TurnContextImpl OnUpdateActivity(UpdateActivityHandler handler) { + if (handler == null) + throw new IllegalArgumentException("handler"); + + this.onUpdateActivity.add(handler); + return this; + } + + /** + * Adds a response handler for delete activity operations. + * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws IllegalArgumentException {@code handler} is {@code null}. + * When the context's {@link DeleteActivity(string)} is called, + * the adapter calls the registered handlers in the order in which they were + * added to the context object. + */ + public TurnContextImpl OnDeleteActivity(DeleteActivityHandler handler) { + if (handler == null) + throw new IllegalArgumentException("handler"); + + this.onDeleteActivity.add(handler); + return this; + } + + /** + * Gets the bot adapter that created this context object. + */ + public BotAdapter getAdapter() { + return this.adapter; + } + + /** + * Gets the services registered on this context object. + */ + public TurnContextServiceCollection getServices() { + return this.turnServices; + } + + /** + * Gets the activity associated with this turn; or {@code null} when processing + * a proactive message. + */ + @Override + public Activity getActivity() { + return this.activity; + } + + /** + * 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}. + */ + public boolean getResponded() { + return this.responded; + } + + public void setResponded(boolean responded) { + if (responded == false) { + throw new IllegalArgumentException("TurnContext: cannot set 'responded' to a value of 'false'."); + } + this.responded = true; + } + + /** + * Sends a message activity to the sender of the incoming activity. + * + * @param textReplyToSend The text of the message to send. + * @param speak Optional, text to be spoken by your bot on a speech-enabled + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is delivered to the client. + * One of: "acceptingInput", "ignoringInput", or "expectingInput". + * Default is null. + * @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 ResourceResponse SendActivity(String textReplyToSend) throws Exception { + return SendActivity(textReplyToSend, null, null); + } + + @Override + public ResourceResponse SendActivity(String textReplyToSend, String speak) throws Exception { + return SendActivity(textReplyToSend, speak, null); + } + + @Override + public ResourceResponse SendActivity(String textReplyToSend, String speak, String inputHint) throws Exception { + if (StringUtils.isEmpty(textReplyToSend)) + throw new IllegalArgumentException("textReplyToSend"); + + ActivityImpl activityToSend = (ActivityImpl) new ActivityImpl() + .withType(MESSAGE) + .withText(textReplyToSend); + if (speak != null) + activityToSend.withSpeak(speak); + + if (StringUtils.isNotEmpty(inputHint)) + activityToSend.withInputHint(InputHints.fromString(inputHint)); + + return SendActivity(activityToSend); + } + + /** + * Sends an activity to the sender of the incoming activity. + * + * @param activity The activity to send. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code activity} is {@code null}. + * 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. + */ + @Override + public ResourceResponse SendActivity(Activity activity) throws Exception { + if (activity == null) + throw new IllegalArgumentException("activity"); + + System.out.printf("In SENDEACTIVITYASYNC:"); + System.out.flush(); + Activity[] activities = {activity}; + ResourceResponse[] responses; + try { + responses = SendActivities(activities); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("TurnContext:SendActivity fail %s", e.toString())); + } + if (responses == null || responses.length == 0) { + // It's possible an interceptor prevented the activity from having been sent. + // Just return an empty response in that case. + return null; + } else { + return responses[0]; + } + + } + + /** + * Sends a set of activities to the sender of the incoming activity. + * + * @param activities The activities to send. + * @return A task that represents the work queued to execute. + * If the activities are successfully sent, the task result contains + * an array of {@link ResourceResponse} objects containing the IDs that + * the receiving channel assigned to the activities. + */ + @Override + public ResourceResponse[] SendActivities(Activity[] activities) throws Exception { + // 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); + } + + // Convert the IActivities to Activies. + // Activity[] activityArray = Array.ConvertAll(activities, (input) => (Activity)input); + List activityArray = Arrays.stream(activities).map(input -> (Activity) input).collect(toList()); + + + // Create the list used by the recursive methods. + List activityList = new ArrayList(activityArray); + + Callable ActuallySendStuff = () -> { + // 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. + boolean sentNonTraceActivities = false; + if (!activityList.stream().anyMatch((a) -> a.type() == TRACE)) { + sentNonTraceActivities = true; + } + // 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. + ResourceResponse[] responses = new ResourceResponse[0]; + responses = this.getAdapter().SendActivities(this, activityList.toArray(new ActivityImpl[activityList.size()])); + 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.withId(response.id()); + } + } + + // If we actually sent something (that's not Trace), set the flag. + if (sentNonTraceActivities) { + this.setResponded(true); + } + return responses; + }; + + List act_list = new ArrayList<>(activityList); + return SendActivitiesInternal(act_list, onSendActivities.iterator(), ActuallySendStuff); + } + + /** + * Replaces an existing activity. + * + * @param activity New replacement activity. + * @return A task that represents the work queued to execute. + * @throws Microsoft.Bot.Schema.ErrorResponseException The HTTP operation failed and the response contained additional information. + * @throws System.AggregateException One or more exceptions occurred during the operation. + * 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. + *

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

+ */ + @Override + public ResourceResponse UpdateActivity(Activity activity) throws Exception { + + + Callable ActuallyUpdateStuff = () -> { + return this.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) throws Exception { + if (StringUtils.isWhitespace(activityId) || activityId == null) + throw new IllegalArgumentException("activityId"); + + return CompletableFuture.runAsync(() -> { + ConversationReference cr = this.GetConversationReference(this.getActivity()); + cr.withActivityId(activityId); + + Runnable ActuallyDeleteStuff = () -> { + try { + this.getAdapter().DeleteActivity(this, cr); + } catch (ExecutionException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Failed to delete activity %s", e.toString())); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Failed to delete activity %s", e.toString())); + } + return; + }; + + try { + DeleteActivityInternal(cr, onDeleteActivity.iterator(), ActuallyDeleteStuff); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Failed to delete activity %s", e.getMessage())); + } + return; + + }, ExecutorFactory.getExecutor()); + + } + + /** + * Deletes an existing activity. + * + * @param conversationReference The conversation containing the activity to delete. + * @return A task that represents the work queued to execute. + * @throws Microsoft.Bot.Schema.ErrorResponseException The HTTP operation failed and the response contained additional information. + * The conversation reference's {@link ConversationReference.ActivityId} + * indicates the activity in the conversation to delete. + */ + public void DeleteActivity(ConversationReference conversationReference) throws Exception { + if (conversationReference == null) + throw new IllegalArgumentException("conversationReference"); + + Runnable ActuallyDeleteStuff = () -> { + try { + this.getAdapter().DeleteActivity(this, conversationReference); + return; + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + throw new RuntimeException("DeleteActivity failed"); + }; + + DeleteActivityInternal(conversationReference, onDeleteActivity.iterator(), ActuallyDeleteStuff); + return ; + } + + private ResourceResponse[] SendActivitiesInternal(List activities, Iterator sendHandlers, Callable callAtBottom) throws Exception { + if (activities == null) + throw new IllegalArgumentException("activities"); + if (sendHandlers == null) + throw new IllegalArgumentException("sendHandlers"); + + if (false == sendHandlers.hasNext()) { // No middleware to run. + if (callAtBottom != null) + return callAtBottom.call(); + return new ResourceResponse[0]; + } + + // Default to "No more Middleware after this". + Callable 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.handle(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 ResourceResponse UpdateActivityInternal(Activity activity, + Iterator updateHandlers, + Callable callAtBottom) throws Exception { + BotAssert.ActivityNotNull(activity); + if (updateHandlers == null) + throw new IllegalArgumentException("updateHandlers"); + + if (false == updateHandlers.hasNext()) { // No middleware to run. + if (callAtBottom != null) { + return callAtBottom.call(); + } + return null; + } + + // Default to "No more Middleware after this". + Callable 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()) + updateHandlers.next(); + ResourceResponse result = null; + try { + result = UpdateActivityInternal(activity, updateHandlers, callAtBottom); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("Error updating activity: %s", e.toString())); + } + activity.withId(result.id()); + return result; + }; + + // Grab the current middleware, which is the 1st element in the array, and execute it + UpdateActivityHandler toCall = updateHandlers.next(); + return toCall.handle(this, activity, next); + } + + + private void DeleteActivityInternal(ConversationReference cr, + Iterator deleteHandlers, + Runnable callAtBottom) throws Exception { + BotAssert.ConversationReferenceNotNull(cr); + if (deleteHandlers == null) + throw new IllegalArgumentException("deleteHandlers"); + + if (deleteHandlers.hasNext() == false) { // No middleware to run. + if (callAtBottom != null) { + callAtBottom.run(); + } + return; + } + + // Default to "No more Middleware after this". + Runnable 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()) + deleteHandlers.next(); + + + try { + DeleteActivityInternal(cr, deleteHandlers, callAtBottom); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("DeleteActivityInternal failed"); + } + return; + }; + + // Grab the current middleware, which is the 1st element in the array, and execute it. + DeleteActivityHandler toCall = deleteHandlers.next(); + toCall.handle(this, cr, next); + } + + /** + * 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() + .withActivityId(activity.id()) + .withUser(activity.from()) + .withBot(activity.recipient()) + .withConversation(activity.conversation()) + .withChannelId(activity.channelId()) + .withServiceUrl(activity.serviceUrl()); + + 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. + * @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) { + return ApplyConversationReference(activity, reference, false); + } + + public static Activity ApplyConversationReference(Activity activity, ConversationReference reference, boolean isIncoming) { + activity.withChannelId(reference.channelId()); + activity.withServiceUrl(reference.serviceUrl()); + activity.withConversation(reference.conversation()); + + if (isIncoming) { + activity.withFrom(reference.user()); + activity.withRecipient(reference.bot()); + if (reference.activityId() != null) + activity.withId(reference.activityId()); + } else { // Outgoing + activity.withFrom(reference.bot()); + activity.withRecipient(reference.user()); + if (reference.activityId() != null) + activity.withReplyToId(reference.activityId()); + } + return activity; + } + + public void close() throws Exception { + turnServices.close(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTest.java index 8b8587848..d344ec620 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTest.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTest.java @@ -1,379 +1,379 @@ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.builder; - - - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.microsoft.bot.builder.adapters.TestAdapter; -import com.microsoft.bot.builder.adapters.TestFlow; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.bot.schema.models.ResourceResponse; -import com.microsoft.rest.RestClient; -import org.apache.commons.lang3.StringUtils; -import org.junit.Assert; -import org.junit.Test; - -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; - - -// [TestClass] -// [TestCategory("State Management")] -public class BotStateTest { - protected ConnectorClientImpl connector; - protected ChannelAccount bot; - protected ChannelAccount user; - - - protected void initializeClients(RestClient restClient, String botId, String userId) { - - connector = new ConnectorClientImpl(restClient); - bot = new ChannelAccount().withId(botId); - user = new ChannelAccount().withId(userId); - - } - - - protected void cleanUpResources() { - } - - @Test - public void State_DoNOTRememberContextState() throws ExecutionException, InterruptedException { - - TestAdapter adapter = new TestAdapter(); - - new TestFlow(adapter, (context) -> { - TestPocoState obj = StateTurnContextExtensions.GetConversationState(context); - Assert.assertNull("context.state should not exist", obj); } - ) - .Send("set value") - .StartTest(); - - } - - //@Test - public void State_RememberIStoreItemUserState() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new UserState(new MemoryStorage(), TestState::new)); - - - Consumer callback = (context) -> { - System.out.print(String.format("State_RememberIStoreItemUserState CALLBACK called..")); - System.out.flush(); - TestState userState = StateTurnContextExtensions.GetUserState(context); - Assert.assertNotNull("user state should exist", userState); - switch (context.getActivity().text()) { - case "set value": - userState.withValue("test"); - try { - ((TurnContextImpl)context).SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - Assert.assertFalse(StringUtils.isBlank(userState.value())); - ((TurnContextImpl)context).SendActivity(userState.value()); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - - }; - - new TestFlow(adapter, callback) - .Test("set value", "value saved") - .Test("get value", "test") - .StartTest(); - - } - - @Test - public void State_RememberPocoUserState() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new UserState(new MemoryStorage(), TestPocoState::new)); - new TestFlow(adapter, - (context) -> - { - TestPocoState userState = StateTurnContextExtensions.GetUserState(context); - - Assert.assertNotNull("user state should exist", userState); - switch (context.getActivity().text()) { - case "set value": - userState.setValue("test"); - try { - context.SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - Assert.assertFalse(StringUtils.isBlank(userState.getValue())); - context.SendActivity(userState.getValue()); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - .Test("set value", "value saved") - .Test("get value", "test") - .StartTest(); - } - - //@Test - public void State_RememberIStoreItemConversationState() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new ConversationState(new MemoryStorage(), TestState::new)); - new TestFlow(adapter, - (context) -> - { - TestState conversationState = StateTurnContextExtensions.GetConversationState(context); - Assert.assertNotNull("state.conversation should exist", conversationState); - switch (context.getActivity().text()) { - case "set value": - conversationState.withValue("test"); - try { - context.SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - Assert.assertFalse(StringUtils.isBlank(conversationState.value())); - context.SendActivity(conversationState.value()); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - .Test("set value", "value saved") - .Test("get value", "test") - .StartTest(); - } - - //@Test - public void State_RememberPocoConversationState() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new ConversationState(new MemoryStorage(), TestPocoState::new)); - new TestFlow(adapter, - (context) -> - { - TestPocoState conversationState = StateTurnContextExtensions.GetConversationState(context); - Assert.assertNotNull("state.conversation should exist", conversationState); - switch (context.getActivity().text()) { - case "set value": - conversationState.setValue("test"); - try { - context.SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - Assert.assertFalse(StringUtils.isBlank(conversationState.getValue())); - context.SendActivity(conversationState.getValue()); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - - .Test("set value", "value saved") - .Test("get value", "test") - .StartTest(); - } - - @Test - public void State_CustomStateManagerTest() throws ExecutionException, InterruptedException { - - String testGuid = UUID.randomUUID().toString(); - TestAdapter adapter = new TestAdapter() - .Use(new CustomKeyState(new MemoryStorage())); - new TestFlow(adapter, - (context) -> - { - CustomState customState = CustomKeyState.Get(context); - - switch (context.getActivity().text()) { - case "set value": - customState.setCustomString(testGuid); - try { - context.SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - Assert.assertFalse(StringUtils.isBlank(customState.getCustomString())); - context.SendActivity(customState.getCustomString()); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - .Test("set value", "value saved") - .Test("get value", testGuid.toString()) - .StartTest(); - } - @Test - public void State_RoundTripTypedObjectwTrace() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new ConversationState(new MemoryStorage(), TypedObject::new)); - new TestFlow(adapter, - (context) -> - { - System.out.println(String.format(">>Test Callback(tid:%s): STARTING : %s", Thread.currentThread().getId(), context.getActivity().text())); - System.out.flush(); - TypedObject conversation = StateTurnContextExtensions.GetConversationState(context); - Assert.assertNotNull("conversationstate should exist", conversation); - System.out.println(String.format(">>Test Callback(tid:%s): Text is : %s", Thread.currentThread().getId(), context.getActivity().text())); - System.out.flush(); - switch (context.getActivity().text()) { - case "set value": - conversation.withName("test"); - try { - System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(), - "value saved")); - System.out.flush(); - ResourceResponse response = context.SendActivity("value saved"); - System.out.println(String.format(">>Test Callback(tid:%s): Response Id: %s", Thread.currentThread().getId(), - response.id())); - System.out.flush(); - - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(), - "TypedObject")); - System.out.flush(); - context.SendActivity("TypedObject"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - .Turn("set value", "value saved", "Description", 50000) - .Turn("get value", "TypedObject", "Description", 50000) - .StartTest(); - - } - - - @Test - public void State_RoundTripTypedObject() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter() - .Use(new ConversationState(new MemoryStorage(), TypedObject::new)); - - new TestFlow(adapter, - (context) -> - { - TypedObject conversation = StateTurnContextExtensions.GetConversationState(context); - Assert.assertNotNull("conversationstate should exist", conversation); - switch (context.getActivity().text()) { - case "set value": - conversation.withName("test"); - try { - context.SendActivity("value saved"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - set value")); - } - break; - case "get value": - try { - context.SendActivity("TypedObject"); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(String.format("Error sending activity! - get value")); - } - break; - } - }) - .Test("set value", "value saved") - .Test("get value", "TypedObject") - .StartTest(); - - } - - @Test - public void State_UseBotStateDirectly() throws ExecutionException, InterruptedException { - TestAdapter adapter = new TestAdapter(); - - new TestFlow(adapter, - (context) -> - { - BotState botStateManager = new BotState(new MemoryStorage(), "BotState:com.microsoft.bot.builder.core.extensions.BotState", - (ctx) -> String.format("botstate/%s/%s/com.microsoft.bot.builder.core.extensions.BotState", - ctx.getActivity().channelId(), ctx.getActivity().conversation().id()), CustomState::new); - - // read initial state object - CustomState customState = null; - try { - customState = (CustomState) botStateManager.Read(context).join(); - } catch (JsonProcessingException e) { - e.printStackTrace(); - Assert.fail("Error reading custom state"); - } - - // this should be a 'new CustomState' as nothing is currently stored in storage - Assert.assertEquals(customState, new CustomState()); - - // amend property and write to storage - customState.setCustomString("test"); - try { - botStateManager.Write(context, customState).join(); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail("Could not write customstate"); - } - - // set customState to null before reading from storage - customState = null; - try { - customState = (CustomState) botStateManager.Read(context).join(); - } catch (JsonProcessingException e) { - e.printStackTrace(); - Assert.fail("Could not read customstate back"); - } - - // check object read from value has the correct value for CustomString - Assert.assertEquals(customState.getCustomString(), "test"); - } - ) - .StartTest(); - } - - -} - + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.schema.models.ChannelAccount; +import com.microsoft.bot.schema.models.ResourceResponse; +import com.microsoft.rest.RestClient; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + + +// [TestClass] +// [TestCategory("State Management")] +public class BotStateTest { + protected RestConnectorClient connector; + protected ChannelAccount bot; + protected ChannelAccount user; + + + protected void initializeClients(RestClient restClient, String botId, String userId) { + + connector = new RestConnectorClient(restClient); + bot = new ChannelAccount().withId(botId); + user = new ChannelAccount().withId(userId); + + } + + + protected void cleanUpResources() { + } + + @Test + public void State_DoNOTRememberContextState() throws ExecutionException, InterruptedException { + + TestAdapter adapter = new TestAdapter(); + + new TestFlow(adapter, (context) -> { + TestPocoState obj = StateTurnContextExtensions.GetConversationState(context); + Assert.assertNull("context.state should not exist", obj); } + ) + .Send("set value") + .StartTest(); + + } + + //@Test + public void State_RememberIStoreItemUserState() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new UserState(new MemoryStorage(), TestState::new)); + + + Consumer callback = (context) -> { + System.out.print(String.format("State_RememberIStoreItemUserState CALLBACK called..")); + System.out.flush(); + TestState userState = StateTurnContextExtensions.GetUserState(context); + Assert.assertNotNull("user state should exist", userState); + switch (context.getActivity().text()) { + case "set value": + userState.withValue("test"); + try { + ((TurnContextImpl)context).SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + Assert.assertFalse(StringUtils.isBlank(userState.value())); + ((TurnContextImpl)context).SendActivity(userState.value()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + + }; + + new TestFlow(adapter, callback) + .Test("set value", "value saved") + .Test("get value", "test") + .StartTest(); + + } + + @Test + public void State_RememberPocoUserState() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new UserState(new MemoryStorage(), TestPocoState::new)); + new TestFlow(adapter, + (context) -> + { + TestPocoState userState = StateTurnContextExtensions.GetUserState(context); + + Assert.assertNotNull("user state should exist", userState); + switch (context.getActivity().text()) { + case "set value": + userState.setValue("test"); + try { + context.SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + Assert.assertFalse(StringUtils.isBlank(userState.getValue())); + context.SendActivity(userState.getValue()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + .Test("set value", "value saved") + .Test("get value", "test") + .StartTest(); + } + + //@Test + public void State_RememberIStoreItemConversationState() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage(), TestState::new)); + new TestFlow(adapter, + (context) -> + { + TestState conversationState = StateTurnContextExtensions.GetConversationState(context); + Assert.assertNotNull("state.conversation should exist", conversationState); + switch (context.getActivity().text()) { + case "set value": + conversationState.withValue("test"); + try { + context.SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + Assert.assertFalse(StringUtils.isBlank(conversationState.value())); + context.SendActivity(conversationState.value()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + .Test("set value", "value saved") + .Test("get value", "test") + .StartTest(); + } + + //@Test + public void State_RememberPocoConversationState() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage(), TestPocoState::new)); + new TestFlow(adapter, + (context) -> + { + TestPocoState conversationState = StateTurnContextExtensions.GetConversationState(context); + Assert.assertNotNull("state.conversation should exist", conversationState); + switch (context.getActivity().text()) { + case "set value": + conversationState.setValue("test"); + try { + context.SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + Assert.assertFalse(StringUtils.isBlank(conversationState.getValue())); + context.SendActivity(conversationState.getValue()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + + .Test("set value", "value saved") + .Test("get value", "test") + .StartTest(); + } + + @Test + public void State_CustomStateManagerTest() throws ExecutionException, InterruptedException { + + String testGuid = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter() + .Use(new CustomKeyState(new MemoryStorage())); + new TestFlow(adapter, + (context) -> + { + CustomState customState = CustomKeyState.Get(context); + + switch (context.getActivity().text()) { + case "set value": + customState.setCustomString(testGuid); + try { + context.SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + Assert.assertFalse(StringUtils.isBlank(customState.getCustomString())); + context.SendActivity(customState.getCustomString()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + .Test("set value", "value saved") + .Test("get value", testGuid.toString()) + .StartTest(); + } + @Test + public void State_RoundTripTypedObjectwTrace() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage(), TypedObject::new)); + new TestFlow(adapter, + (context) -> + { + System.out.println(String.format(">>Test Callback(tid:%s): STARTING : %s", Thread.currentThread().getId(), context.getActivity().text())); + System.out.flush(); + TypedObject conversation = StateTurnContextExtensions.GetConversationState(context); + Assert.assertNotNull("conversationstate should exist", conversation); + System.out.println(String.format(">>Test Callback(tid:%s): Text is : %s", Thread.currentThread().getId(), context.getActivity().text())); + System.out.flush(); + switch (context.getActivity().text()) { + case "set value": + conversation.withName("test"); + try { + System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(), + "value saved")); + System.out.flush(); + ResourceResponse response = context.SendActivity("value saved"); + System.out.println(String.format(">>Test Callback(tid:%s): Response Id: %s", Thread.currentThread().getId(), + response.id())); + System.out.flush(); + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(), + "TypedObject")); + System.out.flush(); + context.SendActivity("TypedObject"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + .Turn("set value", "value saved", "Description", 50000) + .Turn("get value", "TypedObject", "Description", 50000) + .StartTest(); + + } + + + @Test + public void State_RoundTripTypedObject() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage(), TypedObject::new)); + + new TestFlow(adapter, + (context) -> + { + TypedObject conversation = StateTurnContextExtensions.GetConversationState(context); + Assert.assertNotNull("conversationstate should exist", conversation); + switch (context.getActivity().text()) { + case "set value": + conversation.withName("test"); + try { + context.SendActivity("value saved"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - set value")); + } + break; + case "get value": + try { + context.SendActivity("TypedObject"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(String.format("Error sending activity! - get value")); + } + break; + } + }) + .Test("set value", "value saved") + .Test("get value", "TypedObject") + .StartTest(); + + } + + @Test + public void State_UseBotStateDirectly() throws ExecutionException, InterruptedException { + TestAdapter adapter = new TestAdapter(); + + new TestFlow(adapter, + (context) -> + { + BotState botStateManager = new BotState(new MemoryStorage(), "BotState:com.microsoft.bot.builder.core.extensions.BotState", + (ctx) -> String.format("botstate/%s/%s/com.microsoft.bot.builder.core.extensions.BotState", + ctx.getActivity().channelId(), ctx.getActivity().conversation().id()), CustomState::new); + + // read initial state object + CustomState customState = null; + try { + customState = (CustomState) botStateManager.Read(context).join(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + Assert.fail("Error reading custom state"); + } + + // this should be a 'new CustomState' as nothing is currently stored in storage + Assert.assertEquals(customState, new CustomState()); + + // amend property and write to storage + customState.setCustomString("test"); + try { + botStateManager.Write(context, customState).join(); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Could not write customstate"); + } + + // set customState to null before reading from storage + customState = null; + try { + customState = (CustomState) botStateManager.Read(context).join(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + Assert.fail("Could not read customstate back"); + } + + // check object read from value has the correct value for CustomString + Assert.assertEquals(customState.getCustomString(), "test"); + } + ) + .StartTest(); + } + + +} + diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchException_MiddlewareTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchException_MiddlewareTest.java index 16d3029f5..9c29fbe77 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchException_MiddlewareTest.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchException_MiddlewareTest.java @@ -1,109 +1,110 @@ -package com.microsoft.bot.builder; - -import com.microsoft.bot.builder.adapters.TestAdapter; -import com.microsoft.bot.builder.adapters.TestFlow; -import com.microsoft.bot.schema.ActivityImpl; -import com.microsoft.bot.schema.models.Activity; -import org.junit.Assert; -import org.junit.Test; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -public class CatchException_MiddlewareTest { - - @Test - public void CatchException_TestMiddleware_TestStackedErrorMiddleware() throws ExecutionException, InterruptedException { - - TestAdapter adapter = new TestAdapter() - .Use(new CatchExceptionMiddleware(new CallOnException() { - @Override - public CompletableFuture apply(TurnContext context, T t) throws Exception { - return CompletableFuture.runAsync(() -> { - Activity activity = context.getActivity(); - if (activity instanceof ActivityImpl) { - try { - context.SendActivity(((ActivityImpl) activity).CreateReply(t.toString())); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(String.format("CatchException_TestMiddleware_TestStackedErrorMiddleware:SendActivity failed %s", e.toString())); - } - } else - Assert.assertTrue("Test was built for ActivityImpl", false); - - }); - - } - }, Exception.class)) - // Add middleware to catch NullReferenceExceptions before throwing up to the general exception instance - .Use(new CatchExceptionMiddleware(new CallOnException() { - @Override - public CompletableFuture apply(TurnContext context, T t) throws Exception { - context.SendActivity("Sorry - Null Reference Exception"); - return CompletableFuture.completedFuture(null); - } - }, NullPointerException.class)); - - - new TestFlow(adapter, (context) -> - { - - if (context.getActivity().text() == "foo") { - try { - context.SendActivity(context.getActivity().text()); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (context.getActivity().text() == "UnsupportedOperationException") { - throw new UnsupportedOperationException("Test"); - } - - } - ) - .Send("foo") - .AssertReply("foo", "passthrough") - .Send("UnsupportedOperationException") - .AssertReply("Test") - .StartTest(); - - } - -/* @Test - // [TestCategory("Middleware")] - public void CatchException_TestMiddleware_SpecificExceptionType() -{ - TestAdapter adapter = new TestAdapter() - .Use(new CatchExceptionMiddleware((context, exception) => - { - context.SendActivity("Generic Exception Caught"); - return CompletableFuture.CompletedTask; - })) - .Use(new CatchExceptionMiddleware((context, exception) => - { - context.SendActivity(exception.Message); - return CompletableFuture.CompletedTask; - })); - - - await new TestFlow(adapter, (context) => - { - if (context.Activity.AsMessageActivity().Text == "foo") - { - context.SendActivity(context.Activity.AsMessageActivity().Text); - } - - if (context.Activity.AsMessageActivity().Text == "NullReferenceException") - { - throw new NullReferenceException("Test"); - } - - return CompletableFuture.CompletedTask; - }) - .Send("foo") - .AssertReply("foo", "passthrough") - .Send("NullReferenceException") - .AssertReply("Test") - .StartTest(); -}*/ -} +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.schema.ActivityImpl; +import com.microsoft.bot.schema.models.Activity; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class CatchException_MiddlewareTest { + + @Test + public void CatchException_TestMiddleware_TestStackedErrorMiddleware() throws ExecutionException, InterruptedException { + + TestAdapter adapter = new TestAdapter() + .Use(new CatchExceptionMiddleware(new CallOnException() { + @Override + public CompletableFuture apply(TurnContext context, T t) throws Exception { + return CompletableFuture.runAsync(() -> { + Activity activity = context.getActivity(); + if (activity instanceof ActivityImpl) { + try { + context.SendActivity(((ActivityImpl) activity).CreateReply(t.toString())); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("CatchException_TestMiddleware_TestStackedErrorMiddleware:SendActivity failed %s", e.toString())); + } + } else + Assert.assertTrue("Test was built for ActivityImpl", false); + + }, ExecutorFactory.getExecutor()); + + } + }, Exception.class)) + // Add middleware to catch NullReferenceExceptions before throwing up to the general exception instance + .Use(new CatchExceptionMiddleware(new CallOnException() { + @Override + public CompletableFuture apply(TurnContext context, T t) throws Exception { + context.SendActivity("Sorry - Null Reference Exception"); + return CompletableFuture.completedFuture(null); + } + }, NullPointerException.class)); + + + new TestFlow(adapter, (context) -> + { + + if (context.getActivity().text() == "foo") { + try { + context.SendActivity(context.getActivity().text()); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (context.getActivity().text() == "UnsupportedOperationException") { + throw new UnsupportedOperationException("Test"); + } + + } + ) + .Send("foo") + .AssertReply("foo", "passthrough") + .Send("UnsupportedOperationException") + .AssertReply("Test") + .StartTest(); + + } + +/* @Test + // [TestCategory("Middleware")] + public void CatchException_TestMiddleware_SpecificExceptionType() +{ + TestAdapter adapter = new TestAdapter() + .Use(new CatchExceptionMiddleware((context, exception) => + { + context.SendActivity("Generic Exception Caught"); + return CompletableFuture.CompletedTask; + })) + .Use(new CatchExceptionMiddleware((context, exception) => + { + context.SendActivity(exception.Message); + return CompletableFuture.CompletedTask; + })); + + + await new TestFlow(adapter, (context) => + { + if (context.Activity.AsMessageActivity().Text == "foo") + { + context.SendActivity(context.Activity.AsMessageActivity().Text); + } + + if (context.Activity.AsMessageActivity().Text == "NullReferenceException") + { + throw new NullReferenceException("Test"); + } + + return CompletableFuture.CompletedTask; + }) + .Send("foo") + .AssertReply("foo", "passthrough") + .Send("NullReferenceException") + .AssertReply("Test") + .StartTest(); +}*/ +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/DictionaryStorage.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/DictionaryStorage.java index 289ced22c..b8525b1c3 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/DictionaryStorage.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/DictionaryStorage.java @@ -1,129 +1,130 @@ -package com.microsoft.bot.builder; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -/** - * Models IStorage around a dictionary - */ -public class DictionaryStorage implements Storage { - private static ObjectMapper objectMapper; - - // TODO: Object needs to be defined - private final Map memory; - private final Object syncroot = new Object(); - private int _eTag = 0; - private final String typeNameForNonEntity = "__type_name_"; - - public DictionaryStorage() { - this(null); - } - public DictionaryStorage(Map dictionary ) { - DictionaryStorage.objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .findAndRegisterModules(); - this.memory = (dictionary != null) ? dictionary : new HashMap(); - } - - public CompletableFuture Delete(String[] keys) { - synchronized (this.syncroot) { - for (String key : keys) { - Object o = this.memory.get(key); - this.memory.remove(o); - } - } - return completedFuture(null); - } - - @Override - public CompletableFuture> Read(String[] keys) throws JsonProcessingException { - return CompletableFuture.supplyAsync(() -> { - Map storeItems = new HashMap(keys.length); - synchronized (this.syncroot) { - for (String key : keys) { - if (this.memory.containsKey(key)) { - Object state = this.memory.get(key); - if (state != null) { - try { - if (!(state instanceof JsonNode)) - throw new RuntimeException("DictionaryRead failed: entry not JsonNode"); - JsonNode stateNode = (JsonNode) state; - // Check if type info is set for the class - if (!(stateNode.hasNonNull(this.typeNameForNonEntity))) { - throw new RuntimeException(String.format("DictionaryRead failed: Type info not present")); - } - String clsName = stateNode.get(this.typeNameForNonEntity).textValue(); - - // Load the class info - Class cls; - try { - cls = Class.forName(clsName); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("DictionaryRead failed: Could not load class %s", clsName)); - } - - // Populate dictionary - storeItems.put(key,DictionaryStorage.objectMapper.treeToValue(stateNode, cls )); - } catch (JsonProcessingException e) { - e.printStackTrace(); - throw new RuntimeException(String.format("DictionaryRead failed: %s", e.toString())); - } - } - } - - } - } - - return storeItems; - }); - } - - @Override - public CompletableFuture Write(Map changes) throws Exception { - synchronized (this.syncroot) { - for (Map.Entry change : changes.entrySet()) { - Object newValue = change.getValue(); - - String oldStateETag = null; // default(string); - if (this.memory.containsValue(change.getKey())) { - Map oldState = (Map) this.memory.get(change.getKey()); - if (oldState.containsValue("eTag")) { - Map.Entry eTagToken = (Map.Entry) oldState.get("eTag"); - oldStateETag = (String) eTagToken.getValue(); - } - - } - // Dictionary stores Key:JsonNode (with type information held within the JsonNode) - JsonNode newState = DictionaryStorage.objectMapper.valueToTree(newValue); - ((ObjectNode)newState).put(this.typeNameForNonEntity, newValue.getClass().getTypeName()); - - // Set ETag if applicable - if (newValue instanceof StoreItem) { - StoreItem newStoreItem = (StoreItem) newValue; - if(oldStateETag != null && newStoreItem.geteTag() != "*" && - newStoreItem.geteTag() != oldStateETag) { - throw new Exception(String.format("Etag conflict.\r\n\r\nOriginal: %s\r\nCurrent: %s", - newStoreItem.geteTag(), oldStateETag)); - } - Integer newTag = _eTag++; - ((ObjectNode)newState).put("eTag", newTag.toString()); - } - - this.memory.put((String)change.getKey(), newState); - } - } - return completedFuture(null); - } - -} - +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.connector.ExecutorFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +/** + * Models IStorage around a dictionary + */ +public class DictionaryStorage implements Storage { + private static ObjectMapper objectMapper; + + // TODO: Object needs to be defined + private final Map memory; + private final Object syncroot = new Object(); + private int _eTag = 0; + private final String typeNameForNonEntity = "__type_name_"; + + public DictionaryStorage() { + this(null); + } + public DictionaryStorage(Map dictionary ) { + DictionaryStorage.objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .findAndRegisterModules(); + this.memory = (dictionary != null) ? dictionary : new HashMap(); + } + + public CompletableFuture Delete(String[] keys) { + synchronized (this.syncroot) { + for (String key : keys) { + Object o = this.memory.get(key); + this.memory.remove(o); + } + } + return completedFuture(null); + } + + @Override + public CompletableFuture> Read(String[] keys) throws JsonProcessingException { + return CompletableFuture.supplyAsync(() -> { + Map storeItems = new HashMap(keys.length); + synchronized (this.syncroot) { + for (String key : keys) { + if (this.memory.containsKey(key)) { + Object state = this.memory.get(key); + if (state != null) { + try { + if (!(state instanceof JsonNode)) + throw new RuntimeException("DictionaryRead failed: entry not JsonNode"); + JsonNode stateNode = (JsonNode) state; + // Check if type info is set for the class + if (!(stateNode.hasNonNull(this.typeNameForNonEntity))) { + throw new RuntimeException(String.format("DictionaryRead failed: Type info not present")); + } + String clsName = stateNode.get(this.typeNameForNonEntity).textValue(); + + // Load the class info + Class cls; + try { + cls = Class.forName(clsName); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("DictionaryRead failed: Could not load class %s", clsName)); + } + + // Populate dictionary + storeItems.put(key,DictionaryStorage.objectMapper.treeToValue(stateNode, cls )); + } catch (JsonProcessingException e) { + e.printStackTrace(); + throw new RuntimeException(String.format("DictionaryRead failed: %s", e.toString())); + } + } + } + + } + } + + return storeItems; + }, ExecutorFactory.getExecutor()); + } + + @Override + public CompletableFuture Write(Map changes) throws Exception { + synchronized (this.syncroot) { + for (Map.Entry change : changes.entrySet()) { + Object newValue = change.getValue(); + + String oldStateETag = null; // default(string); + if (this.memory.containsValue(change.getKey())) { + Map oldState = (Map) this.memory.get(change.getKey()); + if (oldState.containsValue("eTag")) { + Map.Entry eTagToken = (Map.Entry) oldState.get("eTag"); + oldStateETag = (String) eTagToken.getValue(); + } + + } + // Dictionary stores Key:JsonNode (with type information held within the JsonNode) + JsonNode newState = DictionaryStorage.objectMapper.valueToTree(newValue); + ((ObjectNode)newState).put(this.typeNameForNonEntity, newValue.getClass().getTypeName()); + + // Set ETag if applicable + if (newValue instanceof StoreItem) { + StoreItem newStoreItem = (StoreItem) newValue; + if(oldStateETag != null && newStoreItem.geteTag() != "*" && + newStoreItem.geteTag() != oldStateETag) { + throw new Exception(String.format("Etag conflict.\r\n\r\nOriginal: %s\r\nCurrent: %s", + newStoreItem.geteTag(), oldStateETag)); + } + Integer newTag = _eTag++; + ((ObjectNode)newState).put("eTag", newTag.toString()); + } + + this.memory.put((String)change.getKey(), newState); + } + } + return completedFuture(null); + } + +} + diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java index d289200d8..a4083f993 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java @@ -1,534 +1,533 @@ -package com.microsoft.bot.builder; - - -import com.microsoft.bot.builder.ActionDel; -import com.microsoft.bot.builder.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.rest.RestClient; -import org.junit.Assert; -import org.junit.Test; - -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; - - -// [TestCategory("Russian Doll Middleware, Nested Middleware sets")] -public class MiddlewareSetTest extends TestBase -{ - protected ConnectorClientImpl connector; - protected ChannelAccount bot; - protected ChannelAccount user; - private boolean innerOnreceiveCalled; - - public MiddlewareSetTest() { - super(RunCondition.BOTH); - } - - @Override - protected void initializeClients(RestClient restClient, String botId, String userId) { - - connector = new ConnectorClientImpl(restClient); - bot = new ChannelAccount().withId(botId); - user = new ChannelAccount().withId(userId); - - // Test-specific stuff - innerOnreceiveCalled = false; - } - - @Override - protected void cleanUpResources() { - } - - - @Test - public void NoMiddleware() throws Exception { - MiddlewareSet m = new MiddlewareSet(); - // No middleware. Should not explode. - try { - m.ReceiveActivity(null); - Assert.assertTrue(true); - } catch (ExecutionException e) { - e.printStackTrace(); - Assert.fail("No exception expected" + e.getMessage()); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail("No exception expected" + e.getMessage()); - } - } - - - @Test - public void NestedSet_OnReceive() throws Exception { - final boolean[] wasCalled = {false}; - MiddlewareSet inner = new MiddlewareSet(); - inner.Use(new AnonymousReceiveMiddleware(new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - wasCalled[0] = true; - nd.next(); - } - })); - MiddlewareSet outer = new MiddlewareSet(); - outer.Use(inner); - try { - outer.ReceiveActivity(null); - } catch (ExecutionException e) { - Assert.fail(e.getMessage()); - return; - } catch (InterruptedException e) { - Assert.fail(e.getMessage()); - return; - } - - Assert.assertTrue("Inner Middleware Receive was not called.", wasCalled[0]); - } - - - @Test - public void NoMiddlewareWithDelegate() throws Exception { - MiddlewareSet m = new MiddlewareSet(); - final boolean wasCalled[] = {false}; - Consumer cb = context -> { - wasCalled[0] = true; - }; - // No middleware. Should not explode. - m.ReceiveActivityWithStatus(null, cb); - Assert.assertTrue("Delegate was not called", wasCalled[0]); - } - - @Test - public void OneMiddlewareItem() throws Exception { - WasCalledMiddlware simple = new WasCalledMiddlware(); - - final boolean wasCalled[] = {false}; - Consumer cb = context -> { - wasCalled[0] = true; - }; - - MiddlewareSet m = new MiddlewareSet(); - m.Use(simple); - - Assert.assertFalse(simple.getCalled()); - m.ReceiveActivityWithStatus(null, cb); - Assert.assertTrue(simple.getCalled()); - Assert.assertTrue( "Delegate was not called", wasCalled[0]); - } - - @Test - public void OneMiddlewareItemWithDelegate() throws Exception { - WasCalledMiddlware simple = new WasCalledMiddlware(); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(simple); - - Assert.assertFalse(simple.getCalled()); - m.ReceiveActivity(null); - Assert.assertTrue(simple.getCalled()); - } - - @Test(expected = IllegalStateException.class) - //[ExpectedException(typeof(InvalidOperationException))] - public void BubbleUncaughtException() throws Exception { - MiddlewareSet m = new MiddlewareSet(); - m.Use(new AnonymousReceiveMiddleware(new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws IllegalStateException { - throw new IllegalStateException("test"); - }} - )); - - m.ReceiveActivity(null); - Assert.assertFalse("Should never have gotten here", true); - } - - @Test - public void TwoMiddlewareItems() throws Exception { - WasCalledMiddlware one = new WasCalledMiddlware(); - WasCalledMiddlware two = new WasCalledMiddlware(); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - m.Use(two); - - m.ReceiveActivity(null); - Assert.assertTrue(one.getCalled()); - Assert.assertTrue(two.getCalled()); - } - - @Test - public void TwoMiddlewareItemsWithDelegate() throws Exception { - WasCalledMiddlware one = new WasCalledMiddlware(); - WasCalledMiddlware two = new WasCalledMiddlware(); - - final int called[] = {0}; - Consumer cb = (context) -> { - called[0]++; - }; - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - m.Use(two); - - m.ReceiveActivityWithStatus(null, cb); - Assert.assertTrue(one.getCalled()); - Assert.assertTrue(two.getCalled()); - Assert.assertTrue("Incorrect number of calls to Delegate", called[0] == 1 ); - } - - @Test - public void TwoMiddlewareItemsInOrder() throws Exception { - final boolean called1[] = {false}; - final boolean called2[] = {false}; - - CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { - @Override - public void CallMe() { - Assert.assertFalse( "Second Middleware was called", called2[0]); - called1[0] = true; - } - }); - - CallMeMiddlware two = new CallMeMiddlware(new ActionDel() { - @Override - public void CallMe() { - Assert.assertTrue("First Middleware was not called", called1[0]); - called2[0] = true; - } - }); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - m.Use(two); - - m.ReceiveActivity(null); - Assert.assertTrue(called1[0]); - Assert.assertTrue(called2[0]); - } - - @Test - public void Status_OneMiddlewareRan() throws Exception { - final boolean called1[] = {false}; - - CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { - @Override - public void CallMe() { - called1[0] = true; - } - }); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - - // The middlware in this pipeline calls next(), so the delegate should be called - final boolean didAllRun[] = {false}; - Consumer cb = (context) -> { - didAllRun[0] = true; - }; - m.ReceiveActivityWithStatus(null, cb); - - Assert.assertTrue(called1[0]); - Assert.assertTrue(didAllRun[0]); - } - - @Test - public void Status_RunAtEndEmptyPipeline() throws Exception { - MiddlewareSet m = new MiddlewareSet(); - final boolean didAllRun[] = {false}; - Consumer cb = (context)-> { - didAllRun[0] = true; - }; - - // This middlware pipeline has no entries. This should result in - // the status being TRUE. - m.ReceiveActivityWithStatus(null, cb); - Assert.assertTrue(didAllRun[0]); - - } - - @Test - public void Status_TwoItemsOneDoesNotCallNext() throws Exception { - final boolean called1[] = {false}; - final boolean called2[] = {false}; - - CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { - @Override - public void CallMe() { - Assert.assertFalse("Second Middleware was called", called2[0]); - called1[0] = true; - } - }); - - DoNotCallNextMiddleware two = new DoNotCallNextMiddleware(new ActionDel() { - @Override - public void CallMe() { - Assert.assertTrue("First Middleware was not called", called1[0]); - called2[0] = true; - }}); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - m.Use(two); - - boolean didAllRun[] = {false}; - Consumer cb= (context) -> { - didAllRun[0] = true; - }; - m.ReceiveActivityWithStatus(null, cb); - Assert.assertTrue(called1[0]); - Assert.assertTrue(called2[0]); - - // The 2nd middleware did not call next, so the "final" action should not have run. - Assert.assertFalse(didAllRun[0]); - } - - @Test - public void Status_OneEntryThatDoesNotCallNext() throws Exception { - final boolean called1[] = {false}; - - DoNotCallNextMiddleware one = new DoNotCallNextMiddleware(new ActionDel() { - @Override - public void CallMe() { - called1[0] = true; - } - }); - - MiddlewareSet m = new MiddlewareSet(); - m.Use(one); - - // The middleware in this pipeline DOES NOT call next(), so this must not be called - boolean didAllRun[] = {false}; - Consumer cb = (context) -> { - didAllRun[0] = true; - }; - m.ReceiveActivityWithStatus(null, cb); - - Assert.assertTrue(called1[0]); - - // Our "Final" action MUST NOT have been called, as the Middlware Pipeline - // didn't complete. - Assert.assertFalse(didAllRun[0]); - } - - @Test - public void AnonymousMiddleware() throws Exception { - final boolean didRun[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - MiddlewareCall mwc = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - didRun[0] = true; - nd.next(); - return; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc)); - - Assert.assertFalse(didRun[0]); - m.ReceiveActivity(null); - Assert.assertTrue(didRun[0]); - } - - @Test - public void TwoAnonymousMiddleware() throws Exception { - final boolean didRun1[] = {false}; - final boolean didRun2[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - didRun1[0] = true; - nd.next(); - return; - } - }; - - m.Use(new AnonymousReceiveMiddleware(mwc1)); - MiddlewareCall mwc2 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - didRun2[0] = true; - nd.next(); - return; - } - }; - - m.Use(new AnonymousReceiveMiddleware(mwc2)); - - m.ReceiveActivity(null); - Assert.assertTrue(didRun1[0]); - Assert.assertTrue(didRun2[0]); - } - - @Test - public void TwoAnonymousMiddlewareInOrder() throws Exception { - final boolean didRun1[] = {false}; - final boolean didRun2[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertFalse("Looks like the 2nd one has already run", didRun2[0]); - didRun1[0] = true; - nd.next(); - return; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc1)); - - MiddlewareCall mwc2 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertTrue("Looks like the 1nd one has not yet run", didRun1[0]); - didRun2[0] = true; - nd.next(); - return ; - } - }; - - m.Use(new AnonymousReceiveMiddleware(mwc2)); - - m.ReceiveActivity(null); - Assert.assertTrue(didRun1[0]); - Assert.assertTrue(didRun2[0]); - } - - @Test - public void MixedMiddlewareInOrderAnonymousFirst() throws Exception { - final boolean didRun1[] = {false}; - final boolean didRun2[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertFalse("First middleware already ran", didRun1[0]); - Assert.assertFalse("Looks like the second middleware was already run", didRun2[0]); - didRun1[0] = true; - nd.next(); - Assert.assertTrue("Second middleware should have completed running", didRun2[0]); - return ; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc1)); - - ActionDel act = new ActionDel() { - @Override - public void CallMe() { - Assert.assertTrue("First middleware should have already been called", didRun1[0]); - Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); - didRun2[0] = true; - } - }; - m.Use(new CallMeMiddlware(act)); - - m.ReceiveActivity(null); - Assert.assertTrue(didRun1[0]); - Assert.assertTrue(didRun2[0]); - } - - @Test - public void MixedMiddlewareInOrderAnonymousLast() throws Exception { - final boolean didRun1[] = {false}; - final boolean didRun2[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - - ActionDel act = new ActionDel() { - @Override - public void CallMe() { - Assert.assertFalse("First middleware should not have already been called", didRun1[0]); - Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); - didRun1[0] = true; - } - }; - m.Use(new CallMeMiddlware(act)); - - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertTrue("First middleware has not been run yet", didRun1[0]); - didRun2[0] = true; - nd.next(); - return; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc1)); - - m.ReceiveActivity(null); - Assert.assertTrue(didRun1[0]); - Assert.assertTrue(didRun2[0]); - } - - @Test - public void RunCodeBeforeAndAfter() throws Exception { - final boolean didRun1[] = {false}; - final boolean codeafter2run[] = {false}; - final boolean didRun2[] = {false}; - - MiddlewareSet m = new MiddlewareSet(); - - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertFalse("Looks like the 1st middleware has already run", didRun1[0]); - didRun1[0] = true; - nd.next(); - Assert.assertTrue("The 2nd middleware should have run now.", didRun1[0]); - codeafter2run[0] = true; - return ; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc1)); - - MiddlewareCall mwc2 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { - Assert.assertTrue("Looks like the 1st middleware has not been run", didRun1[0]); - Assert.assertFalse("The code that runs after middleware 2 is complete has already run.", codeafter2run[0]); - didRun2[0] = true; - nd.next(); - return ; - } - }; - m.Use(new AnonymousReceiveMiddleware(mwc2)); - - m.ReceiveActivity(null); - Assert.assertTrue(didRun1[0]); - Assert.assertTrue(didRun2[0]); - Assert.assertTrue(codeafter2run[0]); - } - - @Test - public void CatchAnExceptionViaMiddlware() throws Exception { - MiddlewareSet m = new MiddlewareSet(); - final boolean caughtException[] = {false}; - - MiddlewareCall mwc1 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws ExecutionException, InterruptedException { - try { - nd.next(); - Assert.assertTrue("Should not get here", false); - - } - catch (InterruptedException ex) { - System.out.println("Here isi the exception message" + ex.getMessage()); - System.out.flush(); - Assert.assertTrue(ex.getMessage() == "test"); - - caughtException[0] = true; - } catch (Exception e) { - Assert.assertTrue("Should not get here" + e.getMessage(), false); - } - return ; - }}; - - m.Use(new AnonymousReceiveMiddleware(mwc1)); - - MiddlewareCall mwc2 = new MiddlewareCall() { - public void requestHandler(TurnContext tc, NextDelegate nd) throws InterruptedException { - throw new InterruptedException("test"); - } - }; - - m.Use(new AnonymousReceiveMiddleware(mwc2)); - - m.ReceiveActivity(null); - Assert.assertTrue(caughtException[0]); - } - - - -} +package com.microsoft.bot.builder; + + +import com.microsoft.bot.builder.base.TestBase; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.schema.models.ChannelAccount; +import com.microsoft.rest.RestClient; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + + +// [TestCategory("Russian Doll Middleware, Nested Middleware sets")] +public class MiddlewareSetTest extends TestBase +{ + protected RestConnectorClient connector; + protected ChannelAccount bot; + protected ChannelAccount user; + private boolean innerOnreceiveCalled; + + public MiddlewareSetTest() { + super(RunCondition.BOTH); + } + + @Override + protected void initializeClients(RestClient restClient, String botId, String userId) { + + connector = new RestConnectorClient(restClient); + bot = new ChannelAccount().withId(botId); + user = new ChannelAccount().withId(userId); + + // Test-specific stuff + innerOnreceiveCalled = false; + } + + @Override + protected void cleanUpResources() { + } + + + @Test + public void NoMiddleware() throws Exception { + MiddlewareSet m = new MiddlewareSet(); + // No middleware. Should not explode. + try { + m.ReceiveActivity(null); + Assert.assertTrue(true); + } catch (ExecutionException e) { + e.printStackTrace(); + Assert.fail("No exception expected" + e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail("No exception expected" + e.getMessage()); + } + } + + + @Test + public void NestedSet_OnReceive() throws Exception { + final boolean[] wasCalled = {false}; + MiddlewareSet inner = new MiddlewareSet(); + inner.Use(new AnonymousReceiveMiddleware(new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + wasCalled[0] = true; + nd.next(); + } + })); + MiddlewareSet outer = new MiddlewareSet(); + outer.Use(inner); + try { + outer.ReceiveActivity(null); + } catch (ExecutionException e) { + Assert.fail(e.getMessage()); + return; + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + return; + } + + Assert.assertTrue("Inner Middleware Receive was not called.", wasCalled[0]); + } + + + @Test + public void NoMiddlewareWithDelegate() throws Exception { + MiddlewareSet m = new MiddlewareSet(); + final boolean wasCalled[] = {false}; + Consumer cb = context -> { + wasCalled[0] = true; + }; + // No middleware. Should not explode. + m.ReceiveActivityWithStatus(null, cb); + Assert.assertTrue("Delegate was not called", wasCalled[0]); + } + + @Test + public void OneMiddlewareItem() throws Exception { + WasCalledMiddlware simple = new WasCalledMiddlware(); + + final boolean wasCalled[] = {false}; + Consumer cb = context -> { + wasCalled[0] = true; + }; + + MiddlewareSet m = new MiddlewareSet(); + m.Use(simple); + + Assert.assertFalse(simple.getCalled()); + m.ReceiveActivityWithStatus(null, cb); + Assert.assertTrue(simple.getCalled()); + Assert.assertTrue( "Delegate was not called", wasCalled[0]); + } + + @Test + public void OneMiddlewareItemWithDelegate() throws Exception { + WasCalledMiddlware simple = new WasCalledMiddlware(); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(simple); + + Assert.assertFalse(simple.getCalled()); + m.ReceiveActivity(null); + Assert.assertTrue(simple.getCalled()); + } + + @Test(expected = IllegalStateException.class) + //[ExpectedException(typeof(InvalidOperationException))] + public void BubbleUncaughtException() throws Exception { + MiddlewareSet m = new MiddlewareSet(); + m.Use(new AnonymousReceiveMiddleware(new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws IllegalStateException { + throw new IllegalStateException("test"); + }} + )); + + m.ReceiveActivity(null); + Assert.assertFalse("Should never have gotten here", true); + } + + @Test + public void TwoMiddlewareItems() throws Exception { + WasCalledMiddlware one = new WasCalledMiddlware(); + WasCalledMiddlware two = new WasCalledMiddlware(); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + m.Use(two); + + m.ReceiveActivity(null); + Assert.assertTrue(one.getCalled()); + Assert.assertTrue(two.getCalled()); + } + + @Test + public void TwoMiddlewareItemsWithDelegate() throws Exception { + WasCalledMiddlware one = new WasCalledMiddlware(); + WasCalledMiddlware two = new WasCalledMiddlware(); + + final int called[] = {0}; + Consumer cb = (context) -> { + called[0]++; + }; + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + m.Use(two); + + m.ReceiveActivityWithStatus(null, cb); + Assert.assertTrue(one.getCalled()); + Assert.assertTrue(two.getCalled()); + Assert.assertTrue("Incorrect number of calls to Delegate", called[0] == 1 ); + } + + @Test + public void TwoMiddlewareItemsInOrder() throws Exception { + final boolean called1[] = {false}; + final boolean called2[] = {false}; + + CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { + @Override + public void CallMe() { + Assert.assertFalse( "Second Middleware was called", called2[0]); + called1[0] = true; + } + }); + + CallMeMiddlware two = new CallMeMiddlware(new ActionDel() { + @Override + public void CallMe() { + Assert.assertTrue("First Middleware was not called", called1[0]); + called2[0] = true; + } + }); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + m.Use(two); + + m.ReceiveActivity(null); + Assert.assertTrue(called1[0]); + Assert.assertTrue(called2[0]); + } + + @Test + public void Status_OneMiddlewareRan() throws Exception { + final boolean called1[] = {false}; + + CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { + @Override + public void CallMe() { + called1[0] = true; + } + }); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + + // The middlware in this pipeline calls next(), so the delegate should be called + final boolean didAllRun[] = {false}; + Consumer cb = (context) -> { + didAllRun[0] = true; + }; + m.ReceiveActivityWithStatus(null, cb); + + Assert.assertTrue(called1[0]); + Assert.assertTrue(didAllRun[0]); + } + + @Test + public void Status_RunAtEndEmptyPipeline() throws Exception { + MiddlewareSet m = new MiddlewareSet(); + final boolean didAllRun[] = {false}; + Consumer cb = (context)-> { + didAllRun[0] = true; + }; + + // This middlware pipeline has no entries. This should result in + // the status being TRUE. + m.ReceiveActivityWithStatus(null, cb); + Assert.assertTrue(didAllRun[0]); + + } + + @Test + public void Status_TwoItemsOneDoesNotCallNext() throws Exception { + final boolean called1[] = {false}; + final boolean called2[] = {false}; + + CallMeMiddlware one = new CallMeMiddlware(new ActionDel() { + @Override + public void CallMe() { + Assert.assertFalse("Second Middleware was called", called2[0]); + called1[0] = true; + } + }); + + DoNotCallNextMiddleware two = new DoNotCallNextMiddleware(new ActionDel() { + @Override + public void CallMe() { + Assert.assertTrue("First Middleware was not called", called1[0]); + called2[0] = true; + }}); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + m.Use(two); + + boolean didAllRun[] = {false}; + Consumer cb= (context) -> { + didAllRun[0] = true; + }; + m.ReceiveActivityWithStatus(null, cb); + Assert.assertTrue(called1[0]); + Assert.assertTrue(called2[0]); + + // The 2nd middleware did not call next, so the "final" action should not have run. + Assert.assertFalse(didAllRun[0]); + } + + @Test + public void Status_OneEntryThatDoesNotCallNext() throws Exception { + final boolean called1[] = {false}; + + DoNotCallNextMiddleware one = new DoNotCallNextMiddleware(new ActionDel() { + @Override + public void CallMe() { + called1[0] = true; + } + }); + + MiddlewareSet m = new MiddlewareSet(); + m.Use(one); + + // The middleware in this pipeline DOES NOT call next(), so this must not be called + boolean didAllRun[] = {false}; + Consumer cb = (context) -> { + didAllRun[0] = true; + }; + m.ReceiveActivityWithStatus(null, cb); + + Assert.assertTrue(called1[0]); + + // Our "Final" action MUST NOT have been called, as the Middlware Pipeline + // didn't complete. + Assert.assertFalse(didAllRun[0]); + } + + @Test + public void AnonymousMiddleware() throws Exception { + final boolean didRun[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + MiddlewareCall mwc = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + didRun[0] = true; + nd.next(); + return; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc)); + + Assert.assertFalse(didRun[0]); + m.ReceiveActivity(null); + Assert.assertTrue(didRun[0]); + } + + @Test + public void TwoAnonymousMiddleware() throws Exception { + final boolean didRun1[] = {false}; + final boolean didRun2[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + didRun1[0] = true; + nd.next(); + return; + } + }; + + m.Use(new AnonymousReceiveMiddleware(mwc1)); + MiddlewareCall mwc2 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + didRun2[0] = true; + nd.next(); + return; + } + }; + + m.Use(new AnonymousReceiveMiddleware(mwc2)); + + m.ReceiveActivity(null); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void TwoAnonymousMiddlewareInOrder() throws Exception { + final boolean didRun1[] = {false}; + final boolean didRun2[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertFalse("Looks like the 2nd one has already run", didRun2[0]); + didRun1[0] = true; + nd.next(); + return; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc1)); + + MiddlewareCall mwc2 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertTrue("Looks like the 1nd one has not yet run", didRun1[0]); + didRun2[0] = true; + nd.next(); + return ; + } + }; + + m.Use(new AnonymousReceiveMiddleware(mwc2)); + + m.ReceiveActivity(null); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void MixedMiddlewareInOrderAnonymousFirst() throws Exception { + final boolean didRun1[] = {false}; + final boolean didRun2[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertFalse("First middleware already ran", didRun1[0]); + Assert.assertFalse("Looks like the second middleware was already run", didRun2[0]); + didRun1[0] = true; + nd.next(); + Assert.assertTrue("Second middleware should have completed running", didRun2[0]); + return ; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc1)); + + ActionDel act = new ActionDel() { + @Override + public void CallMe() { + Assert.assertTrue("First middleware should have already been called", didRun1[0]); + Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); + didRun2[0] = true; + } + }; + m.Use(new CallMeMiddlware(act)); + + m.ReceiveActivity(null); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void MixedMiddlewareInOrderAnonymousLast() throws Exception { + final boolean didRun1[] = {false}; + final boolean didRun2[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + + ActionDel act = new ActionDel() { + @Override + public void CallMe() { + Assert.assertFalse("First middleware should not have already been called", didRun1[0]); + Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); + didRun1[0] = true; + } + }; + m.Use(new CallMeMiddlware(act)); + + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertTrue("First middleware has not been run yet", didRun1[0]); + didRun2[0] = true; + nd.next(); + return; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc1)); + + m.ReceiveActivity(null); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void RunCodeBeforeAndAfter() throws Exception { + final boolean didRun1[] = {false}; + final boolean codeafter2run[] = {false}; + final boolean didRun2[] = {false}; + + MiddlewareSet m = new MiddlewareSet(); + + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertFalse("Looks like the 1st middleware has already run", didRun1[0]); + didRun1[0] = true; + nd.next(); + Assert.assertTrue("The 2nd middleware should have run now.", didRun1[0]); + codeafter2run[0] = true; + return ; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc1)); + + MiddlewareCall mwc2 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws Exception { + Assert.assertTrue("Looks like the 1st middleware has not been run", didRun1[0]); + Assert.assertFalse("The code that runs after middleware 2 is complete has already run.", codeafter2run[0]); + didRun2[0] = true; + nd.next(); + return ; + } + }; + m.Use(new AnonymousReceiveMiddleware(mwc2)); + + m.ReceiveActivity(null); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + Assert.assertTrue(codeafter2run[0]); + } + + @Test + public void CatchAnExceptionViaMiddlware() throws Exception { + MiddlewareSet m = new MiddlewareSet(); + final boolean caughtException[] = {false}; + + MiddlewareCall mwc1 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws ExecutionException, InterruptedException { + try { + nd.next(); + Assert.assertTrue("Should not get here", false); + + } + catch (InterruptedException ex) { + System.out.println("Here isi the exception message" + ex.getMessage()); + System.out.flush(); + Assert.assertTrue(ex.getMessage() == "test"); + + caughtException[0] = true; + } catch (Exception e) { + Assert.assertTrue("Should not get here" + e.getMessage(), false); + } + return ; + }}; + + m.Use(new AnonymousReceiveMiddleware(mwc1)); + + MiddlewareCall mwc2 = new MiddlewareCall() { + public void requestHandler(TurnContext tc, NextDelegate nd) throws InterruptedException { + throw new InterruptedException("test"); + } + }; + + m.Use(new AnonymousReceiveMiddleware(mwc2)); + + m.ReceiveActivity(null); + Assert.assertTrue(caughtException[0]); + } + + + +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java index c68956c89..e48a727bd 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java @@ -1,468 +1,469 @@ -package com.microsoft.bot.builder.adapters; - -import com.microsoft.bot.builder.TurnContext; -import com.microsoft.bot.schema.ActivityImpl; -import com.microsoft.bot.schema.models.Activity; -import org.joda.time.DateTime; -import org.junit.Assert; - -import java.lang.management.ManagementFactory; -import java.util.ArrayList; -import java.util.concurrent.*; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -public class TestFlow { - final TestAdapter adapter; - CompletableFuture testTask; - Consumer callback; - - ArrayList> tasks = new ArrayList>(); - ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() - { - @Override - public ForkJoinWorkerThread newThread(ForkJoinPool pool) - { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setName("TestFlow-" + worker.getPoolIndex()); - return worker; - } - }; - - ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, true); - - - public TestFlow(TestAdapter adapter) { - this(adapter, null); - } - - public TestFlow(TestAdapter adapter, Consumer callback) { - this.adapter = adapter; - this.callback = callback; - this.testTask = completedFuture(null); - } - - - public TestFlow(Supplier testTask, TestFlow flow) { - this.tasks = flow.tasks; - if (testTask != null) - this.tasks.add(testTask); - this.callback = flow.callback; - this.adapter = flow.adapter; - } - - - /** - * Start the execution of the test flow - * - * @return - */ - public String StartTest() throws ExecutionException, InterruptedException { - - System.out.printf("+------------------------------------------+\n"); - int count = 0; - for (Supplier task : this.tasks) { - System.out.printf("| Running task %s of %s\n", count++, this.tasks.size()); - String result = null; - result = task.get(); - System.out.printf("| --> Result: %s", result); - System.out.flush(); - } - System.out.printf("+------------------------------------------+\n"); - return "Completed"; - - } - - /** - * Send a message from the user to the bot - * - * @param userSays - * @return - */ - public TestFlow Send(String userSays) throws IllegalArgumentException { - if (userSays == null) - throw new IllegalArgumentException("You have to pass a userSays parameter"); - - // Function - return new TestFlow((() -> { - System.out.print(String.format("USER SAYS: %s (Thread Id: %s)\n", userSays, Thread.currentThread().getId())); - System.out.flush(); - try { - this.adapter.SendTextToBot(userSays, this.callback); - return "Successfully sent " + userSays; - } catch (Exception e) { - Assert.fail(e.getMessage()); - return e.getMessage(); - } - }), this); - } - - /** - * Send an activity from the user to the bot - * - * @param userActivity - * @return - */ - public TestFlow Send(Activity userActivity) { - if (userActivity == null) - throw new IllegalArgumentException("You have to pass an Activity"); - - return new TestFlow((() -> { - System.out.printf("TestFlow(%s): Send with User Activity! %s", Thread.currentThread().getId(), userActivity.text()); - System.out.flush(); - - - try { - this.adapter.ProcessActivity((ActivityImpl) userActivity, this.callback); - return "TestFlow: Send() -> ProcessActivity: " + userActivity.text(); - } catch (Exception e) { - return e.getMessage(); - - } - - }), this); - } - - /** - * Delay for time period - * - * @param ms - * @return - */ - public TestFlow Delay(int ms) { - return new TestFlow(() -> - { - System.out.printf("TestFlow(%s): Delay(%s ms) called. ", Thread.currentThread().getId(), ms); - System.out.flush(); - try { - Thread.sleep((int) ms); - } catch (InterruptedException e) { - return e.getMessage(); - } - return null; - }, this); - } - - /** - * Assert that reply is expected text - * - * @param expected - * @param description - * @param timeout - * @return - */ - public TestFlow AssertReply(String expected) { - return this.AssertReply(expected, null, 3000); - } - - public TestFlow AssertReply(String expected, String description) { - return this.AssertReply(expected, description, 3000); - } - - public TestFlow AssertReply(String expected, String description, int timeout) { - return this.AssertReply(this.adapter.MakeActivity(expected), description, timeout); - } - - /** - * Assert that the reply is expected activity - * - * @param expected - * @param description - * @param timeout - * @return - */ - public TestFlow AssertReply(Activity expected) { - String description = Thread.currentThread().getStackTrace()[1].getMethodName(); - return AssertReply(expected, description, 3000); - } - - public TestFlow AssertReply(Activity expected, String description, int timeout) { - if (description == null) - description = Thread.currentThread().getStackTrace()[1].getMethodName(); - String finalDescription = description; - return this.AssertReply((reply) -> { - if (expected.type() != reply.type()) - return String.format("%s: Type should match", finalDescription); - if (expected.text().equals(reply.text())) { - if (finalDescription == null) - return String.format("Expected:%s\nReceived:{reply.AsMessageActivity().Text}", expected.text()); - else - return String.format("%s: Text should match", finalDescription); - } - // TODO, expand this to do all properties set on expected - return null; - }, description, timeout); - } - - /** - * Assert that the reply matches a custom validation routine - * - * @param validateActivity - * @param description - * @param timeout - * @return - */ - public TestFlow AssertReply(Function validateActivity) { - String description = Thread.currentThread().getStackTrace()[1].getMethodName(); - return AssertReply(validateActivity, description, 3000); - } - - public TestFlow AssertReply(Function validateActivity, String description) { - return AssertReply(validateActivity, description, 3000); - } - - public TestFlow AssertReply(Function validateActivity, String description, int timeout) { - return new TestFlow(() -> { - System.out.println(String.format("AssertReply: Starting loop : %s (Thread:%s)", description, Thread.currentThread().getId())); - System.out.flush(); - - int finalTimeout = Integer.MAX_VALUE; - if (isDebug()) - finalTimeout = Integer.MAX_VALUE; - - DateTime start = DateTime.now(); - while (true) { - DateTime current = DateTime.now(); - - if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) { - System.out.println("AssertReply: Timeout!\n"); - System.out.flush(); - return String.format("%d ms Timed out waiting for:'%s'", finalTimeout, description); - } - -// System.out.println("Before GetNextReply\n"); -// System.out.flush(); - - Activity replyActivity = this.adapter.GetNextReply(); -// System.out.println("After GetNextReply\n"); -// System.out.flush(); - - if (replyActivity != null) { - System.out.printf("AssertReply(tid:%s): Received Reply: %s ", Thread.currentThread().getId(), (replyActivity.text() == null) ? "No Text set" : replyActivity.text()); - System.out.flush(); - System.out.printf("=============\n From: %s\n To:%s\n ==========\n", (replyActivity.from() == null) ? "No from set" : replyActivity.from().name(), - (replyActivity.recipient() == null) ? "No recipient set" : replyActivity.recipient().name()); - System.out.flush(); - - // if we have a reply - return validateActivity.apply(replyActivity); - } else { - System.out.printf("AssertReply(tid:%s): Waiting..\n", Thread.currentThread().getId()); - System.out.flush(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - }, this); - } - - // Hack to determine if debugger attached.. - public boolean isDebug() { - for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { - if (arg.contains("jdwp=")) { - return true; - } - } - return false; - } - - - /** - * @param userSays - * @param expected - * @return - */ - public TestFlow Turn(String userSays, String expected, String description, int timeout) { - String result = null; - try { - - result = CompletableFuture.supplyAsync(() -> { // Send the message - - if (userSays == null) - throw new IllegalArgumentException("You have to pass a userSays parameter"); - - System.out.print(String.format("TestTurn(%s): USER SAYS: %s \n", Thread.currentThread().getId(), userSays)); - System.out.flush(); - - try { - this.adapter.SendTextToBot(userSays, this.callback); - return null; - } catch (Exception e) { - return e.getMessage(); - } - - }) - .thenApply(arg -> { // Assert Reply - int finalTimeout = Integer.MAX_VALUE; - if (isDebug()) - finalTimeout = Integer.MAX_VALUE; - Function validateActivity = activity -> { - if (activity.text().equals(expected)) { - System.out.println(String.format("TestTurn(tid:%s): Validated text is: %s", Thread.currentThread().getId(), expected)); - System.out.flush(); - - return "SUCCESS"; - } - System.out.println(String.format("TestTurn(tid:%s): Failed validate text is: %s", Thread.currentThread().getId(), expected)); - System.out.flush(); - - return String.format("FAIL: %s received in Activity.text (%s expected)", activity.text(), expected); - }; - - - System.out.println(String.format("TestTurn(tid:%s): Started receive loop: %s", Thread.currentThread().getId(), description)); - System.out.flush(); - DateTime start = DateTime.now(); - while (true) { - DateTime current = DateTime.now(); - - if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) - return String.format("TestTurn: %d ms Timed out waiting for:'%s'", finalTimeout, description); - - - Activity replyActivity = this.adapter.GetNextReply(); - - - if (replyActivity != null) { - // if we have a reply - System.out.println(String.format("TestTurn(tid:%s): Received Reply: %s", - Thread.currentThread().getId(), - String.format("\n========\n To:%s\n From:%s\n Msg:%s\n=======", replyActivity.recipient().name(), replyActivity.from().name(), replyActivity.text()) - )); - System.out.flush(); - return validateActivity.apply(replyActivity); - } else { - System.out.println(String.format("TestTurn(tid:%s): No reply..", Thread.currentThread().getId())); - System.out.flush(); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - } - }) - .get(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } catch (TimeoutException e) { - e.printStackTrace(); - } - return this; - - } - - /** - * Say() -> shortcut for .Send(user).AssertReply(Expected) - * - * @param userSays - * @param expected - * @param description - * @param timeout - * @return - */ - public TestFlow Test(String userSays, String expected) { - return Test(userSays, expected, null, 3000); - } - - public TestFlow Test(String userSays, String expected, String description) { - return Test(userSays, expected, description, 3000); - } - - public TestFlow Test(String userSays, String expected, String description, int timeout) { - if (expected == null) - throw new IllegalArgumentException("expected"); - - return this.Send(userSays) - .AssertReply(expected, description, timeout); - } - - /** - * Test() -> shortcut for .Send(user).AssertReply(Expected) - * - * @param userSays - * @param expected - * @param description - * @param timeout - * @return - */ - public TestFlow Test(String userSays, Activity expected) { - return Test(userSays, expected, null, 3000); - } - - public TestFlow Test(String userSays, Activity expected, String description) { - return Test(userSays, expected, description, 3000); - } - - public TestFlow Test(String userSays, Activity expected, String description, int timeout) { - if (expected == null) - throw new IllegalArgumentException("expected"); - - return this.Send(userSays) - .AssertReply(expected, description, timeout); - } - - /** - * Say() -> shortcut for .Send(user).AssertReply(Expected) - * - * @param userSays - * @param expected - * @param description - * @param timeout - * @return - */ - public TestFlow Test(String userSays, Function expected) { - return Test(userSays, expected, null, 3000); - } - - public TestFlow Test(String userSays, Function expected, String description) { - return Test(userSays, expected, description, 3000); - } - - public TestFlow Test(String userSays, Function expected, String description, int timeout) { - if (expected == null) - throw new IllegalArgumentException("expected"); - - return this.Send(userSays) - .AssertReply(expected, description, timeout); - } - - /** - * Assert that reply is one of the candidate responses - * - * @param candidates - * @param description - * @param timeout - * @return - */ - public TestFlow AssertReplyOneOf(String[] candidates) { - return AssertReplyOneOf(candidates, null, 3000); - } - - public TestFlow AssertReplyOneOf(String[] candidates, String description) { - return AssertReplyOneOf(candidates, description, 3000); - } - - public TestFlow AssertReplyOneOf(String[] candidates, String description, int timeout) { - if (candidates == null) - throw new IllegalArgumentException("candidates"); - - return this.AssertReply((reply) -> { - for (String candidate : candidates) { - if (reply.text() == candidate) - return null; - } - return String.format("%s: Not one of candidates: %s", description, String.join("\n ", candidates)); - }, description, timeout); - } - -} +package com.microsoft.bot.builder.adapters; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.schema.ActivityImpl; +import com.microsoft.bot.schema.models.Activity; +import org.joda.time.DateTime; +import org.junit.Assert; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class TestFlow { + final TestAdapter adapter; + CompletableFuture testTask; + Consumer callback; + + ArrayList> tasks = new ArrayList>(); + ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() + { + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) + { + final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setName("TestFlow-" + worker.getPoolIndex()); + return worker; + } + }; + + ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, true); + + + public TestFlow(TestAdapter adapter) { + this(adapter, null); + } + + public TestFlow(TestAdapter adapter, Consumer callback) { + this.adapter = adapter; + this.callback = callback; + this.testTask = completedFuture(null); + } + + + public TestFlow(Supplier testTask, TestFlow flow) { + this.tasks = flow.tasks; + if (testTask != null) + this.tasks.add(testTask); + this.callback = flow.callback; + this.adapter = flow.adapter; + } + + + /** + * Start the execution of the test flow + * + * @return + */ + public String StartTest() throws ExecutionException, InterruptedException { + + System.out.printf("+------------------------------------------+\n"); + int count = 0; + for (Supplier task : this.tasks) { + System.out.printf("| Running task %s of %s\n", count++, this.tasks.size()); + String result = null; + result = task.get(); + System.out.printf("| --> Result: %s", result); + System.out.flush(); + } + System.out.printf("+------------------------------------------+\n"); + return "Completed"; + + } + + /** + * Send a message from the user to the bot + * + * @param userSays + * @return + */ + public TestFlow Send(String userSays) throws IllegalArgumentException { + if (userSays == null) + throw new IllegalArgumentException("You have to pass a userSays parameter"); + + // Function + return new TestFlow((() -> { + System.out.print(String.format("USER SAYS: %s (Thread Id: %s)\n", userSays, Thread.currentThread().getId())); + System.out.flush(); + try { + this.adapter.SendTextToBot(userSays, this.callback); + return "Successfully sent " + userSays; + } catch (Exception e) { + Assert.fail(e.getMessage()); + return e.getMessage(); + } + }), this); + } + + /** + * Send an activity from the user to the bot + * + * @param userActivity + * @return + */ + public TestFlow Send(Activity userActivity) { + if (userActivity == null) + throw new IllegalArgumentException("You have to pass an Activity"); + + return new TestFlow((() -> { + System.out.printf("TestFlow(%s): Send with User Activity! %s", Thread.currentThread().getId(), userActivity.text()); + System.out.flush(); + + + try { + this.adapter.ProcessActivity((ActivityImpl) userActivity, this.callback); + return "TestFlow: Send() -> ProcessActivity: " + userActivity.text(); + } catch (Exception e) { + return e.getMessage(); + + } + + }), this); + } + + /** + * Delay for time period + * + * @param ms + * @return + */ + public TestFlow Delay(int ms) { + return new TestFlow(() -> + { + System.out.printf("TestFlow(%s): Delay(%s ms) called. ", Thread.currentThread().getId(), ms); + System.out.flush(); + try { + Thread.sleep((int) ms); + } catch (InterruptedException e) { + return e.getMessage(); + } + return null; + }, this); + } + + /** + * Assert that reply is expected text + * + * @param expected + * @param description + * @param timeout + * @return + */ + public TestFlow AssertReply(String expected) { + return this.AssertReply(expected, null, 3000); + } + + public TestFlow AssertReply(String expected, String description) { + return this.AssertReply(expected, description, 3000); + } + + public TestFlow AssertReply(String expected, String description, int timeout) { + return this.AssertReply(this.adapter.MakeActivity(expected), description, timeout); + } + + /** + * Assert that the reply is expected activity + * + * @param expected + * @param description + * @param timeout + * @return + */ + public TestFlow AssertReply(Activity expected) { + String description = Thread.currentThread().getStackTrace()[1].getMethodName(); + return AssertReply(expected, description, 3000); + } + + public TestFlow AssertReply(Activity expected, String description, int timeout) { + if (description == null) + description = Thread.currentThread().getStackTrace()[1].getMethodName(); + String finalDescription = description; + return this.AssertReply((reply) -> { + if (expected.type() != reply.type()) + return String.format("%s: Type should match", finalDescription); + if (expected.text().equals(reply.text())) { + if (finalDescription == null) + return String.format("Expected:%s\nReceived:{reply.AsMessageActivity().Text}", expected.text()); + else + return String.format("%s: Text should match", finalDescription); + } + // TODO, expand this to do all properties set on expected + return null; + }, description, timeout); + } + + /** + * Assert that the reply matches a custom validation routine + * + * @param validateActivity + * @param description + * @param timeout + * @return + */ + public TestFlow AssertReply(Function validateActivity) { + String description = Thread.currentThread().getStackTrace()[1].getMethodName(); + return AssertReply(validateActivity, description, 3000); + } + + public TestFlow AssertReply(Function validateActivity, String description) { + return AssertReply(validateActivity, description, 3000); + } + + public TestFlow AssertReply(Function validateActivity, String description, int timeout) { + return new TestFlow(() -> { + System.out.println(String.format("AssertReply: Starting loop : %s (Thread:%s)", description, Thread.currentThread().getId())); + System.out.flush(); + + int finalTimeout = Integer.MAX_VALUE; + if (isDebug()) + finalTimeout = Integer.MAX_VALUE; + + DateTime start = DateTime.now(); + while (true) { + DateTime current = DateTime.now(); + + if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) { + System.out.println("AssertReply: Timeout!\n"); + System.out.flush(); + return String.format("%d ms Timed out waiting for:'%s'", finalTimeout, description); + } + +// System.out.println("Before GetNextReply\n"); +// System.out.flush(); + + Activity replyActivity = this.adapter.GetNextReply(); +// System.out.println("After GetNextReply\n"); +// System.out.flush(); + + if (replyActivity != null) { + System.out.printf("AssertReply(tid:%s): Received Reply: %s ", Thread.currentThread().getId(), (replyActivity.text() == null) ? "No Text set" : replyActivity.text()); + System.out.flush(); + System.out.printf("=============\n From: %s\n To:%s\n ==========\n", (replyActivity.from() == null) ? "No from set" : replyActivity.from().name(), + (replyActivity.recipient() == null) ? "No recipient set" : replyActivity.recipient().name()); + System.out.flush(); + + // if we have a reply + return validateActivity.apply(replyActivity); + } else { + System.out.printf("AssertReply(tid:%s): Waiting..\n", Thread.currentThread().getId()); + System.out.flush(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }, this); + } + + // Hack to determine if debugger attached.. + public boolean isDebug() { + for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + if (arg.contains("jdwp=")) { + return true; + } + } + return false; + } + + + /** + * @param userSays + * @param expected + * @return + */ + public TestFlow Turn(String userSays, String expected, String description, int timeout) { + String result = null; + try { + + result = CompletableFuture.supplyAsync(() -> { // Send the message + + if (userSays == null) + throw new IllegalArgumentException("You have to pass a userSays parameter"); + + System.out.print(String.format("TestTurn(%s): USER SAYS: %s \n", Thread.currentThread().getId(), userSays)); + System.out.flush(); + + try { + this.adapter.SendTextToBot(userSays, this.callback); + return null; + } catch (Exception e) { + return e.getMessage(); + } + + }, ExecutorFactory.getExecutor()) + .thenApply(arg -> { // Assert Reply + int finalTimeout = Integer.MAX_VALUE; + if (isDebug()) + finalTimeout = Integer.MAX_VALUE; + Function validateActivity = activity -> { + if (activity.text().equals(expected)) { + System.out.println(String.format("TestTurn(tid:%s): Validated text is: %s", Thread.currentThread().getId(), expected)); + System.out.flush(); + + return "SUCCESS"; + } + System.out.println(String.format("TestTurn(tid:%s): Failed validate text is: %s", Thread.currentThread().getId(), expected)); + System.out.flush(); + + return String.format("FAIL: %s received in Activity.text (%s expected)", activity.text(), expected); + }; + + + System.out.println(String.format("TestTurn(tid:%s): Started receive loop: %s", Thread.currentThread().getId(), description)); + System.out.flush(); + DateTime start = DateTime.now(); + while (true) { + DateTime current = DateTime.now(); + + if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) + return String.format("TestTurn: %d ms Timed out waiting for:'%s'", finalTimeout, description); + + + Activity replyActivity = this.adapter.GetNextReply(); + + + if (replyActivity != null) { + // if we have a reply + System.out.println(String.format("TestTurn(tid:%s): Received Reply: %s", + Thread.currentThread().getId(), + String.format("\n========\n To:%s\n From:%s\n Msg:%s\n=======", replyActivity.recipient().name(), replyActivity.from().name(), replyActivity.text()) + )); + System.out.flush(); + return validateActivity.apply(replyActivity); + } else { + System.out.println(String.format("TestTurn(tid:%s): No reply..", Thread.currentThread().getId())); + System.out.flush(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + } + }) + .get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } + return this; + + } + + /** + * Say() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @param description + * @param timeout + * @return + */ + public TestFlow Test(String userSays, String expected) { + return Test(userSays, expected, null, 3000); + } + + public TestFlow Test(String userSays, String expected, String description) { + return Test(userSays, expected, description, 3000); + } + + public TestFlow Test(String userSays, String expected, String description, int timeout) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.Send(userSays) + .AssertReply(expected, description, timeout); + } + + /** + * Test() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @param description + * @param timeout + * @return + */ + public TestFlow Test(String userSays, Activity expected) { + return Test(userSays, expected, null, 3000); + } + + public TestFlow Test(String userSays, Activity expected, String description) { + return Test(userSays, expected, description, 3000); + } + + public TestFlow Test(String userSays, Activity expected, String description, int timeout) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.Send(userSays) + .AssertReply(expected, description, timeout); + } + + /** + * Say() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @param description + * @param timeout + * @return + */ + public TestFlow Test(String userSays, Function expected) { + return Test(userSays, expected, null, 3000); + } + + public TestFlow Test(String userSays, Function expected, String description) { + return Test(userSays, expected, description, 3000); + } + + public TestFlow Test(String userSays, Function expected, String description, int timeout) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.Send(userSays) + .AssertReply(expected, description, timeout); + } + + /** + * Assert that reply is one of the candidate responses + * + * @param candidates + * @param description + * @param timeout + * @return + */ + public TestFlow AssertReplyOneOf(String[] candidates) { + return AssertReplyOneOf(candidates, null, 3000); + } + + public TestFlow AssertReplyOneOf(String[] candidates, String description) { + return AssertReplyOneOf(candidates, description, 3000); + } + + public TestFlow AssertReplyOneOf(String[] candidates, String description, int timeout) { + if (candidates == null) + throw new IllegalArgumentException("candidates"); + + return this.AssertReply((reply) -> { + for (String candidate : candidates) { + if (reply.text() == candidate) + return null; + } + return String.format("%s: Not one of candidates: %s", description, String.join("\n ", candidates)); + }, description, timeout); + } + +} diff --git a/libraries/bot-connector/pom.xml b/libraries/bot-connector/pom.xml index 00832d30e..b88c170e0 100644 --- a/libraries/bot-connector/pom.xml +++ b/libraries/bot-connector/pom.xml @@ -86,6 +86,10 @@ com.auth0 jwks-rsa + + commons-io + commons-io + com.microsoft.bot diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java index 7c5cca1e2..8c52d36a7 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java @@ -2,22 +2,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. */ package com.microsoft.bot.connector; import com.microsoft.bot.schema.models.AttachmentInfo; -import com.microsoft.bot.connector.models.ErrorResponseException; import com.microsoft.rest.ServiceCallback; import com.microsoft.rest.ServiceFuture; import com.microsoft.rest.ServiceResponse; -import java.io.InputStream; import rx.Observable; +import java.io.InputStream; + /** * An instance of this class provides access to all the operations defined * in Attachments. @@ -28,10 +24,9 @@ public interface Attachments { * Get AttachmentInfo structure describing the attachment views. * * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the AttachmentInfo object if successful. + * @throws IllegalArgumentException thrown if parameters fail the validation + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent */ AttachmentInfo getAttachmentInfo(String attachmentId); @@ -39,10 +34,10 @@ public interface Attachments { * GetAttachmentInfo. * Get AttachmentInfo structure describing the attachment views. * - * @param attachmentId attachment id + * @param attachmentId attachment id * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation * @return the {@link ServiceFuture} object + * @throws IllegalArgumentException thrown if parameters fail the validation */ ServiceFuture getAttachmentInfoAsync(String attachmentId, final ServiceCallback serviceCallback); @@ -51,8 +46,8 @@ public interface Attachments { * Get AttachmentInfo structure describing the attachment views. * * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the AttachmentInfo object + * @throws IllegalArgumentException thrown if parameters fail the validation */ Observable getAttachmentInfoAsync(String attachmentId); @@ -61,8 +56,8 @@ public interface Attachments { * Get AttachmentInfo structure describing the attachment views. * * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the AttachmentInfo object + * @throws IllegalArgumentException thrown if parameters fail the validation */ Observable> getAttachmentInfoWithServiceResponseAsync(String attachmentId); @@ -71,11 +66,10 @@ public interface Attachments { * Get the named view as binary content. * * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent + * @param viewId View id from attachmentInfo * @return the InputStream object if successful. + * @throws IllegalArgumentException thrown if parameters fail the validation + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent */ InputStream getAttachment(String attachmentId, String viewId); @@ -83,11 +77,11 @@ public interface Attachments { * GetAttachment. * Get the named view as binary content. * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo + * @param attachmentId attachment id + * @param viewId View id from attachmentInfo * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation * @return the {@link ServiceFuture} object + * @throws IllegalArgumentException thrown if parameters fail the validation */ ServiceFuture getAttachmentAsync(String attachmentId, String viewId, final ServiceCallback serviceCallback); @@ -96,9 +90,9 @@ public interface Attachments { * Get the named view as binary content. * * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation + * @param viewId View id from attachmentInfo * @return the observable to the InputStream object + * @throws IllegalArgumentException thrown if parameters fail the validation */ Observable getAttachmentAsync(String attachmentId, String viewId); @@ -107,9 +101,9 @@ public interface Attachments { * Get the named view as binary content. * * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation + * @param viewId View id from attachmentInfo * @return the observable to the InputStream object + * @throws IllegalArgumentException thrown if parameters fail the validation */ Observable> getAttachmentWithServiceResponseAsync(String attachmentId, String viewId); diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java new file mode 100644 index 000000000..3b7a2e4ee --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +public class Channels { + /** + * Console channel. + */ + public static final String CONSOLE = "console"; + + /** + * Cortana channel. + */ + public static final String CORTANA = "cortana"; + + /** + * Direct Line channel. + */ + public static final String DIRECTLINE = "directline"; + + /** + * Email channel. + */ + public static final String EMAIL = "email"; + + /** + * Emulator channel. + */ + public static final String EMULATOR = "emulator"; + + /** + * Facebook channel. + */ + public static final String FACEBOOK = "facebook"; + + /** + * Group Me channel. + */ + public static final String GROUPME = "groupme"; + + /** + * Kik channel. + */ + public static final String KIK = "kik"; + + /** + * Line channel. + */ + public static final String LINE = "line"; + + /** + * MS Teams channel. + */ + public static final String MSTEAMS = "msteams"; + + /** + * Skype channel. + */ + public static final String SKYPE = "skype"; + + /** + * Skype for Business channel. + */ + public static final String SKYPEFORBUSINESS = "skypeforbusiness"; + + /** + * Slack channel. + */ + public static final String SLACK = "slack"; + + /** + * SMS (Twilio) channel. + */ + public static final String SMS = "sms"; + + /** + * Telegram channel. + */ + public static final String TELEGRAM = "telegram"; + + /** + * WebChat channel. + */ + public static final String WEBCHAT = "webchat"; + + /** + * Test channel. + */ + public static final String TEST = "test"; +} 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 a1d509585..fa7b9b5cb 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 @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * + *

* Code generated by Microsoft (R) AutoRest Code Generator. * Changes may cause incorrect behavior and will be lost if the code is * regenerated. @@ -21,7 +21,7 @@ public interface ConnectorClient { * Gets the REST client. * * @return the {@link RestClient} object. - */ + */ RestClient restClient(); /** diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java deleted file mode 100644 index cc5ab5194..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.microsoft.bot.connector; - -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.rest.RestClient; -import com.microsoft.rest.credentials.ServiceClientCredentials; - -public class ConnectorClientFuture extends ConnectorClientImpl { - - /** - * Initializes an instance of ConnectorClient client. - * - * @param credentials the management credentials for Azure - */ - public ConnectorClientFuture(ServiceClientCredentials credentials) { - super(credentials); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param baseUrl the base URL of the host - * @param credentials the management credentials for Azure - */ - public ConnectorClientFuture(String baseUrl, ServiceClientCredentials credentials) { - super(baseUrl, credentials); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param restClient the REST client to connect to Azure. - */ - public ConnectorClientFuture(RestClient restClient) { - super(restClient); - } - - @Override - public String userAgent() { - return "Microsoft-BotFramework/4.0"; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java index 6c7ebd424..a07381758 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * + *

* Code generated by Microsoft (R) AutoRest Code Generator. * Changes may cause incorrect behavior and will be lost if the code is * regenerated. @@ -10,22 +10,14 @@ package com.microsoft.bot.connector; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.AttachmentData; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.bot.schema.models.ConversationParameters; -import com.microsoft.bot.schema.models.ConversationResourceResponse; -import com.microsoft.bot.schema.models.ConversationsResult; -import com.microsoft.bot.schema.models.PagedMembersResult; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.bot.schema.models.ResourceResponse; -import com.microsoft.bot.schema.models.Transcript; +import com.microsoft.bot.schema.models.*; import com.microsoft.rest.ServiceCallback; import com.microsoft.rest.ServiceFuture; import com.microsoft.rest.ServiceResponse; -import java.util.List; import rx.Observable; +import java.util.List; + /** * An instance of this class provides access to all the operations defined * in Conversations. @@ -40,7 +32,6 @@ public interface Conversations { Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. * * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ConversationsResult object if successful. */ @@ -85,6 +76,7 @@ public interface Conversations { * @return the observable to the ConversationsResult object */ Observable> getConversationsWithServiceResponseAsync(); + /** * GetConversations. * List the Conversations in which this bot has participated. @@ -95,7 +87,6 @@ public interface Conversations { * * @param continuationToken skip or continuation token * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ConversationsResult object if successful. */ @@ -161,7 +152,6 @@ public interface Conversations { * * @param parameters Parameters to create the conversation from * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ConversationResourceResponse object if successful. */ @@ -243,7 +233,6 @@ This is slightly different from ReplyToActivity(). * @param conversationId Conversation ID * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -308,7 +297,6 @@ This is slightly different from ReplyToActivity(). * @param activityId activityId to update * @param activity replacement Activity * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -370,7 +358,6 @@ This is slightly different from SendToConversation(). * @param activityId activityId the reply is to (OPTIONAL) * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -436,7 +423,6 @@ This is slightly different from SendToConversation(). * @param conversationId Conversation ID * @param activityId activityId to delete * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent */ void deleteActivity(String conversationId, String activityId); @@ -485,7 +471,6 @@ This is slightly different from SendToConversation(). * * @param conversationId Conversation ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the List<ChannelAccount> object if successful. */ @@ -534,7 +519,6 @@ This REST API takes a ConversationId and a memberId (of type string) and removes * @param conversationId Conversation ID * @param memberId ID of the member to delete from this conversation * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent */ void deleteConversationMember(String conversationId, String memberId); @@ -587,7 +571,6 @@ This REST API takes a ConversationId and a memberId (of type string) and removes * @param conversationId Conversation ID * @param activityId Activity ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the List<ChannelAccount> object if successful. */ @@ -639,7 +622,6 @@ This REST API takes a ConversationId and a memberId (of type string) and removes * @param conversationId Conversation ID * @param attachmentUpload Attachment data * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -688,15 +670,14 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * This method allows you to upload the historic activities to the conversation. - * - * Sender must ensure that the historic activities have unique ids and appropriate timestamps. - * The ids are used by the client to deal with duplicate activities and the timestamps are used by + * + * Sender must ensure that the historic activities have unique ids and appropriate timestamps. + * The ids are used by the client to deal with duplicate activities and the timestamps are used by * the client to render the activities in the right order. * * @param conversationId Conversation ID * @param history Historic activities * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -704,16 +685,15 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * This method allows you to upload the historic activities to the conversation. - * - * Sender must ensure that the historic activities have unique ids and appropriate timestamps. - * The ids are used by the client to deal with duplicate activities and the timestamps are used by + * + * Sender must ensure that the historic activities have unique ids and appropriate timestamps. + * The ids are used by the client to deal with duplicate activities and the timestamps are used by * the client to render the activities in the right order. * * @param conversationId Conversation ID * @param history Historic activities * @param serviceCallback the async ServiceCallback to handle successful and failed responses. * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -721,15 +701,14 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * This method allows you to upload the historic activities to the conversation. - * - * Sender must ensure that the historic activities have unique ids and appropriate timestamps. - * The ids are used by the client to deal with duplicate activities and the timestamps are used by + * + * Sender must ensure that the historic activities have unique ids and appropriate timestamps. + * The ids are used by the client to deal with duplicate activities and the timestamps are used by * the client to render the activities in the right order. * * @param conversationId Conversation ID * @param history Historic activities * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ @@ -737,38 +716,36 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * This method allows you to upload the historic activities to the conversation. - * - * Sender must ensure that the historic activities have unique ids and appropriate timestamps. - * The ids are used by the client to deal with duplicate activities and the timestamps are used by + * + * Sender must ensure that the historic activities have unique ids and appropriate timestamps. + * The ids are used by the client to deal with duplicate activities and the timestamps are used by * the client to render the activities in the right order. * * @param conversationId Conversation ID * @param history Historic activities * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the ResourceResponse object if successful. */ - Observable> sendConversationHistoryWithServiceResponseAsync(String conversationId, Transcript history); + Observable> sendConversationHistoryWithServiceResponseAsync(String conversationId, Transcript history); /** * Enumerate the members of a conversation one page at a time. - * - * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. - * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members + * + * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. + * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members * of the conversation and a continuation token that can be used to get more values. - * - * One page of ChannelAccounts records are returned with each call. The number of records in a page may - * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no - * additional results the response will not contain a continuation token. If there are no members in the + * + * One page of ChannelAccounts records are returned with each call. The number of records in a page may + * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no + * additional results the response will not contain a continuation token. If there are no members in the * conversation the Members will be empty or not present in the response. - * - * A response to a request that has a continuation token from a prior request may rarely return members + * + * A response to a request that has a continuation token from a prior request may rarely return members * from a previous request. * * @param conversationId Conversation ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the PagedMembersResult object if successful. */ @@ -776,23 +753,22 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * Enumerate the members of a conversation one page at a time. - * - * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. - * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members + * + * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. + * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members * of the conversation and a continuation token that can be used to get more values. - * - * One page of ChannelAccounts records are returned with each call. The number of records in a page may - * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no - * additional results the response will not contain a continuation token. If there are no members in the + * + * One page of ChannelAccounts records are returned with each call. The number of records in a page may + * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no + * additional results the response will not contain a continuation token. If there are no members in the * conversation the Members will be empty or not present in the response. - * - * A response to a request that has a continuation token from a prior request may rarely return members + * + * A response to a request that has a continuation token from a prior request may rarely return members * from a previous request. * * @param conversationId Conversation ID * @param serviceCallback the async ServiceCallback to handle successful and failed responses. * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the PagedMembersResult object if successful. */ @@ -800,22 +776,21 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * Enumerate the members of a conversation one page at a time. - * - * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. - * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members + * + * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. + * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members * of the conversation and a continuation token that can be used to get more values. - * - * One page of ChannelAccounts records are returned with each call. The number of records in a page may - * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no - * additional results the response will not contain a continuation token. If there are no members in the + * + * One page of ChannelAccounts records are returned with each call. The number of records in a page may + * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no + * additional results the response will not contain a continuation token. If there are no members in the * conversation the Members will be empty or not present in the response. - * - * A response to a request that has a continuation token from a prior request may rarely return members + * + * A response to a request that has a continuation token from a prior request may rarely return members * from a previous request. * * @param conversationId Conversation ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the PagedMembersResult object if successful. */ @@ -823,25 +798,24 @@ This REST API takes a ConversationId and a memberId (of type string) and removes /** * Enumerate the members of a conversation one page at a time. - * - * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. - * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members + * + * This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. + * It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members * of the conversation and a continuation token that can be used to get more values. - * - * One page of ChannelAccounts records are returned with each call. The number of records in a page may - * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no - * additional results the response will not contain a continuation token. If there are no members in the + * + * One page of ChannelAccounts records are returned with each call. The number of records in a page may + * vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no + * additional results the response will not contain a continuation token. If there are no members in the * conversation the Members will be empty or not present in the response. - * - * A response to a request that has a continuation token from a prior request may rarely return members + * + * A response to a request that has a continuation token from a prior request may rarely return members * from a previous request. * * @param conversationId Conversation ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent * @return the observable to the ResourceResponse object */ Observable> getConversationPagedMembersWithServiceResponseAsync(String conversationId); - } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java new file mode 100644 index 000000000..9783ffa43 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; +import java.util.concurrent.ForkJoinWorkerThread; + +/** + * Provides a common Executor for Future operations. + */ +public class ExecutorFactory { + private static ForkJoinWorkerThreadFactory factory = new ForkJoinWorkerThreadFactory() { + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setName("Bot-" + worker.getPoolIndex()); + return worker; + } + }; + + private static ExecutorService executor = new ForkJoinPool( + Runtime.getRuntime().availableProcessors() * 2, + factory, + null, + false); + + public static ExecutorService getExecutor() { + return executor; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java index 84febd4db..efc985190 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java @@ -1,6 +1,6 @@ package com.microsoft.bot.connector; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.RestConnectorClient; import java.io.IOException; import java.io.InputStream; @@ -23,7 +23,7 @@ public class UserAgent { String build_version; final Properties properties = new Properties(); try { - InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties"); + InputStream propStream = RestConnectorClient.class.getClassLoader().getResourceAsStream("project.properties"); properties.load(propStream); build_version = properties.getProperty("version"); } catch (IOException e) { diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AdalAuthenticator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AdalAuthenticator.java new file mode 100644 index 000000000..82c763a63 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AdalAuthenticator.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.adal4j.AuthenticationContext; +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.aad.adal4j.ClientCredential; +import com.microsoft.bot.connector.ExecutorFactory; + +import java.net.MalformedURLException; +import java.util.concurrent.Future; + +public class AdalAuthenticator { + private AuthenticationContext context; + private OAuthConfiguration oAuthConfiguration; + private ClientCredential clientCredential; + + public AdalAuthenticator(ClientCredential clientCredential, OAuthConfiguration configurationOAuth) throws MalformedURLException { + this.oAuthConfiguration = configurationOAuth; + this.clientCredential = clientCredential; + this.context = new AuthenticationContext(configurationOAuth.authority(), false, ExecutorFactory.getExecutor()); + } + + public Future acquireToken() { + return context.acquireToken(oAuthConfiguration.scope(), clientCredential, null); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java new file mode 100644 index 000000000..33a5bce57 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.ArrayList; +import java.util.List; + +/** + * General configuration settings for authentication. + */ +public class AuthenticationConfiguration { + public List requiredEndorsements() { + return new ArrayList(); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java index b8769b30c..f83fadbc4 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java @@ -7,23 +7,127 @@ import java.util.List; public final class AuthenticationConstants { - public static final String ToChannelFromBotLoginUrl = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"; - public static final String ToChannelFromBotOAuthScope = "https://api.botframework.com/.default"; - public static final String ToBotFromChannelTokenIssuer = "https://api.botframework.com"; - public static final String ToBotFromChannelOpenIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration"; - public static final String ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + /** + * TO CHANNEL FROM BOT: Login URL. + */ + @Deprecated + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.com/botframework.com"; + + /** + * TO CHANNEL FROM BOT: Login URL template string. Bot developer may specify + * which tenant to obtain an access token from. By default, the channels only + * accept tokens from "botframework.com". For more details see https://aka.ms/bots/tenant-restriction. + */ + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE = "https://login.microsoftonline.com/%s"; + + /** + * TO CHANNEL FROM BOT: OAuth scope to request. + */ + public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.com"; + + /** + * TO BOT FROM CHANNEL: Token issuer. + */ + public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"; + + /** + * TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = "https://login.botframework.com/v1/.well-known/openidconfiguration"; + + /** + * TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + + /** + * TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT = "https://%s.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration"; + + /** + * Allowed token signing algorithms. Tokens come from channels to the bot. The code + * that uses this also supports tokens coming from the emulator. + */ public static final List AllowedSigningAlgorithms = new ArrayList<>(); - public static final String AuthorizedParty = "azp"; - public static final String AudienceClaim = "aud"; - public static final String ServiceUrlClaim = "serviceurl"; - public static final String VersionClaim = "ver"; - public static final String AppIdClaim = "appid"; + + /** + * Application Setting Key for the OAuthUrl value. + */ + public static final String OAUTH_URL_KEY = "OAuthApiEndpoint"; + + /** + * OAuth Url used to get a token from OAuthApiClient. + */ + public static final String OAUTH_URL = "https://api.botframework.com"; + + /** + * Application Settings Key for whether to emulate OAuthCards when using the emulator. + */ + public static final String EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards"; + + /** + * Application Setting Key for the OpenIdMetadataUrl value. + */ + public static final String BOT_OPENID_METADATA_KEY = "BotOpenIdMetadata"; + + /** + * The default tenant to acquire bot to channel token from. + */ + public static final String DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com"; + /** - * OAuth Url used to get a token from OAuthApiClient + * "azp" Claim. + * Authorized party - the party to which the ID Token was issued. + * This claim follows the general format set forth in the OpenID Spec. + * http://openid.net/specs/openid-connect-core-1_0.html#IDToken. */ - public static final String OAuthUrl = "https://api.botframework.com"; + public static final String AUTHORIZED_PARTY = "azp"; + /** + * Audience Claim. From RFC 7519. + * https://tools.ietf.org/html/rfc7519#section-4.1.3 + * The "aud" (audience) claim identifies the recipients that the JWT is + * intended for. Each principal intended to process the JWT MUST + * identify itself with a value in the audience claim. If the principal + * processing the claim does not identify itself with a value in the + * "aud" claim when this claim is present, then the JWT MUST be + * rejected. In the general case, the "aud" value is an array of case- + * sensitive strings, each containing a StringOrURI value. In the + * special case when the JWT has one audience, the "aud" value MAY be a + * single case-sensitive string containing a StringOrURI value. The + * interpretation of audience values is generally application specific. + * Use of this claim is OPTIONAL. + */ + public static final String AUDIENCE_CLAIM = "aud"; + /** + * From RFC 7515 + * https://tools.ietf.org/html/rfc7515#section-4.1.4 + * The "kid" (key ID) Header Parameter is a hint indicating which key + * was used to secure the JWS. This parameter allows originators to + * explicitly signal a change of key to recipients. The structure of + * the "kid" value is unspecified. Its value MUST be a case-sensitive + * string. Use of this Header Parameter is OPTIONAL. + * When used with a JWK, the "kid" value is used to match a JWK "kid" + * parameter value. + */ + public static final String KEY_ID_HEADER = "kid"; + + /** + * Service URL claim name. As used in Microsoft Bot Framework v3.1 auth. + */ + public static final String SERVICE_URL_CLAIM = "serviceurl"; + + /** + * Token version claim name. As used in Microsoft AAD tokens. + */ + public static final String VERSION_CLAIM = "ver"; + + /** + * App ID claim name. As used in Microsoft AAD 1.0 tokens. + */ + public static final String APPID_CLAIM = "appid"; static { AllowedSigningAlgorithms.add("RS256"); diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java deleted file mode 100644 index 96666f997..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -public class BotCredentials { - protected String appId; - protected String appPassword; - - public String appId() { return this.appId; } - - public String appPassword() { return this.appPassword; } - - public BotCredentials withAppId(String appId) { - this.appId = appId; - return this; - } - - public BotCredentials withAppPassword(String appPassword) { - this.appPassword = appPassword; - return this; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java new file mode 100644 index 000000000..eabde8899 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +/** + * ChannelProvider interface. This interface allows Bots to provide their own + * implementation for the configuration parameters to connect to a Bot. + * Framework channel service. + */ +public interface ChannelProvider { + /** + * Gets the channel service property for this channel provider. + * + * @return The channel service property for the channel provider. + */ + CompletableFuture getChannelService(); + + /** + * Gets a value of whether this provider represents a channel on Government Azure. + * + * @return True if this channel provider represents a channel on Government Azure. + */ + boolean isGovernment(); + + /** + * Gets a value of whether this provider represents a channel on Public Azure. + * + * @return True if this channel provider represents a channel on Public Azure. + */ + boolean isPublicAzure(); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java index 0bd10c642..5e17c8d95 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java @@ -4,88 +4,141 @@ package com.microsoft.bot.connector.authentication; import com.microsoft.aad.adal4j.AuthenticationException; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; +import java.util.ArrayList; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; public class ChannelValidation { /** * TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot */ - public static final TokenValidationParameters ToBotFromChannelTokenValidationParameters = TokenValidationParameters.toBotFromChannelTokenValidationParameters(); + public static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{ + this.validateIssuer = true; + this.validIssuers = new ArrayList() {{ + add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + }}; + this.validateAudience = false; + this.validateLifetime = true; + this.clockSkew = Duration.ofMinutes(5); + this.requireSignedTokens = true; + }}; /** * Validate the incoming Auth Header as a token sent from the Bot Framework Service. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". * @param credentials The user defined set of valid credentials, such as the AppId. - * @param channelId ChannelId for endorsements validation. + * @param channelId ChannelId for endorsements validation. * @return A valid ClaimsIdentity. + * + * On join: * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. */ - public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException { + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId) { + return authenticateToken(authHeader, credentials, channelId, new AuthenticationConfiguration()); + } + + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelId ChannelId for endorsements validation. + * @param authConfig The AuthenticationConfiguration. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId, AuthenticationConfiguration authConfig) { JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( - ToBotFromChannelTokenValidationParameters, - AuthenticationConstants.ToBotFromChannelOpenIdMetadataUrl, + TOKENVALIDATIONPARAMETERS, + AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL, AuthenticationConstants.AllowedSigningAlgorithms); - ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get(); - if (identity == null) { - // No valid identity. Not Authorized. - throw new AuthenticationException("Invalid Identity"); - } - - if (!identity.isAuthenticated()) { - // The token is in some way invalid. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - // Now check that the AppID in the claims set matches - // what we're looking for. Note that in a multi-tenant bot, this value - // comes from developer code that may be reaching out to a service, hence the - // Async validation. - - // Look for the "aud" claim, but only if issued from the Bot Framework - if (!identity.getIssuer().equalsIgnoreCase(AuthenticationConstants.ToBotFromChannelTokenIssuer)) { - throw new AuthenticationException("Token Not Authenticated"); - } - - // The AppId from the claim in the token must match the AppId specified by the developer. Note that - // the Bot Framework uses the Audience claim ("aud") to pass the AppID. - String appIdFromClaim = identity.claims().get(AuthenticationConstants.AudienceClaim); - if (appIdFromClaim == null || appIdFromClaim.isEmpty()) { - // Claim is present, but doesn't have a value. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - if (!credentials.isValidAppIdAsync(appIdFromClaim).get()) { - throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromClaim)); - } - - return CompletableFuture.completedFuture(identity); + return tokenExtractor.getIdentityAsync(authHeader, channelId) + .thenApply(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Token Not Authenticated"); + } + + // Now check that the AppID in the claims set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + // Look for the "aud" claim, but only if issued from the Bot Framework + if (!identity.getIssuer().equalsIgnoreCase(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) { + throw new AuthenticationException("Wrong Issuer"); + } + + // The AppId from the claim in the token must match the AppId specified by the developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + throw new AuthenticationException("No Audience Claim"); + } + + if (!credentials.isValidAppIdAsync(appIdFromAudienceClaim).join()) { + throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim)); + } + + return identity; + }); } /** * Validate the incoming Auth Header as a token sent from the Bot Framework Service. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]" * @param credentials The user defined set of valid credentials, such as the AppId. - * @param channelId ChannelId for endorsements validation. - * @param serviceUrl Service url. + * @param channelId ChannelId for endorsements validation. + * @param serviceUrl Service url. * @return A valid ClaimsIdentity. + * + * On join: * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. */ - public static CompletableFuture authenticateToken(String authHeader,CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException { - ClaimsIdentity identity = ChannelValidation.authenticateToken(authHeader, credentials, channelId).get(); - - if (!identity.claims().containsKey(AuthenticationConstants.ServiceUrlClaim)) { - // Claim must be present. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Channel Token.", AuthenticationConstants.ServiceUrlClaim)); - } - - if (!serviceUrl.equalsIgnoreCase(identity.claims().get(AuthenticationConstants.ServiceUrlClaim))) { - // Claim must match. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim does not match service url provided (%s).", AuthenticationConstants.ServiceUrlClaim, serviceUrl)); - } + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl) { + return authenticateToken(authHeader, credentials, channelId, serviceUrl, new AuthenticationConfiguration()); + } - return CompletableFuture.completedFuture(identity); + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]" + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelId ChannelId for endorsements validation. + * @param serviceUrl Service url. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl, AuthenticationConfiguration authConfig) { + return ChannelValidation.authenticateToken(authHeader, credentials, channelId, authConfig) + .thenApply(identity -> { + if (!identity.claims().containsKey(AuthenticationConstants.SERVICE_URL_CLAIM)) { + // Claim must be present. Not Authorized. + throw new AuthenticationException(String.format("'%s' claim is required on Channel Token.", AuthenticationConstants.SERVICE_URL_CLAIM)); + } + + if (!serviceUrl.equalsIgnoreCase(identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM))) { + // Claim must match. Not Authorized. + throw new AuthenticationException(String.format("'%s' claim does not match service url provided (%s).", AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl)); + } + + return identity; + }); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java index d7aa764a1..1bb46948c 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java @@ -3,10 +3,45 @@ package com.microsoft.bot.connector.authentication; +import com.auth0.jwt.interfaces.DecodedJWT; + +import java.util.HashMap; import java.util.Map; -public interface ClaimsIdentity { - boolean isAuthenticated(); - Map claims(); - String getIssuer(); +public class ClaimsIdentity { + private String issuer; + private Map claims; + + private ClaimsIdentity() { + this("", new HashMap<>()); + } + + public ClaimsIdentity(String authIssuer) { + this(authIssuer, new HashMap<>()); + } + + public ClaimsIdentity(String authIssuer, Map claims) { + this.issuer = authIssuer; + this.claims = claims; + } + + public ClaimsIdentity(DecodedJWT jwt) { + claims = new HashMap<>(); + if (jwt.getClaims() != null) { + jwt.getClaims().forEach((k, v) -> claims.put(k, v.asString())); + } + issuer = jwt.getIssuer(); + } + + public boolean isAuthenticated() { + return this.issuer != null && !this.issuer.isEmpty(); + } + + public Map claims() { + return this.claims; + } + + public String getIssuer() { + return issuer; + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java deleted file mode 100644 index 1b7f66d84..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -import java.util.HashMap; -import java.util.Map; - -public class ClaimsIdentityImpl implements ClaimsIdentity { - private String issuer; - private Map claims; - - public ClaimsIdentityImpl() { - this("", new HashMap<>()); - } - - public ClaimsIdentityImpl(String authType) { - this(authType, new HashMap<>()); - } - - public ClaimsIdentityImpl(String authType, Map claims) { - this.issuer = authType; - this.claims = claims; - } - - @Override - public boolean isAuthenticated() { - return this.issuer != null && !this.issuer.isEmpty(); - } - - @Override - public Map claims() { - return this.claims; - } - - @Override - public String getIssuer() { - return issuer; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java deleted file mode 100644 index 779216470..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -import java.util.concurrent.CompletableFuture; - -public class CredentialProviderImpl extends BotCredentials implements CredentialProvider { - - public CredentialProviderImpl(String appId, String appPassword) { - this.appId = appId; - this.appPassword = appPassword; - } - - public CredentialProviderImpl(BotCredentials credentials) { - this(credentials.appId, credentials.appPassword); - } - - @Override - public CredentialProviderImpl withAppId(String appId) { - return (CredentialProviderImpl) super.withAppId(appId); - } - - @Override - public CredentialProviderImpl withAppPassword(String appPassword) { - return (CredentialProviderImpl) super.withAppPassword(appPassword); - } - - @Override - public CompletableFuture isValidAppIdAsync(String appId) { - return CompletableFuture.completedFuture(this.appId.equals(appId)); - } - - @Override - public CompletableFuture getAppPasswordAsync(String appId) { - return CompletableFuture.completedFuture(this.appId.equals(appId) ? this.appPassword : null); - } - - @Override - public CompletableFuture isAuthenticationDisabledAsync() { - return CompletableFuture.completedFuture(this.appId == null || this.appId.isEmpty()); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java index c63842f86..93ad9f9d5 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java @@ -6,9 +6,11 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; import com.microsoft.aad.adal4j.AuthenticationException; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; +import java.util.ArrayList; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; /** * Validates and Examines JWT tokens from the Bot Framework Emulator @@ -17,25 +19,40 @@ public class EmulatorValidation { /** * TO BOT FROM EMULATOR: Token validation parameters when connecting to a channel. */ - public static final TokenValidationParameters ToBotFromEmulatorTokenValidationParameters = TokenValidationParameters.toBotFromEmulatorTokenValidationParameters(); + private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{ + this.validateIssuer = true; + this.validIssuers = new ArrayList() {{ + add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/"); // Auth v3.1, 1.0 token + add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0"); // Auth v3.1, 2.0 token + add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/"); // Auth v3.2, 1.0 token + add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0"); // Auth v3.2, 2.0 token + add("https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/"); // Auth for US Gov, 1.0 token + add("https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0"); // Auth for US Gov, 2.0 token + }}; + this.validateAudience = false; + this.validateLifetime = true; + this.clockSkew = Duration.ofMinutes(5); + this.requireSignedTokens = true; + }}; /** * Determines if a given Auth header is from the Bot Framework Emulator + * * @param authHeader Bearer Token, in the "Bearer [Long String]" Format. * @return True, if the token was issued by the Emulator. Otherwise, false. */ - public static CompletableFuture isTokenFromEmulator(String authHeader) { + public static Boolean isTokenFromEmulator(String authHeader) { // The Auth Header generally looks like this: // "Bearer eyJ0e[...Big Long String...]XAiO" - if (authHeader == null || authHeader.isEmpty()) { + if (StringUtils.isEmpty(authHeader)) { // No token. Can't be an emulator token. - return CompletableFuture.completedFuture(false); + return false; } String[] parts = authHeader.split(" "); if (parts.length != 2) { // Emulator tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not an emulator token - return CompletableFuture.completedFuture(false); + return false; } String schema = parts[0]; @@ -43,91 +60,120 @@ public static CompletableFuture isTokenFromEmulator(String authHeader) if (!schema.equalsIgnoreCase("bearer")) { // The scheme from the emulator MUST be "Bearer" - return CompletableFuture.completedFuture(false); + return false; } // Parse the Big Long String into an actual token. - DecodedJWT decodedJWT = JWT.decode(token); + try { + DecodedJWT decodedJWT = JWT.decode(token); - // Is there an Issuer? - if (decodedJWT.getIssuer().isEmpty()) { - // No Issuer, means it's not from the Emulator. - return CompletableFuture.completedFuture(false); - } + // Is there an Issuer? + if (StringUtils.isEmpty(decodedJWT.getIssuer())) { + // No Issuer, means it's not from the Emulator. + return false; + } - // Is the token issues by a source we consider to be the emulator? - if (!ToBotFromEmulatorTokenValidationParameters.validIssuers.contains(decodedJWT.getIssuer())) { + // Is the token issues by a source we consider to be the emulator? // Not a Valid Issuer. This is NOT a Bot Framework Emulator Token. - return CompletableFuture.completedFuture(false); + return TOKENVALIDATIONPARAMETERS.validIssuers.contains(decodedJWT.getIssuer()); + } catch (Throwable t) { + return false; } - - // The Token is from the Bot Framework Emulator. Success! - return CompletableFuture.completedFuture(true); } /** * Validate the incoming Auth Header as a token sent from the Bot Framework Emulator. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" - * @param credentials The user defined set of valid credentials, such as the AppId. + * A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelProvider The channelService value that distinguishes public Azure from US Government Azure. + * @param channelId The ID of the channel to validate. * @return A valid ClaimsIdentity. + *

+ * On join: * @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. */ - public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException { - JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( - ToBotFromEmulatorTokenValidationParameters, - AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl, - AuthenticationConstants.AllowedSigningAlgorithms); - - ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get(); - if (identity == null) { - // No valid identity. Not Authorized. - throw new AuthenticationException("Invalid Identity"); - } - - if (!identity.isAuthenticated()) { - // The token is in some way invalid. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - // Now check that the AppID in the claims set matches - // what we're looking for. Note that in a multi-tenant bot, this value - // comes from developer code that may be reaching out to a service, hence the - // Async validation. - if (!identity.claims().containsKey(AuthenticationConstants.VersionClaim)) { - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Tokens.", AuthenticationConstants.VersionClaim)); - } - - String tokenVersion = identity.claims().get(AuthenticationConstants.VersionClaim); - String appId = ""; - - // The Emulator, depending on Version, sends the AppId via either the - // appid claim (Version 1) or the Authorized Party claim (Version 2). - if (tokenVersion.isEmpty() || tokenVersion.equalsIgnoreCase("1.0")) { - // either no Version or a version of "1.0" means we should look for - // the claim in the "appid" claim. - if (!identity.claims().containsKey(AuthenticationConstants.AppIdClaim)) { - // No claim around AppID. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '1.0'.", AuthenticationConstants.AppIdClaim)); - } - - appId = identity.claims().get(AuthenticationConstants.AppIdClaim); - } else if (tokenVersion.equalsIgnoreCase("2.0")) { - // Emulator, "2.0" puts the AppId in the "azp" claim. - if (!identity.claims().containsKey(AuthenticationConstants.AuthorizedParty)) { - // No claim around AppID. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '2.0'.", AuthenticationConstants.AuthorizedParty)); - } - - appId = identity.claims().get(AuthenticationConstants.AuthorizedParty); - } else { - // Unknown Version. Not Authorized. - throw new AuthenticationException(String.format("Unknown Emulator Token version '%s'.", tokenVersion)); - } + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId) { + return authenticateToken(authHeader, credentials, channelProvider, channelId, new AuthenticationConfiguration()); + } - if (!credentials.isValidAppIdAsync(appId).get()) { - throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appId)); - } + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework Emulator. + * A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelProvider The channelService value that distinguishes public Azure from US Government Azure. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + *

+ * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, AuthenticationConfiguration authConfig) { + String openIdMetadataUrl = channelProvider != null && channelProvider.isGovernment() ? + GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL : + AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL; - return CompletableFuture.completedFuture(identity); + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + TOKENVALIDATIONPARAMETERS, + openIdMetadataUrl, + AuthenticationConstants.AllowedSigningAlgorithms); + + return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements()) + .thenApply(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Token Not Authenticated"); + } + + // Now check that the AppID in the claims set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + if (!identity.claims().containsKey(AuthenticationConstants.VERSION_CLAIM)) { + throw new AuthenticationException(String.format("'%s' claim is required on Emulator Tokens.", AuthenticationConstants.VERSION_CLAIM)); + } + + String tokenVersion = identity.claims().get(AuthenticationConstants.VERSION_CLAIM); + String appId = ""; + + // The Emulator, depending on Version, sends the AppId via either the + // appid claim (Version 1) or the Authorized Party claim (Version 2). + if (StringUtils.isEmpty(tokenVersion) || tokenVersion.equalsIgnoreCase("1.0")) { + // either no Version or a version of "1.0" means we should look for + // the claim in the "appid" claim. + if (!identity.claims().containsKey(AuthenticationConstants.APPID_CLAIM)) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '1.0'.", AuthenticationConstants.APPID_CLAIM)); + } + + appId = identity.claims().get(AuthenticationConstants.APPID_CLAIM); + } else if (tokenVersion.equalsIgnoreCase("2.0")) { + // Emulator, "2.0" puts the AppId in the "azp" claim. + if (!identity.claims().containsKey(AuthenticationConstants.AUTHORIZED_PARTY)) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '2.0'.", AuthenticationConstants.AUTHORIZED_PARTY)); + } + + appId = identity.claims().get(AuthenticationConstants.AUTHORIZED_PARTY); + } else { + // Unknown Version. Not Authorized. + throw new AuthenticationException(String.format("Unknown Emulator Token version '%s'.", tokenVersion)); + } + + if (!credentials.isValidAppIdAsync(appId).join()) { + throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appId)); + } + + return identity; + }); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java index 4182b11c0..f1b6b0288 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java @@ -1,25 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.authentication; +import org.apache.commons.lang3.StringUtils; + import java.util.List; public abstract class EndorsementsValidator { - /** * Verify that the set of ChannelIds, which come from the incoming activities, * all match the endorsements found on the JWT Token. * For example, if an Activity comes from webchat, that channelId says * says "webchat" and the jwt token endorsement MUST match that. - * @param channelId The channel name, typically extracted from the activity.ChannelId field, that - * to which the Activity is affinitized. + * + * @param channelId The channel name, typically extracted from the activity.ChannelId field, that + * to which the Activity is affinitized. * @param endorsements Whoever signed the JWT token is permitted to send activities only for - * some specific channels. That list is the endorsement list, and is validated here against the channelId. + * some specific channels. That list is the endorsement list, and is validated here against the channelId. * @return True is the channelId is found in the Endorsement set. False if the channelId is not found. */ public static boolean validate(String channelId, List endorsements) { // If the Activity came in and doesn't have a Channel ID then it's making no // assertions as to who endorses it. This means it should pass. - if (channelId == null || channelId.isEmpty()) + if (StringUtils.isEmpty(channelId)) return true; if (endorsements == null) @@ -35,12 +40,6 @@ public static boolean validate(String channelId, List endorsements) { // JwtTokenExtractor // Does the set of endorsements match the channelId that was passed in? - - // ToDo: Consider moving this to a HashSet instead of a string - // array, to make lookups O(1) instead of O(N). To give a sense - // of scope, tokens from WebChat have about 10 endorsements, and - // tokens coming from Teams have about 20. - return endorsements.contains(channelId); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java new file mode 100644 index 000000000..265401525 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.adal4j.AuthenticationException; +import com.microsoft.bot.connector.ExecutorFactory; +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +public class EnterpriseChannelValidation { + private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{ + this.validateIssuer = true; + this.validIssuers = new ArrayList() {{ + add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + }}; + this.validateAudience = false; + this.validateLifetime = true; + this.clockSkew = Duration.ofMinutes(5); + this.requireSignedTokens = true; + }}; + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework Channel Service. + * A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelProvider The channelService value that distinguishes public Azure from US Government Azure. + * @param channelId The ID of the channel to validate. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String serviceUrl, String channelId) { + return authenticateToken(authHeader, credentials, channelProvider, serviceUrl, channelId, new AuthenticationConfiguration()); + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework Channel Service. + * A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param channelProvider The channelService value that distinguishes public Azure from US Government Azure. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String serviceUrl, String channelId, AuthenticationConfiguration authConfig) { + if (authConfig == null) { + throw new IllegalArgumentException("Missing AuthenticationConfiguration"); + } + + return channelProvider.getChannelService() + + .thenCompose(channelService -> { + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + TOKENVALIDATIONPARAMETERS, + String.format(AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT, channelService), + AuthenticationConstants.AllowedSigningAlgorithms); + + return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements()); + }) + + .thenCompose(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + return validateIdentity(identity, credentials, serviceUrl); + }); + } + + public static CompletableFuture validateIdentity(ClaimsIdentity identity, CredentialProvider credentials, String serviceUrl) { + return CompletableFuture.supplyAsync(() -> { + if (identity == null || !identity.isAuthenticated()) { + throw new AuthenticationException("Invalid Identity"); + } + + // Now check that the AppID in the claim set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + if (!StringUtils.equalsIgnoreCase(identity.getIssuer(), AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) { + throw new AuthenticationException("Wrong Issuer"); + } + + // The AppId from the claim in the token must match the AppId specified by the developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + throw new AuthenticationException("No Audience Claim"); + } + + boolean isValid = credentials.isValidAppIdAsync(appIdFromAudienceClaim).join(); + if (!isValid) { + throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim)); + } + + String serviceUrlClaim = identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM); + if (StringUtils.isEmpty(serviceUrl)) { + throw new AuthenticationException(String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim)); + } + + if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) { + throw new AuthenticationException(String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim)); + } + + return identity; + }, ExecutorFactory.getExecutor()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java new file mode 100644 index 000000000..9608f98be --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * Values and Constants used for Authentication and Authorization by the Bot Framework Protocol + * to US Government DataCenters. + */ +public class GovernmentAuthenticationConstants { + public static final String CHANNELSERVICE = "https://botframework.azure.us"; + + /** + * TO GOVERNMENT CHANNEL FROM BOT: Login URL. + */ + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e"; + + /** + * TO GOVERNMENT CHANNEL FROM BOT: OAuth scope to request. + */ + public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.us"; + + /** + * TO BOT FROM GOVERNMENT CHANNEL: Token issuer. + */ + public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.us"; + + /** + * OAuth Url used to get a token from OAuthApiClient. + */ + public static final String OAUTH_URL_GOV = "https://api.botframework.azure.us"; + + /** + * TO BOT FROM GOVERNMANT CHANNEL: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = "https://login.botframework.azure.us/v1/.well-known/openidconfiguration"; + + /** + * TO BOT FROM GOVERNMENT EMULATOR: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration"; +} + diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java new file mode 100644 index 000000000..af5494e48 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.adal4j.AuthenticationException; +import com.microsoft.bot.connector.ExecutorFactory; +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +/** + * TO BOT FROM GOVERNMENT CHANNEL: Token validation parameters when connecting to a bot. + */ +public class GovernmentChannelValidation { + private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{ + this.validateIssuer = true; + this.validIssuers = new ArrayList() {{ + add(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + }}; + this.validateAudience = false; + this.validateLifetime = true; + this.clockSkew = Duration.ofMinutes(5); + this.requireSignedTokens = true; + }}; + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework Government Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Authentication failed. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String serviceUrl, String channelId) { + return authenticateToken(authHeader, credentials, serviceUrl, channelId, new AuthenticationConfiguration()); + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework Government Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Authentication failed. + */ + public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String serviceUrl, String channelId, AuthenticationConfiguration authConfig) { + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + TOKENVALIDATIONPARAMETERS, + GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL, + AuthenticationConstants.AllowedSigningAlgorithms); + + return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements()) + .thenCompose(identity -> { + return validateIdentity(identity, credentials, serviceUrl); + }); + } + + /** + * Validate the ClaimsIdentity as sent from a Bot Framework Government Channel Service. + * + * @param identity The claims identity to validate. + * @param credentials The user defined set of valid credentials, such as the AppId. + * @param serviceUrl The service url from the request. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Validation failed. + */ + public static CompletableFuture validateIdentity(ClaimsIdentity identity, CredentialProvider credentials, String serviceUrl) { + + return CompletableFuture.supplyAsync(() -> { + if (identity == null || !identity.isAuthenticated()) { + throw new AuthenticationException("Invalid Identity"); + } + + // Now check that the AppID in the claim set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + if (!StringUtils.equalsIgnoreCase(identity.getIssuer(), GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) { + throw new AuthenticationException("Wrong Issuer"); + } + + // The AppId from the claim in the token must match the AppId specified by the developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + throw new AuthenticationException("No Audience Claim"); + } + + boolean isValid = credentials.isValidAppIdAsync(appIdFromAudienceClaim).join(); + if (!isValid) { + throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim)); + } + + String serviceUrlClaim = identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM); + if (StringUtils.isEmpty(serviceUrl)) { + throw new AuthenticationException(String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim)); + } + + if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) { + throw new AuthenticationException(String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim)); + } + + return identity; + }, ExecutorFactory.getExecutor()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java index 3273a846e..b37b905ba 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java @@ -9,19 +9,19 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; import com.microsoft.aad.adal4j.AuthenticationException; +import com.microsoft.bot.connector.ExecutorFactory; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.logging.Level; -import java.util.logging.Logger; public class JwtTokenExtractor { - private static final Logger LOGGER = Logger.getLogger(OpenIdMetadata.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class); private static final ConcurrentMap openIdMetadataCache = new ConcurrentHashMap<>(); @@ -37,30 +37,34 @@ public JwtTokenExtractor(TokenValidationParameters tokenValidationParameters, St } public CompletableFuture getIdentityAsync(String authorizationHeader, String channelId) { + return getIdentityAsync(authorizationHeader, channelId, new ArrayList<>()); + } + + public CompletableFuture getIdentityAsync(String authorizationHeader, String channelId, List requiredEndorsements) { if (authorizationHeader == null) { - return CompletableFuture.completedFuture(null); + throw new IllegalArgumentException("authorizationHeader is required"); } String[] parts = authorizationHeader.split(" "); if (parts.length == 2) { - return getIdentityAsync(parts[0], parts[1], channelId); + return getIdentityAsync(parts[0], parts[1], channelId, requiredEndorsements); } return CompletableFuture.completedFuture(null); } - public CompletableFuture getIdentityAsync(String schema, String token, String channelId) { + public CompletableFuture getIdentityAsync(String schema, String token, String channelId, List requiredEndorsements) { // No header in correct scheme or no token if (!schema.equalsIgnoreCase("bearer") || token == null) { return CompletableFuture.completedFuture(null); } // Issuer isn't allowed? No need to check signature - if (!this.hasAllowedIssuer(token)) { + if (!hasAllowedIssuer(token)) { return CompletableFuture.completedFuture(null); } - return this.validateTokenAsync(token, channelId); + return validateTokenAsync(token, channelId, requiredEndorsements); } private boolean hasAllowedIssuer(String token) { @@ -69,11 +73,15 @@ private boolean hasAllowedIssuer(String token) { } @SuppressWarnings("unchecked") - private CompletableFuture validateTokenAsync(String token, String channelId) { + private CompletableFuture validateTokenAsync(String token, String channelId, List requiredEndorsements) { DecodedJWT decodedJWT = JWT.decode(token); OpenIdMetadataKey key = this.openIdMetadata.getKey(decodedJWT.getKeyId()); - if (key != null) { + if (key == null) { + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.supplyAsync(() -> { Verification verification = JWT.require(Algorithm.RSA256(key.key, null)); try { verification.build().verify(token); @@ -87,26 +95,25 @@ private CompletableFuture validateTokenAsync(String token, Strin if (!isEndorsed) { throw new AuthenticationException(String.format("Could not validate endorsement for key: %s with endorsements: %s", key.key.toString(), StringUtils.join(key.endorsements))); } + + // Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well + boolean additionalEndorsementsSatisfied = + requiredEndorsements.stream().allMatch((endorsement) -> EndorsementsValidator.validate(endorsement, key.endorsements)); + if (!additionalEndorsementsSatisfied) { + throw new AuthenticationException(String.format("Could not validate additional endorsement for key: %s with endorsements: %s", key.key.toString(), StringUtils.join(requiredEndorsements))); + } } if (!this.allowedSigningAlgorithms.contains(decodedJWT.getAlgorithm())) { throw new AuthenticationException(String.format("Could not validate algorithm for key: %s with algorithms: %s", decodedJWT.getAlgorithm(), StringUtils.join(allowedSigningAlgorithms))); } - Map claims = new HashMap<>(); - if (decodedJWT.getClaims() != null) { - decodedJWT.getClaims().forEach((k, v) -> claims.put(k, v.asString())); - } - - return CompletableFuture.completedFuture(new ClaimsIdentityImpl(decodedJWT.getIssuer(), claims)); - + return new ClaimsIdentity(decodedJWT); } catch (JWTVerificationException ex) { String errorDescription = ex.getMessage(); - LOGGER.log(Level.WARNING, errorDescription); - return CompletableFuture.completedFuture(null); + LOGGER.warn(errorDescription); + throw new AuthenticationException(ex); } - } - - return CompletableFuture.completedFuture(null); + }, ExecutorFactory.getExecutor()); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java index 0c5ebd3dd..0c4a10546 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java @@ -4,11 +4,16 @@ package com.microsoft.bot.connector.authentication; import com.microsoft.aad.adal4j.AuthenticationException; +import com.microsoft.bot.connector.ExecutorFactory; import com.microsoft.bot.schema.models.Activity; +import org.apache.commons.lang3.StringUtils; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +/** + * Contains helper methods for authenticating incoming HTTP requests. + */ public class JwtTokenValidation { /** @@ -17,48 +22,106 @@ public class JwtTokenValidation { * @param activity The incoming Activity from the Bot Framework or the Emulator * @param authHeader The Bearer token included as part of the request * @param credentials The set of valid credentials, such as the Bot Application ID - * @return Nothing + * @return A task that represents the work queued to execute. * @throws AuthenticationException Throws on auth failed. */ - public static CompletableFuture authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials) throws AuthenticationException, InterruptedException, ExecutionException { - if (authHeader == null || authHeader.isEmpty()) { - // No auth header was sent. We might be on the anonymous code path. - boolean isAuthDisable = credentials.isAuthenticationDisabledAsync().get(); - if (isAuthDisable) { - // In the scenario where Auth is disabled, we still want to have the - // IsAuthenticated flag set in the ClaimsIdentity. To do this requires - // adding in an empty claim. - return CompletableFuture.completedFuture(new ClaimsIdentityImpl("anonymous")); + public static CompletableFuture authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials, ChannelProvider channelProvider) throws AuthenticationException, InterruptedException, ExecutionException { + return authenticateRequest(activity, authHeader, credentials, channelProvider, new AuthenticationConfiguration()); + } + + /** + * Validates the security tokens required by the Bot Framework Protocol. Throws on any exceptions. + * + * @param activity The incoming Activity from the Bot Framework or the Emulator + * @param authHeader The Bearer token included as part of the request + * @param credentials The set of valid credentials, such as the Bot Application ID + * @param authConfig The authentication configuration. + * @return A task that represents the work queued to execute. + * @throws AuthenticationException Throws on auth failed. + */ + public static CompletableFuture authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, AuthenticationConfiguration authConfig) throws AuthenticationException, InterruptedException, ExecutionException { + return CompletableFuture.supplyAsync(() -> { + if (StringUtils.isEmpty(authHeader)) { + // No auth header was sent. We might be on the anonymous code path. + boolean isAuthDisable = credentials.isAuthenticationDisabledAsync().join(); + if (isAuthDisable) { + // In the scenario where Auth is disabled, we still want to have the + // IsAuthenticated flag set in the ClaimsIdentity. To do this requires + // adding in an empty claim. + return new ClaimsIdentity("anonymous"); + } + + // No Auth Header. Auth is required. Request is not authorized. + throw new AuthenticationException("No Auth Header. Auth is required."); } - // No Auth Header. Auth is required. Request is not authorized. - throw new AuthenticationException("No Auth Header. Auth is required."); - } + // Go through the standard authentication path. This will throw AuthenticationException if + // it fails. + ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(authHeader, credentials, channelProvider, activity.channelId(), activity.serviceUrl(), authConfig).join(); + + // On the standard Auth path, we need to trust the URL that was incoming. + MicrosoftAppCredentials.trustServiceUrl(activity.serviceUrl()); - // Go through the standard authentication path. - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(authHeader, credentials, activity.channelId(), activity.serviceUrl()).get(); + return identity; + }, ExecutorFactory.getExecutor()); + } - // On the standard Auth path, we need to trust the URL that was incoming. - MicrosoftAppCredentials.trustServiceUrl(activity.serviceUrl()); - return CompletableFuture.completedFuture(identity); + /** + * Validates the authentication header of an incoming request. + * + * @param authHeader The authentication header to validate. + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @param channelId The ID of the channel that sent the request. + * @param serviceUrl The service URL for the activity. + * @return A task that represents the work queued to execute. + * + * On Call: + * @throws IllegalArgumentException Incorrect arguments supplied + * + * On join: + * @throws AuthenticationException Authentication Error + */ + public static CompletableFuture validateAuthHeader(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, String serviceUrl) { + return validateAuthHeader(authHeader, credentials, channelProvider, channelId, serviceUrl, new AuthenticationConfiguration()); } - // TODO: Recieve httpClient and use ClientID - public static CompletableFuture validateAuthHeader(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException { - if (authHeader == null || authHeader.isEmpty()) { + /** + * Validates the authentication header of an incoming request. + * + * @param authHeader The authentication header to validate. + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @param channelId The ID of the channel that sent the request. + * @param serviceUrl The service URL for the activity. + * @param authConfig The authentication configuration. + * @return A task that represents the work queued to execute. + * + * On Call: + * @throws IllegalArgumentException Incorrect arguments supplied + * + * On Join: + * @throws AuthenticationException Authentication Error + */ + public static CompletableFuture validateAuthHeader(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, String serviceUrl, AuthenticationConfiguration authConfig) { + if (StringUtils.isEmpty(authHeader)) { throw new IllegalArgumentException("No authHeader present. Auth is required."); } - boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader).get(); + boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader); if (usingEmulator) { - return EmulatorValidation.authenticateToken(authHeader, credentials, channelId); - } else { + return EmulatorValidation.authenticateToken(authHeader, credentials, channelProvider, channelId, authConfig); + } else if (channelProvider == null || channelProvider.isPublicAzure()) { // No empty or null check. Empty can point to issues. Null checks only. if (serviceUrl != null) { - return ChannelValidation.authenticateToken(authHeader, credentials, channelId, serviceUrl); + return ChannelValidation.authenticateToken(authHeader, credentials, channelId, serviceUrl, authConfig); } else { - return ChannelValidation.authenticateToken(authHeader, credentials, channelId); + return ChannelValidation.authenticateToken(authHeader, credentials, channelId, authConfig); } + } else if (channelProvider.isGovernment()) { + return GovernmentChannelValidation.authenticateToken(authHeader, credentials, serviceUrl, channelId, authConfig); + } else { + return EnterpriseChannelValidation.authenticateToken(authHeader, credentials, channelProvider, serviceUrl, channelId, authConfig); } } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java index f4c54e513..a6d7bad70 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java @@ -3,111 +3,60 @@ package com.microsoft.bot.connector.authentication; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.aad.adal4j.ClientCredential; import com.microsoft.rest.credentials.ServiceClientCredentials; -import okhttp3.*; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import org.slf4j.LoggerFactory; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotLoginUrl; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotOAuthScope; - +/** + * MicrosoftAppCredentials auth implementation + */ public class MicrosoftAppCredentials implements ServiceClientCredentials { - private String appId; - private String appPassword; - - private OkHttpClient client; - private ObjectMapper mapper; - public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - public static final MediaType FORM_ENCODE = MediaType.parse("application/x-www-form-urlencoded"); + public static final String MICROSOFTAPPID = "MicrosoftAppId"; + public static final String MICROSOFTAPPPASSWORD = "MicrosoftAppPassword"; - private String currentToken = null; - private long expiredTime = 0; - //private static final Object cacheSync = new Object(); - protected static final HashMap cache = new HashMap(); - - public final String OAuthEndpoint = ToChannelFromBotLoginUrl; - public final String OAuthScope = ToChannelFromBotOAuthScope; + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static ConcurrentMap trustHostNames = new ConcurrentHashMap<>(); - public String getTokenCacheKey() { - return String.format("%s-cache", this.appId); + static { + trustHostNames.put("api.botframework.com", LocalDateTime.MAX); + trustHostNames.put("token.botframework.com", LocalDateTime.MAX); + trustHostNames.put("api.botframework.azure.us", LocalDateTime.MAX); + trustHostNames.put("token.botframework.azure.us", LocalDateTime.MAX); } + private String appId; + private String appPassword; + private String channelAuthTenant; + private AdalAuthenticator authenticator; + public MicrosoftAppCredentials(String appId, String appPassword) { this.appId = appId; this.appPassword = appPassword; - this.client = new OkHttpClient.Builder().build(); - this.mapper = new ObjectMapper().findAndRegisterModules(); } - public static final MicrosoftAppCredentials Empty = new MicrosoftAppCredentials(null, null); - - public String microsoftAppId() { - return this.appId; - } - - public MicrosoftAppCredentials withMicrosoftAppId(String appId) { + public MicrosoftAppCredentials(String appId, String appPassword, String channelAuthTenant) throws MalformedURLException { this.appId = appId; - return this; - } - - public String getToken(Request request) throws IOException { - if (System.currentTimeMillis() < expiredTime) { - return currentToken; - } - Request reqToken = request.newBuilder() - .url(ToChannelFromBotLoginUrl) - .post(new FormBody.Builder() - .add("grant_type", "client_credentials") - .add("client_id", this.appId) - .add("client_secret", this.appPassword) - .add("scope", ToChannelFromBotOAuthScope) - .build()).build(); - Response response = this.client.newCall(reqToken).execute(); - if (response.isSuccessful()) { - String payload = response.body().string(); - AuthenticationResponse authResponse = this.mapper.readValue(payload, AuthenticationResponse.class); - this.expiredTime = System.currentTimeMillis() + (authResponse.expiresIn * 1000); - this.currentToken = authResponse.accessToken; - } - return this.currentToken; - } - - - protected boolean ShouldSetToken(String url) { - if (isTrustedServiceUrl(url)) { - return true; - } - return false; - } - - - @Override - public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { - clientBuilder.interceptors().add(new MicrosoftAppCredentialsInterceptor(this)); + this.appPassword = appPassword; + setChannelAuthTenant(channelAuthTenant); } - private static class AuthenticationResponse { - @JsonProperty(value = "token_type") - String tokenType; - @JsonProperty(value = "expires_in") - long expiresIn; - @JsonProperty(value = "ext_expires_in") - long extExpiresIn; - @JsonProperty(value = "access_token") - String accessToken; + public static MicrosoftAppCredentials empty() { + return new MicrosoftAppCredentials(null, null); } - public static void trustServiceUrl(URI serviceUrl) { trustServiceUrl(serviceUrl.toString(), LocalDateTime.now().plusDays(1)); } @@ -121,8 +70,7 @@ public static void trustServiceUrl(String serviceUrl, LocalDateTime expirationTi URL url = new URL(serviceUrl); trustServiceUrl(url, expirationTime); } catch (MalformedURLException e) { - //TODO: What's missing here? - e.printStackTrace(); + LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("trustServiceUrl", e); } } @@ -135,6 +83,7 @@ public static boolean isTrustedServiceUrl(String serviceUrl) { URL url = new URL(serviceUrl); return isTrustedServiceUrl(url); } catch (MalformedURLException e) { + LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("trustServiceUrl", e); return false; } } @@ -147,9 +96,77 @@ public static boolean isTrustedServiceUrl(HttpUrl url) { return !trustHostNames.getOrDefault(url.host(), LocalDateTime.MIN).isBefore(LocalDateTime.now().minusMinutes(5)); } - private static ConcurrentMap trustHostNames = new ConcurrentHashMap<>(); + public String appId() { + return this.appId; + } - static { - trustHostNames.put("state.botframework.com", LocalDateTime.MAX); + public String appPassword() { + return this.appPassword; + } + + public MicrosoftAppCredentials withAppId(String appId) { + this.appId = appId; + return this; + } + + public MicrosoftAppCredentials withAppPassword(String appPassword) { + this.appPassword = appPassword; + return this; + } + + public String channelAuthTenant() { + return channelAuthTenant == null ? AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT : channelAuthTenant; + } + + public void setChannelAuthTenant(String authTenant) throws MalformedURLException { + String originalAuthTenant = channelAuthTenant; + try { + channelAuthTenant = authTenant; + new URL(oAuthEndpoint()).toString(); + } catch(MalformedURLException e) { + channelAuthTenant = originalAuthTenant; + } + } + + public MicrosoftAppCredentials withChannelAuthTenant(String authTenant) throws MalformedURLException { + setChannelAuthTenant(authTenant); + return this; + } + + public String oAuthEndpoint() { + return String.format(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, channelAuthTenant()); + } + + public String oAuthScope() { + return AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE; + } + + public Future getToken() { + return getAuthenticator().acquireToken(); + } + + protected boolean ShouldSetToken(String url) { + return isTrustedServiceUrl(url); + } + + private AdalAuthenticator getAuthenticator() { + try { + if (this.authenticator == null) { + this.authenticator = new AdalAuthenticator( + new ClientCredential(this.appId, this.appPassword), + new OAuthConfiguration(oAuthEndpoint(), oAuthScope())); + } + } catch(MalformedURLException e) { + // intentional no-op. This class validates the URL on construction or setChannelAuthTenant. + // That is... this will never happen. + LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("getAuthenticator", e); + } + + return this.authenticator; + } + + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new MicrosoftAppCredentialsInterceptor(this)); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java index 793ad89b5..1ee3765bc 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java @@ -31,8 +31,15 @@ class MicrosoftAppCredentialsInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { if (MicrosoftAppCredentials.isTrustedServiceUrl(chain.request().url().url().toString())) { + String token; + try { + token = this.credentials.getToken().get().getAccessToken(); + } catch (Throwable t) { + throw new IOException(t); + } + Request newRequest = chain.request().newBuilder() - .header("Authorization", "Bearer " + this.credentials.getToken(chain.request())) + .header("Authorization", "Bearer " + token) .build(); return chain.proceed(newRequest); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java new file mode 100644 index 000000000..70e737dd4 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * MicrosoftGovernmentAppCredentials auth implementation. + */ +public class MicrosoftGovernmentAppCredentials extends MicrosoftAppCredentials { + /** + * Initializes a new instance of the MicrosoftGovernmentAppCredentials class. + * + * @param appId The Microsoft app ID. + * @param password The Microsoft app password. + */ + public MicrosoftGovernmentAppCredentials(String appId, String password) { + super(appId, password); + } + + public static MicrosoftGovernmentAppCredentials empty() { + return new MicrosoftGovernmentAppCredentials(null, null); + } + + /** + * Gets the OAuth endpoint to use. + * + * @return The OAuth endpoint to use. + */ + @Override + public String oAuthEndpoint() { + return GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL; + } + + /** + * Gets the OAuth scope to use. + * + * @return The OAuth scope to use. + */ + @Override + public String oAuthScope() { + return GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java index 3263c79c8..3dd10eb28 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java @@ -1,9 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.authentication; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.connector.ExecutorFactory; import com.microsoft.bot.connector.UserAgent; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.TokenExchangeState; import com.microsoft.bot.schema.models.Activity; import com.microsoft.bot.schema.models.ConversationReference; @@ -41,13 +45,13 @@ * Uses the MicrosoftInterceptor class to add Authorization header from idp. */ public class OAuthClient extends ServiceClient { - private final ConnectorClientImpl client; + private final RestConnectorClient client; private final String uri; private ObjectMapper mapper; - public OAuthClient(ConnectorClientImpl client, String uri) throws URISyntaxException, MalformedURLException { + public OAuthClient(RestConnectorClient client, String uri) throws URISyntaxException, MalformedURLException { super(client.restClient()); URI uriResult = new URI(uri); @@ -69,7 +73,7 @@ public OAuthClient(ConnectorClientImpl client, String uri) throws URISyntaxExcep * @param userId * @param connectionName * @param magicCode - * @return CompletableFuture< TokenResponse > on success; otherwise null. + * @return CompletableFuture on success; otherwise null. */ public CompletableFuture GetUserTokenAsync(String userId, String connectionName, String magicCode) throws IOException, URISyntaxException, ExecutionException, InterruptedException { return GetUserTokenAsync(userId, connectionName, magicCode, null); @@ -77,14 +81,14 @@ public CompletableFuture GetUserTokenAsync(String userId, String protected URI MakeUri(String uri, HashMap queryStrings) throws URISyntaxException { String newUri = queryStrings.keySet().stream() - .map(key -> { - try { - return key + "=" + URLEncoder.encode(queryStrings.get(key), StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - }) - .collect(joining("&", uri.endsWith("?") ? uri : uri + "?", "")); + .map(key -> { + try { + return key + "=" + URLEncoder.encode(queryStrings.get(key), StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + }) + .collect(joining("&", uri.endsWith("?") ? uri : uri + "?", "")); return new URI(newUri); @@ -97,7 +101,7 @@ protected URI MakeUri(String uri, HashMap queryStrings) throws U * @param connectionName * @param magicCode * @param customHeaders - * @return CompletableFuture< TokenResponse > on success; null otherwise. + * @return CompletableFuture on success; null otherwise. */ public CompletableFuture GetUserTokenAsync(String userId, String connectionName, String magicCode, Map> customHeaders) throws IllegalArgumentException { if (StringUtils.isEmpty(userId)) { @@ -132,13 +136,13 @@ public CompletableFuture GetUserTokenAsync(String userId, String // Later: Use client in clientimpl? OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); + .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) + .build(); Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); + .url(tokenUrl.toString()) + .header("User-Agent", UserAgent.value()) + .build(); Response response = null; try { @@ -158,7 +162,7 @@ public CompletableFuture GetUserTokenAsync(String userId, String response.body().close(); } return null; - }); + }, ExecutorFactory.getExecutor()); } /** @@ -198,14 +202,14 @@ public CompletableFuture SignOutUserAsync(String userId, String connect // Later: Use client in clientimpl? OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); + .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) + .build(); Request request = new Request.Builder() - .delete() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); + .delete() + .url(tokenUrl.toString()) + .header("User-Agent", UserAgent.value()) + .build(); Response response = null; try { @@ -218,7 +222,7 @@ public CompletableFuture SignOutUserAsync(String userId, String connect } return false; - }); + }, ExecutorFactory.getExecutor()); } @@ -238,15 +242,15 @@ public CompletableFuture GetSignInLinkAsync(Activity activity, String co } final MicrosoftAppCredentials creds = (MicrosoftAppCredentials) this.client.restClient().credentials(); TokenExchangeState tokenExchangeState = new TokenExchangeState() - .withConnectionName(connectionName) - .withConversation(new ConversationReference() - .withActivityId(activity.id()) - .withBot(activity.recipient()) - .withChannelId(activity.channelId()) - .withConversation(activity.conversation()) - .withServiceUrl(activity.serviceUrl()) - .withUser(activity.from())) - .withMsAppId((creds == null) ? null : creds.microsoftAppId()); + .withConnectionName(connectionName) + .withConversation(new ConversationReference() + .withActivityId(activity.id()) + .withBot(activity.recipient()) + .withChannelId(activity.channelId()) + .withConversation(activity.conversation()) + .withServiceUrl(activity.serviceUrl()) + .withUser(activity.from())) + .withMsAppId((creds == null) ? null : creds.appId()); String serializedState = this.mapper.writeValueAsString(tokenExchangeState); @@ -266,13 +270,13 @@ public CompletableFuture GetSignInLinkAsync(Activity activity, String co // Later: Use client in clientimpl? OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(creds)) - .build(); + .addInterceptor(new MicrosoftAppCredentialsInterceptor(creds)) + .build(); Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); + .url(tokenUrl.toString()) + .header("User-Agent", UserAgent.value()) + .build(); Response response = null; try { @@ -284,7 +288,7 @@ public CompletableFuture GetSignInLinkAsync(Activity activity, String co e.printStackTrace(); } return null; - }); + }, ExecutorFactory.getExecutor()); } /** @@ -313,14 +317,14 @@ public CompletableFuture SendEmulateOAuthCardsAsync(Boolean emulateOAuthCards) t // Later: Use client in clientimpl? OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); + .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) + .build(); Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .post(body) - .build(); + .url(tokenUrl.toString()) + .header("User-Agent", UserAgent.value()) + .post(body) + .build(); Response response = null; try { @@ -336,6 +340,6 @@ public CompletableFuture SendEmulateOAuthCardsAsync(Boolean emulateOAuthCards) t // Apparently swallow any results return; - }); + }, ExecutorFactory.getExecutor()); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java new file mode 100644 index 000000000..133182caa --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +/** + * Configuration for OAuth client credential authentication. + */ +public class OAuthConfiguration { + private String scope; + private String authority; + + public OAuthConfiguration(String authority, String scope) { + this.authority = authority; + this.scope = scope; + } + + /** + * Sets oAuth Authority for authentication. + * + * @param authority + * @return This OAuthConfiguration object. + */ + public OAuthConfiguration withAuthority(String authority) { + this.authority = authority; + return this; + } + + /** + * Gets oAuth Authority for authentication. + * + * @return OAuth Authority for authentication. + */ + public String authority() { + return authority; + } + + /** + * Sets oAuth scope for authentication. + * + * @param scope + * @return This OAuthConfiguration object. + */ + public OAuthConfiguration withScope(String scope) { + this.scope = scope; + return this; + } + + /** + * Gets oAuth scope for authentication. + * + * @return OAuth scope for authentication. + */ + public String scope() { + return scope; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java index 45378175f..150bd891f 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java @@ -16,21 +16,21 @@ import java.security.interfaces.RSAPublicKey; import java.util.HashMap; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class OpenIdMetadata { - private static final Logger LOGGER = Logger.getLogger( OpenIdMetadata.class.getName() ); + private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class); - private String url; - private long lastUpdated; - private JwkProvider cacheKeys; - private ObjectMapper mapper; + private String url; + private long lastUpdated; + private JwkProvider cacheKeys; + private ObjectMapper mapper; - OpenIdMetadata(String url) { - this.url = url; - this.mapper = new ObjectMapper().findAndRegisterModules(); - } + OpenIdMetadata(String url) { + this.url = url; + this.mapper = new ObjectMapper().findAndRegisterModules(); + } public OpenIdMetadataKey getKey(String keyId) { // If keys are more than 5 days old, refresh them @@ -52,7 +52,7 @@ private String refreshCache() { return IOUtils.toString(keysUrl); } catch (IOException e) { String errorDescription = String.format("Failed to load openID config: %s", e.getMessage()); - LOGGER.log(Level.WARNING, errorDescription); + LOGGER.warn(errorDescription); } return null; } @@ -67,7 +67,7 @@ private OpenIdMetadataKey findKey(String keyId) { return key; } catch (JwkException e) { String errorDescription = String.format("Failed to load keys: %s", e.getMessage()); - LOGGER.log(Level.WARNING, errorDescription); + LOGGER.warn(errorDescription); } return null; } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java deleted file mode 100644 index 61baa17c8..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.microsoft.bot.connector.authentication; - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Response; - -public class ResponseFuture implements Callback { - public final CompletableFuture future = new CompletableFuture(); - public Call call; - - public ResponseFuture(Call call) { - this.call = call; - } - - @Override public void onFailure(Call call, IOException e) { - future.completeExceptionally(e); - } - - @Override public void onResponse(Call call, Response response) throws IOException { - future.complete(response); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java new file mode 100644 index 000000000..9bc4fad0a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.CompletableFuture; + +public class SimpleChannelProvider implements ChannelProvider { + private String channelService; + + /** + * Creates a SimpleChannelProvider with no ChannelService which will use Public Azure. + */ + public SimpleChannelProvider() { + + } + + /** + * Creates a SimpleChannelProvider with the specified ChannelService. + * + * @param channelService The ChannelService to use. Null or empty strings represent Public Azure, + * the string 'https://botframework.us' represents US Government Azure, and + * other values are for private channels. + */ + public SimpleChannelProvider(String channelService) { + this.channelService = channelService; + } + + @Override + public CompletableFuture getChannelService() { + return CompletableFuture.completedFuture(channelService); + } + + @Override + public boolean isGovernment() { + return GovernmentAuthenticationConstants.CHANNELSERVICE.equalsIgnoreCase(channelService); + } + + @Override + public boolean isPublicAzure() { + return StringUtils.isEmpty(channelService); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java index cc3e0a119..80ffd2853 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java @@ -1,15 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.authentication; import org.apache.commons.lang3.StringUtils; + import java.util.concurrent.CompletableFuture; +/** + * A simple implementation of the CredentialProvider interface. + */ public class SimpleCredentialProvider implements CredentialProvider { private String appId; private String password; - + /** + * Initializes a new instance with empty credentials. + */ public SimpleCredentialProvider() { } + + /** + * Initializes a new instance with the provided credentials. + * + * @param appId The app ID. + * @param password The app password. + */ public SimpleCredentialProvider(String appId, String password) { this.appId = appId; this.password = password; @@ -18,6 +34,7 @@ public SimpleCredentialProvider(String appId, String password) { public String getAppId() { return this.appId; } + public void setAppId(String appId) { this.appId = appId; } @@ -25,24 +42,42 @@ public void setAppId(String appId) { public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + /** + * Validates an app ID. + * + * @param appId The app ID to validate. + * @return If the task is successful, the result is true if appId is valid for the controller; otherwise, false. + */ @Override public CompletableFuture isValidAppIdAsync(String appId) { - return CompletableFuture.completedFuture(appId == this.appId); + return CompletableFuture.completedFuture(StringUtils.equals(appId, this.appId)); } + /** + * Gets the app password for a given bot app ID. + * + * @param appId The ID of the app to get the password for. + * @return If the task is successful and the app ID is valid, the result + * contains the password; otherwise, null. + */ @Override public CompletableFuture getAppPasswordAsync(String appId) { - return CompletableFuture.completedFuture((appId == this.appId) ? this.password : null); + return CompletableFuture.completedFuture(StringUtils.equals(appId, this.appId) ? this.password : null); } + /** + * Checks whether bot authentication is disabled. + * + * @return A task that represents the work queued to execute If the task is successful and bot authentication + * is disabled, the result is true; otherwise, false. + */ @Override public CompletableFuture isAuthenticationDisabledAsync() { return CompletableFuture.completedFuture(StringUtils.isEmpty(this.appId)); } - - } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java index b27706033..98b90ef2f 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java @@ -4,11 +4,8 @@ package com.microsoft.bot.connector.authentication; import java.time.Duration; -import java.util.ArrayList; import java.util.List; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToBotFromChannelTokenIssuer; - public class TokenValidationParameters { public boolean validateIssuer; public List validIssuers; @@ -32,33 +29,4 @@ public TokenValidationParameters(boolean validateIssuer, List validIssue this.clockSkew = clockSkew; this.requireSignedTokens = requireSignedTokens; } - - static TokenValidationParameters toBotFromEmulatorTokenValidationParameters() { - return new TokenValidationParameters() {{ - this.validateIssuer = true; - this.validIssuers = new ArrayList() {{ - add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/"); - add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0"); - add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/"); - add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0"); - }}; - this.validateAudience = false; - this.validateLifetime = true; - this.clockSkew = Duration.ofMinutes(5); - this.requireSignedTokens = true; - }}; - } - - static TokenValidationParameters toBotFromChannelTokenValidationParameters() { - return new TokenValidationParameters() {{ - this.validateIssuer = true; - this.validIssuers = new ArrayList() {{ - add(ToBotFromChannelTokenIssuer); - }}; - this.validateAudience = false; - this.validateLifetime = true; - this.clockSkew = Duration.ofMinutes(5); - this.requireSignedTokens = true; - }}; - } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java new file mode 100644 index 000000000..b02693e21 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the implementation auth classes for ConnectorClient. + */ +package com.microsoft.bot.connector.authentication; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java deleted file mode 100644 index 4b326a809..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java +++ /dev/null @@ -1,2 +0,0 @@ -package com.microsoft.bot.connector.implementation; - diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java index 7517691b8..c4b63b1fc 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java @@ -1,25 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for // license information. -// -// Code generated by Microsoft (R) AutoRest Code Generator. -// Changes may cause incorrect behavior and will be lost if the code is -// regenerated. /** - * This package contains the classes for ConnectorClient. - * The Bot Connector REST API allows your bot to send and receive messages to channels configured in the - [Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST - and JSON over HTTPS. - Client libraries for this REST API are available. See below for a list. - Many bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The - Bot State REST API allows a bot to store and retrieve state associated with users and conversations. - Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is - described in detail in the [Connector Authentication](/en-us/restapi/authentication) document. - # Client Libraries for the Bot Connector REST API - * [Bot Builder for C#](/en-us/csharp/builder/sdkreference/) - * [Bot Builder for Node.js](/en-us/node/builder/overview/) - * Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json) - © 2016 Microsoft. + * This package contains the classes for Bot-Connector. */ package com.microsoft.bot.connector; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java similarity index 87% rename from libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java index 7e4a0f19f..390c35f13 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java @@ -2,13 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. */ -package com.microsoft.bot.connector.models; +package com.microsoft.bot.connector.rest; import com.microsoft.rest.RestException;import com.microsoft.bot.schema.models.ErrorResponse; import okhttp3.ResponseBody; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java similarity index 95% rename from libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java index ccef8de03..68ba3f111 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java @@ -2,19 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. */ -package com.microsoft.bot.connector.implementation; +package com.microsoft.bot.connector.rest; import retrofit2.Retrofit; import com.microsoft.bot.connector.Attachments; import com.google.common.reflect.TypeToken; import com.microsoft.bot.schema.models.AttachmentInfo; -import com.microsoft.bot.connector.models.ErrorResponseException; import com.microsoft.rest.ServiceCallback; import com.microsoft.rest.ServiceFuture; import com.microsoft.rest.ServiceResponse; @@ -34,11 +29,11 @@ * An instance of this class provides access to all the operations defined * in Attachments. */ -public class AttachmentsImpl implements Attachments { +public class RestAttachments implements Attachments { /** The Retrofit service to perform REST calls. */ private AttachmentsService service; /** The service client containing this operation class. */ - private ConnectorClientImpl client; + private RestConnectorClient client; /** * Initializes an instance of AttachmentsImpl. @@ -46,7 +41,7 @@ public class AttachmentsImpl implements Attachments { * @param retrofit the Retrofit instance built from a Retrofit Builder. * @param client the instance of the service client containing this operation class. */ - public AttachmentsImpl(Retrofit retrofit, ConnectorClientImpl client) { + RestAttachments(Retrofit retrofit, RestConnectorClient client) { this.service = retrofit.create(AttachmentsService.class); this.client = client; } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java similarity index 81% rename from libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java index e46eca1d3..ca92b46e4 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java @@ -2,10 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * */ -package com.microsoft.bot.connector.implementation; +package com.microsoft.bot.connector.rest; import com.microsoft.azure.AzureClient; import com.microsoft.azure.AzureResponseBuilder; @@ -24,13 +23,28 @@ import java.util.Properties; /** - * Initializes a new instance of the ConnectorClientImpl class. + * The Bot Connector REST API allows your bot to send and receive messages + * to channels configured in the + * [Bot Framework Developer Portal](https://dev.botframework.com). The + * Connector service uses industry-standard REST + * and JSON over HTTPS. + * + * Client libraries for this REST API are available. See below for a list. + * + * Many bots will use both the Bot Connector REST API and the associated + * [Bot State REST API](/en-us/restapi/state). The + * Bot State REST API allows a bot to store and retrieve state associated + * with users and conversations. + * + * Authentication for both the Bot Connector and Bot State REST APIs is + * accomplished with JWT Bearer tokens, and is + * described in detail in the [Connector + * Authentication](/en-us/restapi/authentication) document. */ -public class ConnectorClientImpl extends AzureServiceClient implements ConnectorClient { +public class RestConnectorClient extends AzureServiceClient implements ConnectorClient { /** the {@link AzureClient} used for long running operations. */ private AzureClient azureClient; - /** * Gets the {@link AzureClient} used for long running operations. * @return the azure client; @@ -58,7 +72,7 @@ public String acceptLanguage() { * @param acceptLanguage the acceptLanguage value. * @return the service client itself */ - public ConnectorClientImpl withAcceptLanguage(String acceptLanguage) { + public RestConnectorClient withAcceptLanguage(String acceptLanguage) { this.acceptLanguage = acceptLanguage; return this; } @@ -68,7 +82,7 @@ public ConnectorClientImpl withAcceptLanguage(String acceptLanguage) { * TODO: Use this. */ private RetryStrategy retryStrategy = null; - public ConnectorClientImpl withRestRetryStrategy(RetryStrategy retryStrategy) { + public RestConnectorClient withRestRetryStrategy(RetryStrategy retryStrategy) { this.retryStrategy = retryStrategy; return this; } @@ -94,7 +108,7 @@ public int longRunningOperationRetryTimeout() { * @param longRunningOperationRetryTimeout the longRunningOperationRetryTimeout value. * @return the service client itself */ - public ConnectorClientImpl withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) { + public RestConnectorClient withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) { this.longRunningOperationRetryTimeout = longRunningOperationRetryTimeout; return this; } @@ -117,7 +131,7 @@ public boolean generateClientRequestId() { * @param generateClientRequestId the generateClientRequestId value. * @return the service client itself */ - public ConnectorClientImpl withGenerateClientRequestId(boolean generateClientRequestId) { + public RestConnectorClient withGenerateClientRequestId(boolean generateClientRequestId) { this.generateClientRequestId = generateClientRequestId; return this; } @@ -138,14 +152,14 @@ public Attachments attachments() { /** * The Conversations object to access its operations. */ - private ConversationsImpl conversations; + private RestConversations conversations; /** * Gets the Conversations object to access its operations. * @return the Conversations object. */ @Override - public ConversationsImpl conversations() { + public RestConversations conversations() { return this.conversations; } @@ -154,7 +168,7 @@ public ConversationsImpl conversations() { * * @param credentials the management credentials for Azure */ - public ConnectorClientImpl(ServiceClientCredentials credentials) { + public RestConnectorClient(ServiceClientCredentials credentials) { this("https://api.botframework.com", credentials); } @@ -164,7 +178,7 @@ public ConnectorClientImpl(ServiceClientCredentials credentials) { * @param baseUrl the base URL of the host * @param credentials the management credentials for Azure */ - public ConnectorClientImpl(String baseUrl, ServiceClientCredentials credentials) { + public RestConnectorClient(String baseUrl, ServiceClientCredentials credentials) { super(baseUrl, credentials); initialize(); } @@ -174,7 +188,7 @@ public ConnectorClientImpl(String baseUrl, ServiceClientCredentials credentials) * * @param restClient the REST client to connect to Azure. */ - public ConnectorClientImpl(RestClient restClient){ + public RestConnectorClient(RestClient restClient){ super(restClient); initialize(); } @@ -183,8 +197,8 @@ protected void initialize() { this.acceptLanguage = "en-US"; this.longRunningOperationRetryTimeout = 30; this.generateClientRequestId = true; - this.attachments = new AttachmentsImpl(restClient().retrofit(), this); - this.conversations = new ConversationsImpl(restClient().retrofit(), this); + this.attachments = new RestAttachments(restClient().retrofit(), this); + this.conversations = new RestConversations(restClient().retrofit(), this); this.azureClient = new AzureClient(this); @@ -192,7 +206,7 @@ protected void initialize() { String build_version; final Properties properties = new Properties(); try { - InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties"); + InputStream propStream = RestConnectorClient.class.getClassLoader().getResourceAsStream("project.properties"); properties.load(propStream); build_version = properties.getProperty("version"); } catch (IOException e) { diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java similarity index 99% rename from libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java index dfb2309f9..40d2d1d96 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java @@ -2,12 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * - * NOT GENERATED. - * This uses Java 8 CompletionStage for async processing instead of JavaRX/Guava */ -package com.microsoft.bot.connector.implementation; +package com.microsoft.bot.connector.rest; import retrofit2.Retrofit; import com.microsoft.bot.connector.Conversations; @@ -19,7 +16,6 @@ import com.microsoft.bot.schema.models.ConversationResourceResponse; import com.microsoft.bot.schema.models.ConversationsResult; import com.microsoft.bot.schema.models.PagedMembersResult; -import com.microsoft.bot.connector.models.ErrorResponseException; import com.microsoft.bot.schema.models.ResourceResponse; import com.microsoft.bot.schema.models.Transcript; import com.microsoft.rest.ServiceCallback; @@ -49,11 +45,11 @@ * An instance of this class provides access to all the operations defined * in Conversations. */ -public class ConversationsImpl implements Conversations { +public class RestConversations implements Conversations { /** The Retrofit service to perform REST calls. */ private ConversationsService service; /** The service client containing this operation class. */ - private ConnectorClientImpl client; + private RestConnectorClient client; /** * Initializes an instance of ConversationsImpl. @@ -61,7 +57,7 @@ public class ConversationsImpl implements Conversations { * @param retrofit the Retrofit instance built from a Retrofit Builder. * @param client the instance of the service client containing this operation class. */ - public ConversationsImpl(Retrofit retrofit, ConnectorClientImpl client) { + RestConversations(Retrofit retrofit, RestConnectorClient client) { this.service = retrofit.create(ConversationsService.class); this.client = client; } @@ -284,8 +280,8 @@ public ConversationResourceResponse call(ServiceResponse> CreateConversationAsync(ConversationParameters parameters) { CompletableFuture> future_result = completableFutureFromObservable(createConversationAsync(parameters)); @@ -719,7 +715,7 @@ public Void call(ServiceResponse response) { } }); } - + /** * DeleteConversationMemberFuture * Deletes a member from a converstion. diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java similarity index 96% rename from libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java index e57157f5f..86fe999a4 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java @@ -22,4 +22,4 @@ * Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json) © 2016 Microsoft. */ -package com.microsoft.bot.connector.implementation; +package com.microsoft.bot.connector.rest; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java index 2f62336b7..cf4a977cb 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java @@ -1,13 +1,7 @@ package com.microsoft.bot.connector; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.schema.models.TokenResponse; -import com.microsoft.rest.credentials.ServiceClientCredentials; import okhttp3.OkHttpClient; -import okhttp3.Response; - - -import static java.util.concurrent.CompletableFuture.completedFuture; public class BotAccessTokenStub extends MicrosoftAppCredentials { private final String token; @@ -26,8 +20,4 @@ public BotAccessTokenStub(String token, String appId, String appSecret) { public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { clientBuilder.interceptors().add(new TestBearerTokenInterceptor(this.token)); } - - - - } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java deleted file mode 100644 index f8108fd3e..000000000 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.microsoft.bot.connector; - -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.*; -import com.microsoft.bot.schema.models.Activity; -import okhttp3.Request; -import org.junit.Assert; -import org.junit.Test; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -public class BotAuthenticatorTest { - - private static final String AppId = "2cd87869-38a0-4182-9251-d056e8f0ac24"; - private static final String AppPassword = "2.30Vs3VQLKt974F"; - - @Test - public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get(); - - Assert.assertTrue(identity.isAuthenticated()); - } - - @Test - public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - @Test - public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - // token received and auth disabled - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - @Test - public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (IllegalArgumentException e) { - Assert.assertTrue(e.getMessage().contains("authHeader")); - } - } - - @Test - public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get(); - - Assert.assertTrue(identity.isAuthenticated()); - } - - @Test - public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - /** - * Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list. - */ - @Test - public void ChannelMsaHeaderValidServiceUrlShouldBeTrusted() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - JwtTokenValidation.authenticateRequest( - new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"), - header, - credentials); - - Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/")); - } - - /** - * Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list. - */ - @Test - public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("7f74513e-6f96-4dbc-be9d-9a81fea22b88", ""); - - try { - JwtTokenValidation.authenticateRequest( - new Activity().withServiceUrl("https://webchat.botframework.com/"), - header, - credentials); - Assert.fail("Should have thrown AuthenticationException"); - } catch (AuthenticationException ex) { - Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); - } - - } - - /** - * Tests with no authentication header and makes sure the service URL is not added to the trusted list. - */ - @Test - public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get(); - Assert.assertEquals("anonymous", identity.getIssuer()); - } - - /** - * Tests with no authentication header and makes sure the service URL is not added to the trusted list. - */ - @Test - public void ChannelAuthenticationDisabledServiceUrlShouldNotBeTrusted() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get(); - Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); - } - - private static String getHeaderToken() throws IOException { - Request request = new Request.Builder().url(AuthenticationConstants.ToChannelFromBotLoginUrl).build(); - return String.format("Bearer %s", new MicrosoftAppCredentials(AppId, AppPassword).getToken(request)); - } -} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java index bc8e41aa3..e649af8da 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java @@ -1,12 +1,12 @@ package com.microsoft.bot.connector; import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.models.ChannelAccount; import com.microsoft.rest.RestClient; public class BotConnectorTestBase extends TestBase { - protected ConnectorClientImpl connector; + protected ConnectorClient connector; protected ChannelAccount bot; protected ChannelAccount user; @@ -20,7 +20,7 @@ public BotConnectorTestBase(RunCondition runCondition) { @Override protected void initializeClients(RestClient restClient, String botId, String userId) { - connector = new ConnectorClientImpl(restClient); + connector = new RestConnectorClient(restClient); bot = new ChannelAccount().withId(botId); user = new ChannelAccount().withId(userId); } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java index 97f03d82c..0e5cb6982 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java @@ -1,6 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.connector.models.ErrorResponseException; +import com.microsoft.bot.connector.rest.ErrorResponseException; import com.microsoft.bot.schema.models.*; import org.junit.Assert; import org.junit.Test; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java new file mode 100644 index 000000000..24e27839e --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.EmulatorValidation; +import org.junit.Assert; +import org.junit.Test; + +public class EmulatorValidationTests { + @Test + public void NoSchemeTokenIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("AbCdEf123456")); + } + + @Test + public void OnePartTokenIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer AbCdEf123456")); + } + + @Test + public void NoIssuerIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiSldUIFJ1bGVzISIsImlhdCI6MTQ1OTQ0ODExOSwiZXhwIjoxNDU5NDU0NTE5fQ.-yIVBD5b73C75osbmwwshQNRC7frWUYrqaTjTpza2y4")); + } + + @Test + public void ValidTokenSuccess() { + String emToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCIsImtpZCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCJ9.eyJhdWQiOiI5YzI4NmUyZi1lMDcwLTRhZjUtYTNmMS0zNTBkNjY2MjE0ZWQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTY2NDIyMTY3LCJuYmYiOjE1NjY0MjIxNjcsImV4cCI6MTU2NjQyNjA2NywiYWlvIjoiNDJGZ1lKaDBoK0VmRDAvNnVWaUx6NHZuL25UK0RnQT0iLCJhcHBpZCI6IjljMjg2ZTJmLWUwNzAtNGFmNS1hM2YxLTM1MGQ2NjYyMTRlZCIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJPUXNSLWExUlpFS2tJcG9seUNJUUFBIiwidmVyIjoiMS4wIn0.J9qHO11oZlrpDU3MJcTJe3ErUqj0kw-ZQioYKbkwZ7ZpAx5hl01BETts-LOaE14tImqYqM2K86ZyX5LuAp2snru9LJ4S6-cVZ1_lp_IY4r61UuUJRiVUzn25kRZEN-TFi8Aj1iyL-ueeNr52MM1Sr2UUH73fwrferH8_0qa1IYc7affhjlFEWxSte0SN7iT5WaYK32d_nsgzJdZiCMZJPCpG39U2FYnSI8q7vvYjNbp8wDJc46Q4Jdd3zXYRgHWRBGL_EEkzzk9IFpHN7WoVaqNtgMiA4Vf8bde3eAS5lBBtE5VZ0F6fG4Qeg6zjOAxPBZqvAASMpgyDlSQMknevOQ"; + Assert.assertTrue(EmulatorValidation.isTokenFromEmulator(emToken)); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java index 52875b9c2..b01fc078a 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; import com.microsoft.bot.connector.authentication.EndorsementsValidator; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java index 0a934b147..419e07301 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java @@ -1,18 +1,615 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.aad.adal4j.AuthenticationException; +import com.microsoft.bot.connector.authentication.*; +import com.microsoft.bot.schema.models.Activity; import org.junit.Assert; import org.junit.Test; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; + public class JwtTokenValidationTests { + + private static final String APPID = "2cd87869-38a0-4182-9251-d056e8f0ac24"; + private static final String APPPASSWORD = "2.30Vs3VQLKt974F"; + + private static String getHeaderToken() throws ExecutionException, InterruptedException { + return String.format("Bearer %s", new MicrosoftAppCredentials(APPID, APPPASSWORD).getToken().get().getAccessToken()); + } + + private static String getGovHeaderToken() throws ExecutionException, InterruptedException { + return String.format("Bearer %s", new MicrosoftGovernmentAppCredentials(APPID, APPPASSWORD).getToken().get().getAccessToken()); + } + + @Test + public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider(APPID, ""); + ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + "https://webchat.botframework.com/").join(); + + Assert.assertTrue(identity.isAuthenticated()); + } + + @Test + public void Connector_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { + JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( + APPID, + APPPASSWORD, + GovernmentAuthenticationConstants.CHANNELSERVICE + ); + } + @Test - public void Connector_AuthHeader_CorrectAppIdAndServiceUrl_ShouldValidate() - { -// MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F"); -// String header = "Bearer " + credentials. -// var credentials = new SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", string.Empty); -// var result = await JwtTokenValidation.ValidateAuthHeader(header, credentials, new SimpleChannelProvider(), string.Empty, "https://webchat.botframework.com/", client); -// -// Assert.True(result.IsAuthenticated); + public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", ""); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + null).join(); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException { + // token received and auth disabled + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + null).join(); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + null).join(); + Assert.fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains("authHeader")); + } + } + + @Test + public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider(APPID, ""); + ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + "https://webchat.botframework.com/").join(); + + Assert.assertTrue(identity.isAuthenticated()); + } + + @Test + public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", ""); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + null).join(); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { + JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( + "2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds + "2.30Vs3VQLKt974F", + GovernmentAuthenticationConstants.CHANNELSERVICE); + } + + @Test + public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithPrivateChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { + JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( + "2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds + "2.30Vs3VQLKt974F", + "TheChannel"); + } + + /** + * Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list. + */ + @Test + public void ChannelMsaHeaderValidServiceUrlShouldBeTrusted() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider(APPID, ""); + JwtTokenValidation.authenticateRequest( + new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"), + header, + credentials, + new SimpleChannelProvider()).join(); + + Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/")); + } + + /** + * Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list. + */ + @Test + public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException { + String header = getHeaderToken(); + CredentialProvider credentials = new SimpleCredentialProvider("7f74513e-6f96-4dbc-be9d-9a81fea22b88", ""); + + try { + JwtTokenValidation.authenticateRequest( + new Activity().withServiceUrl("https://webchat.botframework.com/"), + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.fail("Should have thrown AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); + } + } + + /** + * Tests with no authentication header and makes sure the service URL is not added to the trusted list. + */ + @Test + public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + ClaimsIdentity identity = JwtTokenValidation.authenticateRequest( + new Activity().withServiceUrl("https://webchat.botframework.com/"), + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.assertEquals("anonymous", identity.getIssuer()); + } + + @Test + public void ChannelNoHeaderAuthenticationEnabledShouldThrow() throws IOException, ExecutionException, InterruptedException { + try { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider(APPID, APPPASSWORD); + JwtTokenValidation.authenticateRequest( + new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"), + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.fail("Should have thrown AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + + Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/")); + } + + /** + * Tests with no authentication header and makes sure the service URL is not added to the trusted list. + */ + @Test + public void ChannelAuthenticationDisabledServiceUrlShouldNotBeTrusted() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + ClaimsIdentity identity = JwtTokenValidation.authenticateRequest( + new Activity().withServiceUrl("https://webchat.botframework.com/"), + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); + } + + @Test + public void EnterpriseChannelValidation_Succeeds() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + } catch (CompletionException e) { + Assert.fail("Should not have thrown " + e.getCause().getClass().getName()); + } + } + + @Test + public void EnterpriseChannelValidation_NoAuthentication_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(null, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoAudienceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, ""); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_WrongAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, "abc"); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, ""); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_WrongServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, "other"); + }}; + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_Succeeds() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + } catch (Exception e) { + Assert.fail("Should not have thrown " + e.getCause().getClass().getName() + ": " + e.getCause().getMessage()); + } + } + + @Test + public void GovernmentChannelValidation_NoAuthentication_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(null, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoAudienceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, ""); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, "abc"); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongAudienceClaimIssuer_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + }}; + ClaimsIdentity identity = new ClaimsIdentity("https://wrongissuer.com", claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, ""); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap() {{ + put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + put(AuthenticationConstants.SERVICE_URL_CLAIM, "other"); + }}; + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String appId, String pwd, String channelService) throws IOException, ExecutionException, InterruptedException { + ChannelProvider channel = new SimpleChannelProvider(channelService); + String header = channel.isGovernment() ? getGovHeaderToken() : getHeaderToken(); + + JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(header, appId, pwd, channel); + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String header, String appId, String pwd, ChannelProvider channel) { + CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd); + + try { + ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader( + header, + credentials, + channel, + "", + "https://webchat.botframework.com/").join(); + + Assert.assertTrue(identity.isAuthenticated()); + } catch (Exception e) { + Assert.fail("Should not have thrown " + e.getClass().getName()); + } + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Throws(String header, String appId, String pwd, String channelService) throws ExecutionException, InterruptedException { + CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd); + ChannelProvider channel = new SimpleChannelProvider(channelService); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + channel, + "", + "https://webchat.botframework.com/").join(); + Assert.fail("Should have thrown AuthenticationException"); + } catch (AuthenticationException e) { + Assert.assertTrue(true); + } } } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java new file mode 100644 index 000000000..831b964ac --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.concurrent.ExecutionException; + +public class MicrosoftAppCredentialsTests { + @Test + public void ValidUrlTrusted() { + MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com"); + Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com")); + } + + @Test + public void InvalidUrlTrusted() { + MicrosoftAppCredentials.trustServiceUrl("badurl"); + Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("badurl")); + } + + @Test + public void TrustedUrlExpiration() throws InterruptedException { + // There is a +5 minute window for an expired url + MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com", LocalDateTime.now().minusMinutes(6)); + Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com")); + + MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com", LocalDateTime.now().minusMinutes(4)); + Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com")); + } + + @Test + public void ValidateAuthEndpoint() { + try { + // In Java, about the only thing that can cause a MalformedURLException in a missing or unknown protocol. + // At any rate, this should validate someone didn't mess up the oAuth Endpoint for the class. + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F"); + new URL(credentials.oAuthEndpoint()); + + credentials.setChannelAuthTenant("tenant.com"); + + MicrosoftAppCredentials credentialsWithTenant = + new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F", "tenant.com"); + + } catch(MalformedURLException e) { + Assert.fail("Should not have thrown MalformedURLException"); + } + } + + @Test + public void GetToken() throws InterruptedException, ExecutionException { + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F"); + AuthenticationResult token = credentials.getToken().get(); + Assert.assertFalse(StringUtils.isEmpty(token.getAccessToken())); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java index 0c9d229c6..6c0069b6d 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java @@ -1,33 +1,21 @@ package com.microsoft.bot.connector; -import com.microsoft.aad.adal4j.ClientCredential; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; import com.microsoft.bot.connector.authentication.OAuthClient; -import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.TokenResponse; -import com.microsoft.rest.RestClient; -import org.apache.commons.lang3.StringUtils; +import com.microsoft.bot.connector.rest.RestConnectorClient; import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.io.IOException; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import static java.util.concurrent.CompletableFuture.completedFuture; - - public class OAuthConnectorTest extends OAuthTestBase { - private ConnectorClientImpl mockConnectorClient; + private RestConnectorClient mockConnectorClient; private MicrosoftAppCredentials credentials; public OAuthConnectorTest() throws IOException, ExecutionException, InterruptedException, URISyntaxException { diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java index 9f3bf684d..615d3d2f8 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java @@ -5,22 +5,17 @@ import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; import com.microsoft.bot.connector.authentication.OAuthClient; import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.models.ChannelAccount; import com.microsoft.rest.RestClient; -import okhttp3.Request; -import org.apache.commons.io.FileSystemUtils; import java.io.IOException; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; -import static java.util.concurrent.CompletableFuture.completedFuture; - public class OAuthTestBase extends TestBase { @@ -32,7 +27,7 @@ public class OAuthTestBase extends TestBase private String token; - protected ConnectorClientImpl connector; + protected RestConnectorClient connector; private ChannelAccount bot; public ChannelAccount getBot() { @@ -70,11 +65,11 @@ protected void initializeClients(RestClient restClient, String botId, String use } } - this.connector = new ConnectorClientImpl(restClient); + this.connector = new RestConnectorClient(restClient); if (this.clientId != null && this.clientSecret != null) { MicrosoftAppCredentials credentials = new MicrosoftAppCredentials(this.clientId, this.clientSecret); - this.token = credentials.getToken(new Request.Builder().build()); + this.token = credentials.getToken().get().getAccessToken(); } else { this.token = null; @@ -116,14 +111,14 @@ public CompletableFuture UseOAuthClientFor(Function{ OAuthClient oauthClient = null; try { - oauthClient = new OAuthClient(this.connector, AuthenticationConstants.OAuthUrl); + oauthClient = new OAuthClient(this.connector, AuthenticationConstants.OAUTH_URL); } catch (URISyntaxException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } doTest.apply(oauthClient); - }); + }, ExecutorFactory.getExecutor()); } } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java new file mode 100644 index 000000000..6ae6f4e72 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.SimpleChannelProvider; +import org.junit.Assert; +import org.junit.Test; + +public class SimpleChannelProviderTests { + @Test + public void PublicChannelProvider() { + SimpleChannelProvider channel = new SimpleChannelProvider(); + Assert.assertTrue(channel.isPublicAzure()); + Assert.assertFalse(channel.isGovernment()); + } + + @Test + public void GovernmentChannelProvider() { + SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE); + Assert.assertFalse(channel.isPublicAzure()); + Assert.assertTrue(channel.isGovernment()); + } + + @Test + public void GetChannelService() { + try { + SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE); + String service = channel.getChannelService().join(); + Assert.assertEquals(service, GovernmentAuthenticationConstants.CHANNELSERVICE); + } catch (Throwable t) { + Assert.fail("Should not have thrown " + t.getClass().getName()); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java new file mode 100644 index 000000000..9c1f6f559 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import org.junit.Assert; +import org.junit.Test; + +public class SimpleCredentialProviderTests { + @Test + public void ValidAppIdAsync() { + SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd"); + + Assert.assertTrue(credentialProvider.isValidAppIdAsync("appid").join()); + Assert.assertFalse(credentialProvider.isValidAppIdAsync("wrongappid").join()); + } + + @Test + public void AppPasswordAsync() { + SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd"); + + Assert.assertEquals(credentialProvider.getAppPasswordAsync("appid").join(), "pwd"); + Assert.assertNull(credentialProvider.getAppPasswordAsync("wrongappid").join()); + } + + @Test + public void AuthenticationDisabledAsync() { + Assert.assertFalse(new SimpleCredentialProvider("appid", "pwd").isAuthenticationDisabledAsync().join()); + Assert.assertTrue(new SimpleCredentialProvider(null, null).isAuthenticationDisabledAsync().join()); + } +} diff --git a/libraries/bot-integration-core/pom.xml b/libraries/bot-integration-core/pom.xml new file mode 100644 index 000000000..64233d8a4 --- /dev/null +++ b/libraries/bot-integration-core/pom.xml @@ -0,0 +1,207 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.0.0 + ../../pom.xml + + + bot-integration-core + jar + 4.0.0-SNAPSHOT + + ${project.groupId}:${project.artifactId} + Bot Framework Integration Core + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.slf4j + slf4j-api + + + com.microsoft.bot + bot-schema + 4.0.0-SNAPSHOT + + + com.microsoft.bot + bot-connector + 4.0.0-SNAPSHOT + + + org.slf4j + slf4j-api + + + + + + MyGet + ${repo.url} + + + + + + MyGet + ${repo.url} + + + + + + build + + true + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-pmd-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + org.eluder.coveralls + coveralls-maven-plugin + + yourcoverallsprojectrepositorytoken + + + + org.codehaus.mojo + cobertura-maven-plugin + + ../../cobertura-report/bot-connector + xml + 256m + + true + + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ClasspathPropertiesConfiguration.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ClasspathPropertiesConfiguration.java new file mode 100644 index 000000000..f3b3182a1 --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ClasspathPropertiesConfiguration.java @@ -0,0 +1,30 @@ +package com.microsoft.bot.integration; + +import java.io.IOException; +import java.util.Properties; +import org.slf4j.LoggerFactory; + +/** + * Provides access to properties defined in a Properties file located on the classpath. + */ +public class ClasspathPropertiesConfiguration implements Configuration { + private Properties properties; + + /** + * Loads properties from the 'application.properties' file. + * @throws IOException + */ + public ClasspathPropertiesConfiguration() { + try { + properties = new Properties(); + properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties")); + } + catch (IOException e) { + (LoggerFactory.getLogger(ClasspathPropertiesConfiguration.class)).error("Unable to load properties", e); + } + } + + public String getProperty(String key) { + return properties.getProperty(key); + } +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/Configuration.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/Configuration.java new file mode 100644 index 000000000..9a98516da --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/Configuration.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +/** + * Provides read-only access to configuration properties. + */ +public interface Configuration { + String getProperty(String key); +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationChannelProvider.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationChannelProvider.java new file mode 100644 index 000000000..08054285a --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationChannelProvider.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.connector.authentication.SimpleChannelProvider; + +/** + * Channel provider which uses Configuration to lookup the channel service property. + * + * This will populate the SimpleChannelProvider.ChannelService from a configuration entry with + * the key of "ChannelService". + */ +public class ConfigurationChannelProvider extends SimpleChannelProvider { + public ConfigurationChannelProvider(Configuration configuration) { + super(configuration.getProperty("ChannelService")); + } +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationCredentialProvider.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationCredentialProvider.java new file mode 100644 index 000000000..7750309bf --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationCredentialProvider.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; + +/** + * Credential provider which uses Configuration to lookup appId and password. + */ +public class ConfigurationCredentialProvider extends SimpleCredentialProvider { + public ConfigurationCredentialProvider(Configuration configuration) { + setAppId(configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPID)); + setPassword(configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPPASSWORD)); + } +} diff --git a/pom.xml b/pom.xml index 43ec2dfd9..b838a9558 100644 --- a/pom.xml +++ b/pom.xml @@ -66,27 +66,27 @@ com.fasterxml.jackson.module jackson-module-parameter-names - 2.9.8 + 2.9.9 com.fasterxml.jackson.datatype jackson-datatype-jdk8 - 2.9.8 + 2.9.9 com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.9.8 + 2.9.9 com.auth0 java-jwt - 3.3.0 + 3.8.2 com.auth0 jwks-rsa - 0.3.0 + 0.8.3 org.slf4j @@ -101,7 +101,12 @@ org.apache.commons commons-lang3 - 3.4 + 3.9 + + + commons-io + commons-io + 2.6 @@ -130,6 +135,7 @@ libraries/bot-schema libraries/bot-builder libraries/bot-connector + libraries/bot-integration-core samples/bot-connector-sample samples/servlet-echo samples/spring-echo @@ -183,7 +189,7 @@ maven-checkstyle-plugin ${checkstyle.version} - + ./etc/bot-checkstyle.xml UTF-8 false false diff --git a/samples/bot-connector-sample/src/main/java/com/microsoft/bot/connector/sample/App.java b/samples/bot-connector-sample/src/main/java/com/microsoft/bot/connector/sample/App.java index 9fa813525..9e626450a 100644 --- a/samples/bot-connector-sample/src/main/java/com/microsoft/bot/connector/sample/App.java +++ b/samples/bot-connector-sample/src/main/java/com/microsoft/bot/connector/sample/App.java @@ -6,11 +6,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.CredentialProvider; -import com.microsoft.bot.connector.authentication.CredentialProviderImpl; -import com.microsoft.bot.connector.authentication.JwtTokenValidation; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.*; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.models.Activity; import com.microsoft.bot.schema.models.ActivityTypes; import com.microsoft.bot.schema.models.ResourceResponse; @@ -30,7 +28,7 @@ public class App { private static String appPassword = ""; // <-- app password --> public static void main( String[] args ) throws IOException { - CredentialProvider credentialProvider = new CredentialProviderImpl(appId, appPassword); + CredentialProvider credentialProvider = new SimpleCredentialProvider(appId, appPassword); HttpServer server = HttpServer.create(new InetSocketAddress(3978), 0); server.createContext("/api/messages", new MessageHandle(credentialProvider)); server.setExecutor(null); @@ -55,7 +53,7 @@ public void handle(HttpExchange httpExchange) throws IOException { Activity activity = getActivity(httpExchange); String authHeader = httpExchange.getRequestHeaders().getFirst("Authorization"); try { - JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider); + JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider, new SimpleChannelProvider()); // send ack to user activity httpExchange.sendResponseHeaders(202, 0); @@ -63,7 +61,7 @@ public void handle(HttpExchange httpExchange) throws IOException { if (activity.type().equals(ActivityTypes.MESSAGE)) { // reply activity with the same text - ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), this.credentials); + ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), this.credentials); ResourceResponse response = connector.conversations().sendToConversation(activity.conversation().id(), new Activity() .withType(ActivityTypes.MESSAGE) diff --git a/samples/servlet-echo/src/main/java/com/microsoft/bot/sample/servlet/EchoServlet.java b/samples/servlet-echo/src/main/java/com/microsoft/bot/sample/servlet/EchoServlet.java index e8547fb03..ea6820d9e 100644 --- a/samples/servlet-echo/src/main/java/com/microsoft/bot/sample/servlet/EchoServlet.java +++ b/samples/servlet-echo/src/main/java/com/microsoft/bot/sample/servlet/EchoServlet.java @@ -8,12 +8,10 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.ClaimsIdentity; -import com.microsoft.bot.connector.authentication.CredentialProvider; -import com.microsoft.bot.connector.authentication.CredentialProviderImpl; -import com.microsoft.bot.connector.authentication.JwtTokenValidation; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.connector.authentication.*; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.models.Activity; import com.microsoft.bot.schema.models.ActivityTypes; import javax.servlet.*; @@ -24,6 +22,7 @@ import java.io.InputStream; import java.util.concurrent.CompletableFuture; import java.util.Properties; +import java.util.concurrent.CompletionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,7 +52,7 @@ public void init() throws ServletException { String appId = p.getProperty("MicrosoftAppId"); String appPassword = p.getProperty("MicrosoftAppPassword"); - this.credentialProvider = new CredentialProviderImpl(appId, appPassword); + this.credentialProvider = new SimpleCredentialProvider(appId, appPassword); this.credentials = new MicrosoftAppCredentials(appId, appPassword); } catch(IOException ioe){ @@ -67,11 +66,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) final Activity activity = getActivity(request); String authHeader = request.getHeader("Authorization"); - CompletableFuture authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider); + CompletableFuture authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider, new SimpleChannelProvider()); authenticateRequest.thenRunAsync(() -> { if (activity.type().equals(ActivityTypes.MESSAGE)) { // reply activity with the same text - ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), this.credentials); + ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), this.credentials); connector.conversations().sendToConversation(activity.conversation().id(), new Activity() .withType(ActivityTypes.MESSAGE) @@ -80,13 +79,21 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) .withFrom(activity.recipient()) ); } - }); - } catch (AuthenticationException ex) { - response.setStatus(401); - LOGGER.log(Level.WARNING, "Auth failed!", ex); + }, ExecutorFactory.getExecutor()).join(); + + response.setStatus(200); + } catch (CompletionException ex) { + if (ex.getCause() instanceof AuthenticationException) { + LOGGER.log(Level.WARNING, "Auth failed!", ex); + response.setStatus(401); + } + else { + LOGGER.log(Level.WARNING, "Execution failed", ex); + response.setStatus(500); + } } catch (Exception ex) { - response.setStatus(500); LOGGER.log(Level.WARNING, "Execution failed", ex); + response.setStatus(500); } } diff --git a/samples/spring-echo/src/main/java/com/microsoft/bot/sample/spring/BotController.java b/samples/spring-echo/src/main/java/com/microsoft/bot/sample/spring/BotController.java index bbabda0de..0ea2bc04f 100644 --- a/samples/spring-echo/src/main/java/com/microsoft/bot/sample/spring/BotController.java +++ b/samples/spring-echo/src/main/java/com/microsoft/bot/sample/spring/BotController.java @@ -2,6 +2,9 @@ // Licensed under the MIT License. package com.microsoft.bot.sample.spring; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.connector.authentication.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -15,16 +18,11 @@ import javax.annotation.PostConstruct; import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.ClaimsIdentity; -import com.microsoft.bot.connector.authentication.CredentialProvider; -import com.microsoft.bot.connector.authentication.CredentialProviderImpl; -import com.microsoft.bot.connector.authentication.JwtTokenValidation; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +import com.microsoft.bot.connector.rest.RestConnectorClient; import com.microsoft.bot.schema.models.Activity; import com.microsoft.bot.schema.models.ActivityTypes; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; /** * This is the controller that will receive incoming Channel Activity messages. @@ -51,7 +49,7 @@ public class BotController { */ @PostConstruct public void init() { - _credentialProvider = new CredentialProviderImpl(appId, appPassword); + _credentialProvider = new SimpleCredentialProvider(appId, appPassword); _credentials = new MicrosoftAppCredentials(appId, appPassword); } @@ -66,20 +64,25 @@ public void init() { public ResponseEntity incoming(@RequestBody Activity activity, @RequestHeader(value = "Authorization", defaultValue = "") String authHeader) { try { - CompletableFuture authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, _credentialProvider); - authenticateRequest.thenRunAsync(() -> { - if (activity.type().equals(ActivityTypes.MESSAGE)) { - logger.info("Received: " + activity.text()); + JwtTokenValidation.authenticateRequest(activity, authHeader, _credentialProvider, new SimpleChannelProvider()) + .thenRunAsync(() -> { + if (activity.type().equals(ActivityTypes.MESSAGE)) { + logger.info("Received: " + activity.text()); - // reply activity with the same text - ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), _credentials); - connector.conversations().sendToConversation(activity.conversation().id(), - new Activity().withType(ActivityTypes.MESSAGE).withText("Echo: " + activity.text()) - .withRecipient(activity.from()).withFrom(activity.recipient())); - } - }); - } catch (AuthenticationException ex) { - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // reply activity with the same text + ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), _credentials); + connector.conversations().sendToConversation(activity.conversation().id(), + new Activity().withType(ActivityTypes.MESSAGE).withText("Echo: " + activity.text()) + .withRecipient(activity.from()).withFrom(activity.recipient())); + } + }, ExecutorFactory.getExecutor()).join(); + } catch (CompletionException ex) { + if (ex.getCause() instanceof AuthenticationException) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + else { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } } catch (Exception ex) { return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/samples/spring-echo/src/main/resources/application.properties b/samples/spring-echo/src/main/resources/application.properties index 653d80609..a695b3bf0 100644 --- a/samples/spring-echo/src/main/resources/application.properties +++ b/samples/spring-echo/src/main/resources/application.properties @@ -1,2 +1,2 @@ -MicrosoftAppId=9c286e2f-e070-4af5-a3f1-350d666214ed -MicrosoftAppPassword=botframework_secret_goes_here +MicrosoftAppId= +MicrosoftAppPassword=