diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/cache/InMemoryCache.java b/src/main/java/com/salesforce/einsteinbot/sdk/cache/InMemoryCache.java index 731436e..64f0323 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/cache/InMemoryCache.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/cache/InMemoryCache.java @@ -17,7 +17,7 @@ */ public class InMemoryCache implements Cache { - private final com.google.common.cache.Cache cache; + private final com.google.common.cache.Cache cache; public InMemoryCache(long ttlSeconds) { cache = CacheBuilder.newBuilder().expireAfterAccess(ttlSeconds, TimeUnit.SECONDS).build(); @@ -25,7 +25,12 @@ public InMemoryCache(long ttlSeconds) { @Override public Optional get(String key) { - String val = cache.getIfPresent(key); + String val = (String) cache.getIfPresent(key); + return Optional.ofNullable(val); + } + + public Optional getObject(String key) { + Object val = cache.getIfPresent(key); return Optional.ofNullable(val); } @@ -34,12 +39,20 @@ public void set(String key, String val) { cache.put(key, val); } + public void setObject(String key, Object val) { + cache.put(key, val); + } + /** * This method does not respect the ttlSeconds parameter. */ @Override public void set(String key, String val, long ttlSeconds) { - cache.put(key, val); + set(key, val); + } + + public void setObject(String key, Object val, long ttlSeconds) { + setObject(key, val); } @Override diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/cache/RedisCache.java b/src/main/java/com/salesforce/einsteinbot/sdk/cache/RedisCache.java index 5e7eb4a..a413c9c 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/cache/RedisCache.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/cache/RedisCache.java @@ -25,7 +25,7 @@ public class RedisCache implements Cache { private static final Long DEFAULT_TTL_SECONDS = 259140L; // 2 days, 23 hours, 59 minutes private JedisPool jedisPool; - private long ttlSeconds; + private final long ttlSeconds; /** * This constructor will use the default ttl of 259,140 seconds and will assume standard Redis diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClient.java b/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClient.java index d0fbfba..fc005f5 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClient.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClient.java @@ -30,9 +30,9 @@ BotResponse startChatSession(RequestConfig config, ExternalSessionId sessionId, BotSendMessageRequest requestEnvelope); - Status getHealthStatus(); + Status getHealthStatus(RequestConfig config); - SupportedVersions getSupportedVersions(); + SupportedVersions getSupportedVersions(RequestConfig config); /** * BasicClientFluentBuilder provides Fluent API to create Basic Chatbot Client. diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientImpl.java b/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientImpl.java index ee9a323..57e976c 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientImpl.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientImpl.java @@ -10,16 +10,15 @@ import static com.salesforce.einsteinbot.sdk.client.model.BotResponseBuilder.fromChatMessageResponseEnvelopeResponseEntity; import static com.salesforce.einsteinbot.sdk.client.util.RequestFactory.buildChatMessageEnvelope; import static com.salesforce.einsteinbot.sdk.client.util.RequestFactory.buildInitMessageEnvelope; -import static com.salesforce.einsteinbot.sdk.util.WebClientUtil.createErrorResponseProcessor; -import static com.salesforce.einsteinbot.sdk.util.WebClientUtil.createFilter; -import static com.salesforce.einsteinbot.sdk.util.WebClientUtil.createLoggingRequestProcessor; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.salesforce.einsteinbot.sdk.api.BotApi; import com.salesforce.einsteinbot.sdk.api.HealthApi; import com.salesforce.einsteinbot.sdk.api.VersionsApi; import com.salesforce.einsteinbot.sdk.auth.AuthMechanism; +import com.salesforce.einsteinbot.sdk.cache.InMemoryCache; import com.salesforce.einsteinbot.sdk.client.model.BotEndSessionRequest; import com.salesforce.einsteinbot.sdk.client.model.BotRequest; import com.salesforce.einsteinbot.sdk.client.model.BotResponse; @@ -28,9 +27,9 @@ import com.salesforce.einsteinbot.sdk.client.model.ExternalSessionId; import com.salesforce.einsteinbot.sdk.client.model.RequestConfig; import com.salesforce.einsteinbot.sdk.client.model.RuntimeSessionId; -import com.salesforce.einsteinbot.sdk.exception.ChatbotResponseException; +import com.salesforce.einsteinbot.sdk.client.util.ClientFactory; +import com.salesforce.einsteinbot.sdk.client.util.ClientFactory.ClientWrapper; import com.salesforce.einsteinbot.sdk.exception.UnsupportedSDKException; -import com.salesforce.einsteinbot.sdk.handler.ApiClient; import com.salesforce.einsteinbot.sdk.model.ChatMessageEnvelope; import com.salesforce.einsteinbot.sdk.model.EndSessionReason; import com.salesforce.einsteinbot.sdk.model.InitMessageEnvelope; @@ -38,25 +37,21 @@ import com.salesforce.einsteinbot.sdk.model.SupportedVersions; import com.salesforce.einsteinbot.sdk.model.SupportedVersionsVersions; import com.salesforce.einsteinbot.sdk.model.SupportedVersionsVersions.StatusEnum; -import com.salesforce.einsteinbot.sdk.util.LoggingJsonEncoder; -import com.salesforce.einsteinbot.sdk.util.ReleaseInfo; -import com.salesforce.einsteinbot.sdk.util.UtilFunctions; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import com.salesforce.einsteinbot.sdk.util.WebClientUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.client.utils.URIUtils; import org.springframework.http.MediaType; -import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; /** * This is a basic implementation of {@link BasicChatbotClient}. It does not perform session @@ -67,41 +62,45 @@ */ public class BasicChatbotClientImpl implements BasicChatbotClient { - protected BotApi botApi; - protected HealthApi healthApi; - protected VersionsApi versionsApi; - protected ApiClient apiClient; + private static final Long DEFAULT_TTL_SECONDS = Duration.ofDays(3).getSeconds(); + private static final ObjectMapper MAPPER = new ObjectMapper(); + static final String API_INFO_URI = "/services/data/v58.0/connect/bots/api-info"; + + protected InMemoryCache cache; + protected String basePath; + protected WebClient.Builder webClientBuilder; protected AuthMechanism authMechanism; - protected ReleaseInfo releaseInfo = ReleaseInfo.getInstance(); + protected ClientWrapper clientWrapper; protected BasicChatbotClientImpl(String basePath, AuthMechanism authMechanism, WebClient.Builder webClientBuilder) { this.authMechanism = authMechanism; - this.apiClient = new ApiClient(createWebClient(webClientBuilder), UtilFunctions.getMapper(), - UtilFunctions - .createDefaultDateFormat()); - apiClient.setBasePath(basePath); - apiClient.setUserAgent(releaseInfo.getAsUserAgent()); - botApi = new BotApi(apiClient); - healthApi = new HealthApi(apiClient); - versionsApi = new VersionsApi(apiClient); + this.basePath = basePath; + this.webClientBuilder = webClientBuilder; + this.clientWrapper = ClientFactory.createClient(basePath, webClientBuilder); + this.cache = new InMemoryCache(DEFAULT_TTL_SECONDS); } @VisibleForTesting void setBotApi(BotApi botApi) { - this.botApi = botApi; + this.clientWrapper.setBotApi(botApi); } @VisibleForTesting void setHealthApi(HealthApi healthApi) { - this.healthApi = healthApi; + this.clientWrapper.setHealthApi(healthApi); } @VisibleForTesting void setVersionsApi(VersionsApi versionsApi) { - this.versionsApi = versionsApi; + this.clientWrapper.setVersionsApi(versionsApi); + } + + @VisibleForTesting + void setCache(InMemoryCache cache) { + this.cache = cache; } @Override @@ -109,18 +108,26 @@ public BotResponse startChatSession(RequestConfig config, ExternalSessionId sessionId, BotSendMessageRequest botSendMessageRequest) { - if (!isApiVersionSupported()) { - throw new UnsupportedSDKException(getCurrentApiVersion(), getLatestApiVersion()); + if (!isApiVersionSupported(config)) { + throw new UnsupportedSDKException(getCurrentApiVersion(), getLatestApiVersion(config)); } + + ClientWrapper clientWrapper = getOrCreateClientWrapper(config); + this.clientWrapper = clientWrapper; + InitMessageEnvelope initMessageEnvelope = createInitMessageEnvelope(config, sessionId, botSendMessageRequest); notifyRequestEnvelopeInterceptor(botSendMessageRequest, initMessageEnvelope); CompletableFuture futureResponse = invokeEstablishChatSession(config, initMessageEnvelope, - botSendMessageRequest); + botSendMessageRequest, + clientWrapper); try { - return futureResponse.get(); + BotResponse botResponse = futureResponse.get(); + this.cache.set(botResponse.getResponseEnvelope().getSessionId(), basePath); + this.cache.setObject(basePath, clientWrapper); + return botResponse; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -141,11 +148,13 @@ public BotResponse sendMessage(RequestConfig config, ChatMessageEnvelope chatMessageEnvelope = createChatMessageEnvelope(botSendMessageRequest); + ClientWrapper clientWrapper = getCachedClientWrapper(sessionId); notifyRequestEnvelopeInterceptor(botSendMessageRequest, chatMessageEnvelope); CompletableFuture futureResponse = invokeContinueChatSession(config.getOrgId(), sessionId.getValue(), chatMessageEnvelope, - botSendMessageRequest); + botSendMessageRequest, + clientWrapper); try { return futureResponse.get(); @@ -166,11 +175,14 @@ public BotResponse endChatSession(RequestConfig config, BotEndSessionRequest botEndSessionRequest) { EndSessionReason endSessionReason = botEndSessionRequest.getEndSessionReason(); + + ClientWrapper clientWrapper = getCachedClientWrapper(sessionId); notifyRequestEnvelopeInterceptor(botEndSessionRequest, "EndSessionReason: " + endSessionReason); CompletableFuture futureResponse = invokeEndChatSession(config.getOrgId(), sessionId.getValue(), endSessionReason, - botEndSessionRequest); + botEndSessionRequest, + clientWrapper); try { return futureResponse.get(); } catch (InterruptedException | ExecutionException e) { @@ -183,11 +195,23 @@ protected void notifyRequestEnvelopeInterceptor(BotRequest botRequest, Object re .accept(requestEnvelope); } + private ClientWrapper getCachedClientWrapper(RuntimeSessionId sessionId) { + Optional basePath = this.cache.get(sessionId.getValue()); + if (!basePath.isPresent()) { + throw new RuntimeException("No base path found in cache for session ID: " + sessionId.getValue()); + } + Optional clientOptional = this.cache.getObject(basePath.get()); + if (!clientOptional.isPresent()) { + throw new RuntimeException("No client implementation found in cache for base path: " + basePath.get()); + } + return (ClientWrapper) clientOptional.get(); + } + protected CompletableFuture invokeEndChatSession(String orgId, String sessionId, - EndSessionReason endSessionReason, BotEndSessionRequest botRequest) { + EndSessionReason endSessionReason, BotEndSessionRequest botRequest, ClientWrapper clientWrapper) { - apiClient.setBearerToken(authMechanism.getToken()); - CompletableFuture futureResponse = botApi + clientWrapper.getApiClient().setBearerToken(authMechanism.getToken()); + CompletableFuture futureResponse = clientWrapper.getBotApi() .endSessionWithHttpInfo(sessionId, orgId, endSessionReason, @@ -202,10 +226,11 @@ protected CompletableFuture invokeEndChatSession(String orgId, Stri protected CompletableFuture invokeEstablishChatSession(RequestConfig config, InitMessageEnvelope initMessageEnvelope, - BotSendMessageRequest botRequest) { + BotSendMessageRequest botRequest, + ClientWrapper clientWrapper) { - apiClient.setBearerToken(authMechanism.getToken()); - CompletableFuture futureResponse = botApi + clientWrapper.getApiClient().setBearerToken(authMechanism.getToken()); + CompletableFuture futureResponse = clientWrapper.getBotApi() .startSessionWithHttpInfo(config.getBotId(), config.getOrgId(), initMessageEnvelope, botRequest.getRequestId().orElse(null)) .toFuture() @@ -216,10 +241,11 @@ protected CompletableFuture invokeEstablishChatSession(RequestConfi protected CompletableFuture invokeContinueChatSession(String orgId, String sessionId, ChatMessageEnvelope messageEnvelope, - BotSendMessageRequest botRequest) { + BotSendMessageRequest botRequest, + ClientWrapper clientWrapper) { - apiClient.setBearerToken(authMechanism.getToken()); - CompletableFuture futureResponse = botApi + clientWrapper.getApiClient().setBearerToken(authMechanism.getToken()); + CompletableFuture futureResponse = clientWrapper.getBotApi() .continueSessionWithHttpInfo(sessionId, orgId, messageEnvelope, @@ -232,8 +258,9 @@ protected CompletableFuture invokeContinueChatSession(String orgId, return futureResponse; } - public Status getHealthStatus() { - CompletableFuture statusFuture = healthApi.checkHealthStatus().toFuture(); + public Status getHealthStatus(RequestConfig requestConfig) { + ClientWrapper clientWrapper = getOrCreateClientWrapper(requestConfig); + CompletableFuture statusFuture = clientWrapper.getHealthApi().checkHealthStatus().toFuture(); try { return statusFuture.get(); @@ -242,8 +269,9 @@ public Status getHealthStatus() { } } - public SupportedVersions getSupportedVersions() { - CompletableFuture versionsFuture = versionsApi.getAPIVersions().toFuture(); + public SupportedVersions getSupportedVersions(RequestConfig requestConfig) { + ClientWrapper clientWrapper = getOrCreateClientWrapper(requestConfig); + CompletableFuture versionsFuture = clientWrapper.getVersionsApi().getAPIVersions().toFuture(); try { SupportedVersions versions = versionsFuture.get(); @@ -256,30 +284,38 @@ public SupportedVersions getSupportedVersions() { } } - private WebClient createWebClient(WebClient.Builder webClientBuilder) { - - return webClientBuilder - .codecs(createCodecsConfiguration(UtilFunctions.getMapper())) - .filter(createFilter(clientRequest -> createLoggingRequestProcessor(clientRequest), - clientResponse -> createErrorResponseProcessor(clientResponse, this::mapErrorResponse))) - .build(); - } - - private Consumer createCodecsConfiguration(ObjectMapper mapper) { - return clientDefaultCodecsConfigurer -> { - clientDefaultCodecsConfigurer.defaultCodecs() - .jackson2JsonEncoder(new LoggingJsonEncoder(mapper, MediaType.APPLICATION_JSON, false)); - clientDefaultCodecsConfigurer.defaultCodecs() - .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON)); - }; + private ClientWrapper getOrCreateClientWrapper(RequestConfig requestConfig) { + String basePath = getRuntimeUrl(requestConfig.getForceConfigEndpoint()); + Optional clientOptional = this.cache.getObject(basePath); + ClientWrapper clientWrapper = ClientFactory.createClient(basePath, webClientBuilder); + if (clientOptional.isPresent()) { + clientWrapper = (ClientWrapper) clientOptional.get(); + } + return clientWrapper; } - private Mono mapErrorResponse(ClientResponse clientResponse) { - return clientResponse - .body(WebClientUtil.errorBodyExtractor()) - .flatMap(errorDetails -> Mono - .error(new ChatbotResponseException(clientResponse.statusCode(), errorDetails, - clientResponse.headers()))); + private String getRuntimeUrl(String forceEndpoint) { + try { + URI uri = URI.create(forceEndpoint); + HttpHost forceHost = URIUtils.extractHost(uri); + String infoPath = uri.getRawPath().replace("/$", "") + API_INFO_URI; + WebClient webClient = WebClient.builder() + .baseUrl(forceHost.toString()) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + JsonNode node = webClient.get() + .uri(infoPath) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + this.authMechanism.getToken()) + .retrieve() + .bodyToMono(JsonNode.class) + .block(); + if (node == null) { + throw new RuntimeException("Could not get runtime URL"); + } + return node.get("runtimeBaseUrl").asText(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } } private String getCurrentApiVersion() { @@ -298,8 +334,8 @@ private String getCurrentApiVersion() { return properties.getProperty("api-spec-version").replace("_", "."); } - private String getLatestApiVersion() { - SupportedVersions versions = getSupportedVersions(); + private String getLatestApiVersion(RequestConfig requestConfig) { + SupportedVersions versions = getSupportedVersions(requestConfig); Optional supportedVersions = versions.getVersions() .stream() .filter(v -> Objects.equals(v.getStatus(), StatusEnum.ACTIVE)) @@ -307,9 +343,9 @@ private String getLatestApiVersion() { return supportedVersions.isPresent() ? supportedVersions.get().getVersionNumber() : getCurrentApiVersion(); } - private boolean isApiVersionSupported() { + private boolean isApiVersionSupported(RequestConfig requestConfig) { String currentApiVersion = getCurrentApiVersion(); - SupportedVersions versions = getSupportedVersions(); + SupportedVersions versions = getSupportedVersions(requestConfig); Optional supportedVersions = versions.getVersions() .stream() .filter(v -> Objects.equals(v.getVersionNumber(), currentApiVersion)) diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/client/ChatbotClient.java b/src/main/java/com/salesforce/einsteinbot/sdk/client/ChatbotClient.java index 11409eb..5d2f864 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/client/ChatbotClient.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/client/ChatbotClient.java @@ -29,7 +29,7 @@ BotResponse endChatSession(RequestConfig config, T sessionId, BotEndSessionRequest requestEnvelope); - Status getHealthStatus(); + Status getHealthStatus(RequestConfig config); - SupportedVersions getSupportedVersions(); + SupportedVersions getSupportedVersions(RequestConfig config); } diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/client/SessionManagedChatbotClientImpl.java b/src/main/java/com/salesforce/einsteinbot/sdk/client/SessionManagedChatbotClientImpl.java index e50e938..0d82ed2 100644 --- a/src/main/java/com/salesforce/einsteinbot/sdk/client/SessionManagedChatbotClientImpl.java +++ b/src/main/java/com/salesforce/einsteinbot/sdk/client/SessionManagedChatbotClientImpl.java @@ -142,13 +142,13 @@ private void removeFromCache(String cacheKey) { } @Override - public Status getHealthStatus() { - return basicClient.getHealthStatus(); + public Status getHealthStatus(RequestConfig config) { + return basicClient.getHealthStatus(config); } @Override - public SupportedVersions getSupportedVersions() { - return basicClient.getSupportedVersions(); + public SupportedVersions getSupportedVersions(RequestConfig config) { + return basicClient.getSupportedVersions(config); } private void addSequenceIds(BotSendMessageRequest requestEnvelope) { diff --git a/src/main/java/com/salesforce/einsteinbot/sdk/client/util/ClientFactory.java b/src/main/java/com/salesforce/einsteinbot/sdk/client/util/ClientFactory.java new file mode 100644 index 0000000..889bf81 --- /dev/null +++ b/src/main/java/com/salesforce/einsteinbot/sdk/client/util/ClientFactory.java @@ -0,0 +1,105 @@ +package com.salesforce.einsteinbot.sdk.client.util; + +import static com.salesforce.einsteinbot.sdk.util.WebClientUtil.createErrorResponseProcessor; +import static com.salesforce.einsteinbot.sdk.util.WebClientUtil.createFilter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.salesforce.einsteinbot.sdk.api.BotApi; +import com.salesforce.einsteinbot.sdk.api.HealthApi; +import com.salesforce.einsteinbot.sdk.api.VersionsApi; +import com.salesforce.einsteinbot.sdk.exception.ChatbotResponseException; +import com.salesforce.einsteinbot.sdk.handler.ApiClient; +import com.salesforce.einsteinbot.sdk.util.LoggingJsonEncoder; +import com.salesforce.einsteinbot.sdk.util.ReleaseInfo; +import com.salesforce.einsteinbot.sdk.util.UtilFunctions; +import com.salesforce.einsteinbot.sdk.util.WebClientUtil; +import java.util.function.Consumer; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public class ClientFactory { + + public static class ClientWrapper { + + protected BotApi botApi; + protected HealthApi healthApi; + protected VersionsApi versionsApi; + protected ApiClient apiClient; + protected ReleaseInfo releaseInfo = ReleaseInfo.getInstance(); + + protected ClientWrapper(String basePath, WebClient.Builder webClientBuilder) { + this.apiClient = new ApiClient(createWebClient(webClientBuilder), UtilFunctions.getMapper(), + UtilFunctions + .createDefaultDateFormat()); + apiClient.setBasePath(basePath); + apiClient.setUserAgent(releaseInfo.getAsUserAgent()); + botApi = new BotApi(apiClient); + healthApi = new HealthApi(apiClient); + versionsApi = new VersionsApi(apiClient); + } + + public ApiClient getApiClient() { + return this.apiClient; + } + + public BotApi getBotApi() { + return this.botApi; + } + + public VersionsApi getVersionsApi() { + return this.versionsApi; + } + + public HealthApi getHealthApi() { + return this.healthApi; + } + + public void setBotApi(BotApi botApi) { + this.botApi = botApi; + } + + public void setHealthApi(HealthApi healthApi) { + this.healthApi = healthApi; + } + + public void setVersionsApi(VersionsApi versionsApi) { + this.versionsApi = versionsApi; + } + + private WebClient createWebClient(WebClient.Builder webClientBuilder) { + + return webClientBuilder + .codecs(createCodecsConfiguration(UtilFunctions.getMapper())) + .filter(createFilter(WebClientUtil::createLoggingRequestProcessor, + clientResponse -> createErrorResponseProcessor(clientResponse, + this::mapErrorResponse))) + .build(); + } + + private Consumer createCodecsConfiguration(ObjectMapper mapper) { + return clientDefaultCodecsConfigurer -> { + clientDefaultCodecsConfigurer.defaultCodecs() + .jackson2JsonEncoder(new LoggingJsonEncoder(mapper, MediaType.APPLICATION_JSON, false)); + clientDefaultCodecsConfigurer.defaultCodecs() + .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON)); + }; + } + + private Mono mapErrorResponse(ClientResponse clientResponse) { + return clientResponse + .body(WebClientUtil.errorBodyExtractor()) + .flatMap(errorDetails -> Mono + .error(new ChatbotResponseException(clientResponse.statusCode(), errorDetails, + clientResponse.headers()))); + } + } + + public static ClientWrapper createClient(String basePath, WebClient.Builder webClientBuilder) { + return new ClientWrapper(basePath, webClientBuilder); + } + +} diff --git a/src/test/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientTest.java b/src/test/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientTest.java index d19f31e..1a59fda 100644 --- a/src/test/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientTest.java +++ b/src/test/java/com/salesforce/einsteinbot/sdk/client/BasicChatbotClientTest.java @@ -7,11 +7,16 @@ package com.salesforce.einsteinbot.sdk.client; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static com.salesforce.einsteinbot.sdk.client.BasicChatbotClientImpl.API_INFO_URI; import static com.salesforce.einsteinbot.sdk.client.model.BotResponseBuilder.fromResponseEnvelopeResponseEntity; import static com.salesforce.einsteinbot.sdk.client.util.RequestFactory.buildBotSendMessageRequest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; @@ -21,6 +26,7 @@ import com.salesforce.einsteinbot.sdk.api.HealthApi; import com.salesforce.einsteinbot.sdk.api.VersionsApi; import com.salesforce.einsteinbot.sdk.auth.AuthMechanism; +import com.salesforce.einsteinbot.sdk.cache.InMemoryCache; import com.salesforce.einsteinbot.sdk.client.model.BotEndSessionRequest; import com.salesforce.einsteinbot.sdk.client.model.BotHttpHeaders; import com.salesforce.einsteinbot.sdk.client.model.BotRequest; @@ -29,8 +35,10 @@ import com.salesforce.einsteinbot.sdk.client.model.ExternalSessionId; import com.salesforce.einsteinbot.sdk.client.model.RequestConfig; import com.salesforce.einsteinbot.sdk.client.model.RuntimeSessionId; +import com.salesforce.einsteinbot.sdk.client.util.ClientFactory.ClientWrapper; import com.salesforce.einsteinbot.sdk.client.util.RequestEnvelopeInterceptor; import com.salesforce.einsteinbot.sdk.exception.UnsupportedSDKException; +import com.salesforce.einsteinbot.sdk.handler.ApiClient; import com.salesforce.einsteinbot.sdk.model.AnyRequestMessage; import com.salesforce.einsteinbot.sdk.model.AnyResponseMessage; import com.salesforce.einsteinbot.sdk.model.AnyVariable; @@ -49,6 +57,7 @@ import com.salesforce.einsteinbot.sdk.model.TextMessage; import com.salesforce.einsteinbot.sdk.model.TextMessage.TypeEnum; import com.salesforce.einsteinbot.sdk.util.TestUtils; +import de.mkammerer.wiremock.WireMockExtension; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -56,6 +65,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -72,10 +82,14 @@ @ExtendWith(MockitoExtension.class) public class BasicChatbotClientTest { + @RegisterExtension + WireMockExtension forceMock = new WireMockExtension(options() + .dynamicPort() + .dynamicHttpsPort()); + private final String authToken = "C2C TOKEN"; private final String orgId = "00DSB0000001ThY2AU"; private final String botId = "testBotId"; - private final String forceConfigEndpoint = "testForceConfigEndpoint"; private final String sessionId = "testSessionId"; private final String externalSessionId = "testExternalSessionIdId"; private final String basePath = "http://runtime-api-na-west.stg.chatbots.sfdc.sh"; @@ -83,12 +97,8 @@ public class BasicChatbotClientTest { private final String requestId = "TestRequestId"; private final String runtimeCRC = null; - private final RequestConfig config = RequestConfig - .with() - .botId(botId) - .orgId(orgId) - .forceConfigEndpoint(forceConfigEndpoint) - .build(); + private String forceConfigEndpoint; + private RequestConfig config; private final long sequenceId = System.currentTimeMillis(); private final String messageText = "hello"; @@ -112,6 +122,12 @@ public class BasicChatbotClientTest { @Mock private BotApi mockBotApi; + @Mock + private InMemoryCache cache; + + @Mock + private ClientWrapper mockClientWrapper; + @Mock private HealthApi mockHealthApi; @@ -130,6 +146,9 @@ public class BasicChatbotClientTest { @Mock private Status healthStatus; + @Mock + private ApiClient mockApiClient; + @Mock private SupportedVersions supportedVersions; @@ -151,7 +170,25 @@ public void setup() { .authMechanism(mockAuthMechanism) .build(); - ((BasicChatbotClientImpl) client).setBotApi(mockBotApi); + forceMock.stubFor( + get(API_INFO_URI) + .willReturn( + aResponse() + .withStatus(HttpStatus.OK.value()) + .withHeader("Content-Type", "application/json;charset=UTF-8") + .withBody("{\"runtimeBaseUrl\":\"" + basePath + "\"}") + ) + ); + + forceConfigEndpoint = forceMock.getBaseUri().toString(); + config = RequestConfig + .with() + .botId(botId) + .orgId(orgId) + .forceConfigEndpoint(forceConfigEndpoint) + .build(); + + ((BasicChatbotClientImpl) this.client).setCache(cache); } @Test @@ -162,7 +199,10 @@ public void testStartSession() { BotResponse startSessionBotResponse = fromResponseEnvelopeResponseEntity(responseEntity); InitMessageEnvelope initMessageEnvelope = buildInitMessageEnvelope(); - + when(mockClientWrapper.getApiClient()).thenReturn(mockApiClient); + when(mockClientWrapper.getBotApi()).thenReturn(mockBotApi); + when(mockClientWrapper.getVersionsApi()).thenReturn(mockVersionsApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); when(mockBotApi.startSessionWithHttpInfo(eq(botId), eq(orgId), eq(initMessageEnvelope), eq(requestId))) .thenReturn(createMonoApiResponse(responseEntity)); @@ -178,7 +218,8 @@ public void testStartSession() { @Test public void testStartSessionWithUnsupportedVersion() { stubVersionsResponse("5.2.0"); - + when(mockClientWrapper.getVersionsApi()).thenReturn(mockVersionsApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); Throwable exception = assertThrows(UnsupportedSDKException.class, () -> client.startChatSession(config, new ExternalSessionId(externalSessionId), buildBotSendMessageRequest(message, Optional.of(requestId)))); @@ -191,6 +232,8 @@ public void testStartSessionWithUnsupportedVersion() { public void testStartSessionWithInvalidFirstMessageType() { stubVersionsResponse("5.1.0"); + when(mockClientWrapper.getVersionsApi()).thenReturn(mockVersionsApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); AnyRequestMessage invalidFirstMessageType = buildChoiceMessage(); Throwable exception = assertThrows(IllegalArgumentException.class, () -> @@ -204,6 +247,10 @@ public void testStartSessionWithInvalidFirstMessageType() { @Test public void testSendMessage() { + when(mockClientWrapper.getApiClient()).thenReturn(mockApiClient); + when(mockClientWrapper.getBotApi()).thenReturn(mockBotApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); + when(cache.get(anyString())).thenReturn(Optional.of(basePath)); ChatMessageResponseEnvelope sendMessageResponseEnvelope = buildChatMessageResponseEnvelope(); ResponseEntity responseEntity = TestUtils .createResponseEntity(sendMessageResponseEnvelope, httpHeaders, httpStatus); @@ -222,6 +269,10 @@ public void testSendMessage() { @Test public void testSendMessageWithRequestInterceptor() { + when(mockClientWrapper.getApiClient()).thenReturn(mockApiClient); + when(mockClientWrapper.getBotApi()).thenReturn(mockBotApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); + when(cache.get(anyString())).thenReturn(Optional.of(basePath)); ChatMessageResponseEnvelope sendMessageResponseEnvelope = buildChatMessageResponseEnvelope(); ResponseEntity responseEntity = TestUtils .createResponseEntity(sendMessageResponseEnvelope, httpHeaders, httpStatus); @@ -252,7 +303,6 @@ private void verifyRequestEnvelopInterceptorInvocation(ChatMessageEnvelope chatM @Test public void testEndSession() { - ChatMessageResponseEnvelope endSessionResponseEnvelope = buildChatMessageResponseEnvelope(); ResponseEntity responseEntity = TestUtils .createResponseEntity(endSessionResponseEnvelope, httpHeaders, httpStatus); @@ -261,6 +311,10 @@ public void testEndSession() { .endSessionWithHttpInfo(eq(sessionId), eq(orgId), eq(endSessionReason), eq(requestId), eq(runtimeCRC))) .thenReturn(createMonoApiResponse(responseEntity)); + when(mockClientWrapper.getApiClient()).thenReturn(mockApiClient); + when(mockClientWrapper.getBotApi()).thenReturn(mockBotApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); + when(cache.get(anyString())).thenReturn(Optional.of(basePath)); BotResponse response = client .endChatSession(config, new RuntimeSessionId(sessionId), buildEndSessionRequestEnvelope()); @@ -345,30 +399,20 @@ public void testGetHealthStatus() { Mono monoResponse = Mono.fromCallable(() -> healthStatus); when(mockHealthApi.checkHealthStatus()).thenReturn(monoResponse); + when(mockClientWrapper.getHealthApi()).thenReturn(mockHealthApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); - BasicChatbotClient client = ChatbotClients.basic() - .basePath(basePath) - .authMechanism(mockAuthMechanism) - .build(); - - ((BasicChatbotClientImpl) client).setHealthApi(mockHealthApi); - - assertEquals(healthStatus, client.getHealthStatus()); + assertEquals(healthStatus, client.getHealthStatus(config)); } @Test public void testGetSupportedVersions() { stubVersionsResponse("5.1.0"); + when(mockClientWrapper.getVersionsApi()).thenReturn(mockVersionsApi); + when(cache.getObject(anyString())).thenReturn(Optional.of(mockClientWrapper)); - BasicChatbotClient client = ChatbotClients.basic() - .basePath(basePath) - .authMechanism(mockAuthMechanism) - .build(); - - ((BasicChatbotClientImpl) client).setVersionsApi(mockVersionsApi); - - assertEquals(1, client.getSupportedVersions().getVersions().size()); + assertEquals(1, client.getSupportedVersions(config).getVersions().size()); } private void stubVersionsResponse(String versionNumber) { diff --git a/src/test/java/com/salesforce/einsteinbot/sdk/client/ClientApiWireMockTest.java b/src/test/java/com/salesforce/einsteinbot/sdk/client/ClientApiWireMockTest.java index 6b63cf7..4f3cfd1 100644 --- a/src/test/java/com/salesforce/einsteinbot/sdk/client/ClientApiWireMockTest.java +++ b/src/test/java/com/salesforce/einsteinbot/sdk/client/ClientApiWireMockTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.JsonProcessingException; +import com.salesforce.einsteinbot.sdk.cache.InMemoryCache; import com.salesforce.einsteinbot.sdk.client.model.BotEndSessionRequest; import com.salesforce.einsteinbot.sdk.client.model.BotHttpHeaders; import com.salesforce.einsteinbot.sdk.client.model.BotResponse; @@ -38,6 +39,7 @@ import com.salesforce.einsteinbot.sdk.client.model.ExternalSessionId; import com.salesforce.einsteinbot.sdk.client.model.RequestConfig; import com.salesforce.einsteinbot.sdk.client.model.RuntimeSessionId; +import com.salesforce.einsteinbot.sdk.client.util.ClientFactory; import com.salesforce.einsteinbot.sdk.exception.ChatbotResponseException; import com.salesforce.einsteinbot.sdk.model.EndSessionReason; import com.salesforce.einsteinbot.sdk.model.ResponseEnvelope; @@ -55,6 +57,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientRequestException; /** @@ -69,7 +72,6 @@ public class ClientApiWireMockTest { private static final String USER_AGENT_HEADER_KEY = "User-Agent"; private static final String ORG_ID_HEADER_KEY = "X-Org-Id"; private static final String REQUEST_ID_HEADER_KEY = "X-Request-ID"; - private static final String TEST_FORCE_CONFIG = "https://esw5.test1.my.pc-rnd.salesforce.com"; private static final String EXTERNAL_SESSION_KEY = "session1"; private static final String SESSION_ID = "chatbotSessionId"; private static final String responseRequestId = "ResponseRequestId"; @@ -79,6 +81,7 @@ public class ClientApiWireMockTest { private static final String END_SESSION_URI = "/v5.1.0/sessions/" + SESSION_ID; private static final String STATUS_URI = "/status"; private static final String VERSIONS_URI = "/versions"; + private static final String API_INFO_URI = "/services/data/v58.0/connect/bots/api-info"; private static final String TEST_REQUEST_ID = UUID.randomUUID().toString(); public static final String SESSION_END_REASON_HEADER_KEY = "X-Session-End-Reason"; @@ -88,18 +91,17 @@ public class ClientApiWireMockTest { private final ExternalSessionId externalSessionId = new ExternalSessionId(EXTERNAL_SESSION_KEY); private final EndSessionReason endSessionReason = EndSessionReason.USERREQUEST; - private static final RequestConfig requestConfig = RequestConfig - .with() - .botId(TEST_BOT_ID) - .orgId(TEST_ORG_ID) - .forceConfigEndpoint(TEST_FORCE_CONFIG) - .build(); - @RegisterExtension WireMockExtension wireMock = new WireMockExtension(options() .dynamicPort() .dynamicHttpsPort()); + @RegisterExtension + WireMockExtension forceMock = new WireMockExtension(options() + .dynamicPort() + .dynamicHttpsPort()); + + private RequestConfig requestConfig; private BasicChatbotClient client; private BotSendMessageRequest botSendMessageRequest; private BotEndSessionRequest botEndSessionRequest; @@ -116,6 +118,13 @@ private void setup() { Optional.ofNullable(TEST_REQUEST_ID)); String responseBodyFile = "versionsResponse.json"; stubVersionsResponse(responseBodyFile); + stubRuntimeUrlResponse(); + this.requestConfig = RequestConfig + .with() + .botId(TEST_BOT_ID) + .orgId(TEST_ORG_ID) + .forceConfigEndpoint(forceMock.getBaseUri().toString()) + .build(); } @Test @@ -134,6 +143,12 @@ void testStartSessionRequest() throws Exception { void testSendMessageRequest() throws Exception { String responseBodyFile = "sendMessageResponse.json"; stubSendMessageResponse(responseBodyFile); + InMemoryCache cache = new InMemoryCache(3600L); + cache.set(runtimeSessionId.getValue(), wireMock.getBaseUri().toString()); + cache.setObject(wireMock.getBaseUri().toString(), ClientFactory.createClient( + wireMock.getBaseUri().toString(), WebClient.builder() + )); + ((BasicChatbotClientImpl) this.client).setCache(cache); BotResponse botResponse = client.sendMessage(requestConfig, runtimeSessionId, botSendMessageRequest); @@ -145,7 +160,12 @@ void testSendMessageRequest() throws Exception { void testEndSessionRequest() throws Exception { String responseBodyFile = "endSessionResponse.json"; stubEndSessionResponse(responseBodyFile); - + InMemoryCache cache = new InMemoryCache(3600L); + cache.set(runtimeSessionId.getValue(), wireMock.getBaseUri().toString()); + cache.setObject(wireMock.getBaseUri().toString(), ClientFactory.createClient( + wireMock.getBaseUri().toString(), WebClient.builder() + )); + ((BasicChatbotClientImpl) this.client).setCache(cache); BotResponse botResponse = client.endChatSession(requestConfig, runtimeSessionId, botEndSessionRequest); verifyRequestUriAndHeadersForEndSession(END_SESSION_URI); @@ -156,7 +176,7 @@ void testEndSessionRequest() throws Exception { void testStatus() throws Exception { String responseBodyFile = "statusResponse.json"; stubStatusResponse(responseBodyFile); - Status status = client.getHealthStatus(); + Status status = client.getHealthStatus(requestConfig); verifyResponseEnvelope(responseBodyFile, status); } @@ -176,7 +196,7 @@ void testVerifyVersionFile() throws Exception { void testSupportedVersions() throws Exception { String responseBodyFile = "versionsResponse.json"; stubVersionsResponse(responseBodyFile); - SupportedVersions versions = client.getSupportedVersions(); + SupportedVersions versions = client.getSupportedVersions(requestConfig); verifyResponseEnvelope(responseBodyFile, versions); } @@ -186,7 +206,7 @@ void testSupportedVersionsIncorrectResponse() throws Exception { String responseBodyFile = "versionsErrorResponse.json"; stubVersionsResponse(responseBodyFile); Throwable exception = assertThrows(RuntimeException.class, - () -> client.getSupportedVersions()); + () -> client.getSupportedVersions(requestConfig)); assertEquals("Versions response was incorrect", exception.getMessage()); } @@ -197,7 +217,7 @@ void testSupportedVersionsErrorResponse() throws Exception { int errorStatusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); stubVersionsResponse(responseBodyFile, errorStatusCode); Throwable exception = assertThrows(RuntimeException.class, - () -> client.getSupportedVersions()); + () -> client.getSupportedVersions(requestConfig)); assertEquals("Error in getting versions response", exception.getMessage()); } @@ -242,6 +262,12 @@ void testConnectionError() { .authMechanism(new TestAuthMechanism()) .build(); + InMemoryCache cache = new InMemoryCache(3600L); + cache.setObject(wireMock.getBaseUri().toString(), ClientFactory.createClient( + replaceUriWithInvalidPort(wireMock.getBaseUri()), WebClient.builder() + )); + ((BasicChatbotClientImpl) clientForError).setCache(cache); + Throwable exceptionThrown = assertThrows(java.lang.RuntimeException.class, () -> clientForError.startChatSession(requestConfig, externalSessionId, botSendMessageRequest)); @@ -407,4 +433,16 @@ private void stubVersionsResponse(String responseBodyFile, int statusCode) { ); } + private void stubRuntimeUrlResponse() { + forceMock.stubFor( + get(API_INFO_URI) + .willReturn( + aResponse() + .withStatus(HttpStatus.OK.value()) + .withHeader("Content-Type", "application/json;charset=UTF-8") + .withBody("{\"runtimeBaseUrl\":\"" + wireMock.getBaseUri() + "\"}") + ) + ); + } + } \ No newline at end of file diff --git a/src/test/java/com/salesforce/einsteinbot/sdk/examples/ApiExampleUsingSDK.java b/src/test/java/com/salesforce/einsteinbot/sdk/examples/ApiExampleUsingSDK.java index e726bc6..b904e06 100644 --- a/src/test/java/com/salesforce/einsteinbot/sdk/examples/ApiExampleUsingSDK.java +++ b/src/test/java/com/salesforce/einsteinbot/sdk/examples/ApiExampleUsingSDK.java @@ -403,9 +403,9 @@ private void getHealthStatus() throws Exception { .authMechanism(oAuth) .build(); - Status status = client.getHealthStatus(); + Status status = client.getHealthStatus(createRequestConfig()); boolean isRuntimeUp = status.equals(StatusEnum.UP); - System.out.println("Health Status: " + convertObjectToJson(client.getHealthStatus())); + System.out.println("Health Status: " + convertObjectToJson(client.getHealthStatus(createRequestConfig()))); } private String getResponseMessageAsText(List messages) { diff --git a/src/test/java/com/salesforce/einsteinbot/sdk/examples/ChatbotClientExamples.java b/src/test/java/com/salesforce/einsteinbot/sdk/examples/ChatbotClientExamples.java index 4012d5f..d057ab1 100644 --- a/src/test/java/com/salesforce/einsteinbot/sdk/examples/ChatbotClientExamples.java +++ b/src/test/java/com/salesforce/einsteinbot/sdk/examples/ChatbotClientExamples.java @@ -153,7 +153,7 @@ private void getHealthStatus() throws Exception { .authMechanism(oAuth) .build(); - System.out.println("Health Status: " + convertObjectToJson(client.getHealthStatus())); + System.out.println("Health Status: " + convertObjectToJson(client.getHealthStatus(config))); } private void getSupportedVersions() throws Exception { @@ -162,6 +162,6 @@ private void getSupportedVersions() throws Exception { .authMechanism(oAuth) .build(); - System.out.println("Supported Versions: " + convertObjectToJson(client.getSupportedVersions())); + System.out.println("Supported Versions: " + convertObjectToJson(client.getSupportedVersions(config))); } }