From 49ce08fcfa6eae1fe9eb614c91f6b17af3932129 Mon Sep 17 00:00:00 2001 From: Gulshan Dhingra Date: Thu, 18 Jun 2026 17:54:28 +0530 Subject: [PATCH 1/2] Added helper to generate identity token --- dashx/build.gradle | 4 + dashx/src/main/java/com/dashx/Constants.java | 5 +- dashx/src/main/java/com/dashx/DashX.java | 367 +++++++++++++----- .../src/main/java/com/dashx/DashXConfig.java | 31 +- .../java/com/dashx/DashXGraphQLClient.java | 83 ++-- .../DashXConfigurationException.java | 1 + .../com/dashx/exception/DashXException.java | 1 + .../exception/DashXGraphQLException.java | 7 +- .../exception/DashXValidationException.java | 1 + .../test/java/com/dashx/DashXConfigTest.java | 101 ++--- .../com/dashx/DashXGraphQLClientTest.java | 71 ++-- .../java/com/dashx/DashXValidationTest.java | 126 ++++-- .../dashx/exception/DashXExceptionTest.java | 21 +- 13 files changed, 551 insertions(+), 268 deletions(-) diff --git a/dashx/build.gradle b/dashx/build.gradle index 92a7dda..a95a73b 100644 --- a/dashx/build.gradle +++ b/dashx/build.gradle @@ -26,6 +26,10 @@ dependencies { implementation 'org.springframework:spring-webflux:6.2.6' implementation 'io.projectreactor.netty:reactor-netty-http:1.2.0' implementation 'io.netty:netty-transport-native-epoll:4.1.115.Final:linux-x86_64' + + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } java { diff --git a/dashx/src/main/java/com/dashx/Constants.java b/dashx/src/main/java/com/dashx/Constants.java index b924307..378cef1 100644 --- a/dashx/src/main/java/com/dashx/Constants.java +++ b/dashx/src/main/java/com/dashx/Constants.java @@ -1,14 +1,17 @@ package com.dashx; public final class Constants { + public static final String PACKAGE_NAME = "com.dashx"; public static final String DEFAULT_INSTANCE = "default"; - public static final String DEFAULT_BASE_URL = "https://api.dashx.com/graphql"; + public static final String DEFAULT_BASE_URL = + "https://api.dashx.com/graphql"; public static final String DATA = "data"; private Constants() {} public static final class UserAttributes { + private UserAttributes() {} public static final String UID = "uid"; diff --git a/dashx/src/main/java/com/dashx/DashX.java b/dashx/src/main/java/com/dashx/DashX.java index b531e11..e2a5e58 100644 --- a/dashx/src/main/java/com/dashx/DashX.java +++ b/dashx/src/main/java/com/dashx/DashX.java @@ -1,42 +1,46 @@ package com.dashx; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - +import com.dashx.exception.DashXConfigurationException; +import com.dashx.exception.DashXValidationException; +import com.dashx.graphql.AccountService; +import com.dashx.graphql.AssetService; +import com.dashx.graphql.BroadcastService; +import com.dashx.graphql.EventService; +import com.dashx.graphql.IssueService; +import com.dashx.graphql.RecordService; import com.dashx.graphql.generated.types.Account; import com.dashx.graphql.generated.types.AggregateResponse; import com.dashx.graphql.generated.types.Asset; -import com.dashx.graphql.generated.types.Issue; import com.dashx.graphql.generated.types.Broadcast; +import com.dashx.graphql.generated.types.CreateBroadcastInput; import com.dashx.graphql.generated.types.CreateIssueInput; -import com.dashx.graphql.generated.types.UpsertIssueInput; import com.dashx.graphql.generated.types.IdentifyAccountInput; +import com.dashx.graphql.generated.types.Issue; import com.dashx.graphql.generated.types.SearchRecordsInput; import com.dashx.graphql.generated.types.TrackEventInput; import com.dashx.graphql.generated.types.TrackEventResponse; -import com.dashx.graphql.generated.types.CreateBroadcastInput; - -import com.dashx.graphql.AccountService; -import com.dashx.graphql.AssetService; -import com.dashx.graphql.EventService; -import com.dashx.graphql.RecordService; -import com.dashx.graphql.IssueService; -import com.dashx.graphql.BroadcastService; +import com.dashx.graphql.generated.types.UpsertIssueInput; import com.dashx.graphql.utils.SearchRecordsOptions; -import com.dashx.exception.DashXConfigurationException; -import com.dashx.exception.DashXValidationException; +import io.jsonwebtoken.Jwts; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; public class DashX { + private static final Logger logger = LoggerFactory.getLogger(DashX.class); - private static final ConcurrentHashMap instances = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap instances = + new ConcurrentHashMap<>(); private final String instanceName; @@ -136,7 +140,11 @@ private DashXGraphQLClient createGraphqlClient() { } String url = baseUrl != null ? baseUrl : Constants.DEFAULT_BASE_URL; - return new DashXGraphQLClient(new URI(url).toURL(), headers, config); + return new DashXGraphQLClient( + new URI(url).toURL(), + headers, + config + ); } catch (URISyntaxException | MalformedURLException e) { throw new DashXConfigurationException("Invalid URL", e); } @@ -150,7 +158,8 @@ private DashXGraphQLClient createGraphqlClient() { private void ensureConfigured() { if (graphqlClient == null) { throw new DashXConfigurationException( - "DashX client is not configured. Call configure() before using the client."); + "DashX client is not configured. Call configure() before using the client." + ); } } @@ -164,7 +173,6 @@ public void close() { } } - private String generateAccountAnonymousUid() { return UUID.randomUUID().toString(); } @@ -184,8 +192,11 @@ public CompletableFuture identify(Map options) { if (options == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "'identify' cannot be called with null, please pass options of type 'object'.")); + future.completeExceptionally( + new DashXValidationException( + "'identify' cannot be called with null, please pass options of type 'object'." + ) + ); return future; } @@ -193,13 +204,15 @@ public CompletableFuture identify(Map options) { ensureConfigured(); String uid = options.containsKey(Constants.UserAttributes.UID) - ? (String) options.get(Constants.UserAttributes.UID) - : this.accountUid; + ? (String) options.get(Constants.UserAttributes.UID) + : this.accountUid; String anonymousUid; if (options.containsKey(Constants.UserAttributes.ANONYMOUS_UID)) { - anonymousUid = (String) options.get(Constants.UserAttributes.ANONYMOUS_UID); + anonymousUid = (String) options.get( + Constants.UserAttributes.ANONYMOUS_UID + ); } else if (this.accountAnonymousUid != null) { anonymousUid = this.accountAnonymousUid; } else if (uid == null) { @@ -208,21 +221,31 @@ public CompletableFuture identify(Map options) { anonymousUid = null; } - IdentifyAccountInput input = - IdentifyAccountInput.newBuilder().uid(uid).anonymousUid(anonymousUid) - .email((String) options.get(Constants.UserAttributes.EMAIL)) - .phone((String) options.get(Constants.UserAttributes.PHONE)) - .name((String) options.get(Constants.UserAttributes.NAME)) - .firstName((String) options.get(Constants.UserAttributes.FIRST_NAME)) - .lastName((String) options.get(Constants.UserAttributes.LAST_NAME)).build(); - - logger.debug("Identifying account with uid: '{}', anonymousUid: '{}'", uid, anonymousUid); - return accountService.identifyAccount(input).toFuture() - .thenApply(account -> { - this.accountUid = account.getUid(); - this.accountAnonymousUid = account.getAnonymousUid(); - return account; - }); + IdentifyAccountInput input = IdentifyAccountInput.newBuilder() + .uid(uid) + .anonymousUid(anonymousUid) + .email((String) options.get(Constants.UserAttributes.EMAIL)) + .phone((String) options.get(Constants.UserAttributes.PHONE)) + .name((String) options.get(Constants.UserAttributes.NAME)) + .firstName( + (String) options.get(Constants.UserAttributes.FIRST_NAME) + ) + .lastName((String) options.get(Constants.UserAttributes.LAST_NAME)) + .build(); + + logger.debug( + "Identifying account with uid: '{}', anonymousUid: '{}'", + uid, + anonymousUid + ); + return accountService + .identifyAccount(input) + .toFuture() + .thenApply(account -> { + this.accountUid = account.getUid(); + this.accountAnonymousUid = account.getAnonymousUid(); + return account; + }); } /** @@ -234,12 +257,19 @@ public CompletableFuture identify(Map options) { * @return A CompletableFuture that will be completed with the tracking result or completed * exceptionally if there are GraphQL errors or execution errors. */ - public CompletableFuture track(String event, String uid, - Map data) { + public CompletableFuture track( + String event, + String uid, + Map data + ) { if (event == null || event.trim().isEmpty()) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "Event name cannot be null or empty")); + CompletableFuture future = + new CompletableFuture<>(); + future.completeExceptionally( + new DashXValidationException( + "Event name cannot be null or empty" + ) + ); return future; } @@ -260,18 +290,33 @@ public CompletableFuture track(String event, String uid, accAnonUid = null; } - TrackEventInput input = TrackEventInput.newBuilder().event(event).accountUid(accUid) - .accountAnonymousUid(accAnonUid).data(data).build(); - - logger.debug("Tracking event '{}' for uid: '{}', anonymousUid: '{}'", event, accUid, accAnonUid); + TrackEventInput input = TrackEventInput.newBuilder() + .event(event) + .accountUid(accUid) + .accountAnonymousUid(accAnonUid) + .data(data) + .build(); + + logger.debug( + "Tracking event '{}' for uid: '{}', anonymousUid: '{}'", + event, + accUid, + accAnonUid + ); return eventService.trackEvent(input).toFuture(); } - public CompletableFuture track(String event, Map data) { + public CompletableFuture track( + String event, + Map data + ) { return track(event, null, data); } - public CompletableFuture track(String event, String uid) { + public CompletableFuture track( + String event, + String uid + ) { return track(event, uid, null); } @@ -289,20 +334,33 @@ public CompletableFuture track(String event) { * @return A CompletableFuture that will be completed with the list of assets or completed * exceptionally if there are GraphQL errors or execution errors. */ - public CompletableFuture> listAssets(Map filter, - List> order, Integer limit, Integer page) { + public CompletableFuture> listAssets( + Map filter, + List> order, + Integer limit, + Integer page + ) { ensureConfigured(); - logger.debug("Listing assets with filter: {}, limit: {}, page: {}", filter, limit, page); + logger.debug( + "Listing assets with filter: {}, limit: {}, page: {}", + filter, + limit, + page + ); return assetService.listAssets(filter, order, limit, page).toFuture(); } - public CompletableFuture> listAssets(Map filter) { + public CompletableFuture> listAssets( + Map filter + ) { return listAssets(filter, null, null, null); } - public CompletableFuture> listAssets(Map filter, - List> order) { + public CompletableFuture> listAssets( + Map filter, + List> order + ) { return listAssets(filter, order, null, null); } @@ -320,8 +378,9 @@ public CompletableFuture> listAssets() { public CompletableFuture getAsset(String id) { if (id == null || id.trim().isEmpty()) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "Asset ID cannot be null or empty")); + future.completeExceptionally( + new DashXValidationException("Asset ID cannot be null or empty") + ); return future; } @@ -339,12 +398,16 @@ public CompletableFuture getAsset(String id) { * @return A CompletableFuture that will be completed with the search results or completed * exceptionally if there are GraphQL errors or execution errors. */ - public CompletableFuture>> searchRecords(String resource, - SearchRecordsOptions options) { + public CompletableFuture>> searchRecords( + String resource, + SearchRecordsOptions options + ) { if (resource == null || resource.trim().isEmpty()) { - CompletableFuture>> future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "Resource cannot be null or empty")); + CompletableFuture>> future = + new CompletableFuture<>(); + future.completeExceptionally( + new DashXValidationException("Resource cannot be null or empty") + ); return future; } @@ -355,17 +418,30 @@ public CompletableFuture>> searchRecords(String resourc options = SearchRecordsOptions.newBuilder().build(); } - SearchRecordsInput input = SearchRecordsInput.newBuilder().resource(resource) - .filter(options.getFilter()).order(options.getOrder()).limit(options.getLimit()) - .page(options.getPage()).preview(options.getPreview()) - .language(options.getLanguage()).fields(options.getFields()) - .include(options.getInclude()).exclude(options.getExclude()).build(); - - logger.debug("Searching records for resource: '{}' with filter: {}", resource, options.getFilter()); + SearchRecordsInput input = SearchRecordsInput.newBuilder() + .resource(resource) + .filter(options.getFilter()) + .order(options.getOrder()) + .limit(options.getLimit()) + .page(options.getPage()) + .preview(options.getPreview()) + .language(options.getLanguage()) + .fields(options.getFields()) + .include(options.getInclude()) + .exclude(options.getExclude()) + .build(); + + logger.debug( + "Searching records for resource: '{}' with filter: {}", + resource, + options.getFilter() + ); return recordService.searchRecords(input).toFuture(); } - public CompletableFuture>> searchRecords(String resource) { + public CompletableFuture>> searchRecords( + String resource + ) { return searchRecords(resource, null); } @@ -379,8 +455,9 @@ public CompletableFuture>> searchRecords(String resourc public CompletableFuture createIssue(CreateIssueInput input) { if (input == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "CreateIssueInput cannot be null")); + future.completeExceptionally( + new DashXValidationException("CreateIssueInput cannot be null") + ); return future; } @@ -401,8 +478,9 @@ public CompletableFuture createIssue(CreateIssueInput input) { public CompletableFuture upsertIssue(UpsertIssueInput input) { if (input == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "UpsertIssueInput cannot be null")); + future.completeExceptionally( + new DashXValidationException("UpsertIssueInput cannot be null") + ); return future; } @@ -423,25 +501,45 @@ public CompletableFuture upsertIssue(UpsertIssueInput input) { * @return A CompletableFuture that will be completed with the list of issues or completed * exceptionally if there are GraphQL errors or execution errors. */ - public CompletableFuture> listIssues(Map filter, - List> order, Integer limit, Integer page, String targetEnvironment) { + public CompletableFuture> listIssues( + Map filter, + List> order, + Integer limit, + Integer page, + String targetEnvironment + ) { ensureConfigured(); - logger.debug("Listing issues with filter: {}, limit: {}, page: {}", filter, limit, page); - return issueService.listIssues(filter, order, limit, page, targetEnvironment).toFuture(); - } - - public CompletableFuture> listIssues(Map filter, - List> order, Integer limit, Integer page) { + logger.debug( + "Listing issues with filter: {}, limit: {}, page: {}", + filter, + limit, + page + ); + return issueService + .listIssues(filter, order, limit, page, targetEnvironment) + .toFuture(); + } + + public CompletableFuture> listIssues( + Map filter, + List> order, + Integer limit, + Integer page + ) { return listIssues(filter, order, limit, page, null); } - public CompletableFuture> listIssues(Map filter, - List> order) { + public CompletableFuture> listIssues( + Map filter, + List> order + ) { return listIssues(filter, order, null, null, null); } - public CompletableFuture> listIssues(Map filter) { + public CompletableFuture> listIssues( + Map filter + ) { return listIssues(filter, null, null, null, null); } @@ -458,15 +556,21 @@ public CompletableFuture> listIssues() { * the count of matching issues, or completed exceptionally if there are GraphQL errors * or execution errors. */ - public CompletableFuture aggregateIssues(Map filter, - String targetEnvironment) { + public CompletableFuture aggregateIssues( + Map filter, + String targetEnvironment + ) { ensureConfigured(); logger.debug("Aggregating issues with filter: {}", filter); - return issueService.aggregateIssues(filter, targetEnvironment).toFuture(); + return issueService + .aggregateIssues(filter, targetEnvironment) + .toFuture(); } - public CompletableFuture aggregateIssues(Map filter) { + public CompletableFuture aggregateIssues( + Map filter + ) { return aggregateIssues(filter, null); } @@ -481,11 +585,16 @@ public CompletableFuture aggregateIssues() { * @return A CompletableFuture that will be completed with the created Broadcast or completed * exceptionally if there are GraphQL errors or execution errors. */ - public CompletableFuture sendBroadcast(CreateBroadcastInput input) { + public CompletableFuture sendBroadcast( + CreateBroadcastInput input + ) { if (input == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new DashXValidationException( - "CreateBroadcastInput cannot be null")); + future.completeExceptionally( + new DashXValidationException( + "CreateBroadcastInput cannot be null" + ) + ); return future; } @@ -494,4 +603,60 @@ public CompletableFuture sendBroadcast(CreateBroadcastInput input) { logger.debug("Creating broadcast"); return broadcastService.createBroadcast(input).toFuture(); } + + /** Default identity-token lifetime: 7 days. */ + private static final long DEFAULT_IDENTITY_TOKEN_EXPIRY_SECONDS = + 60L * 60 * 24 * 7; + + /** + * Generates an identity token (a short-lived JWT) for a visitor, used by the + * browser / React SDKs to authenticate that identity. + * + * @param uid the identity's uid + * @param kind the identity kind, {@code "USER"} or {@code "VISITOR"} + * @param expiresInSeconds token lifetime in seconds + * @return the signed JWT + */ + public String generateIdentityToken( + String uid, + String kind, + long expiresInSeconds + ) { + if (uid == null || uid.trim().isEmpty()) { + throw new DashXValidationException("uid cannot be null or empty"); + } + + ensureConfigured(); + + Date now = new Date(); + Date expiration = new Date(now.getTime() + expiresInSeconds * 1000L); + SecretKey key = new SecretKeySpec( + privateKey.getBytes(StandardCharsets.UTF_8), + "HmacSHA256" + ); + + return Jwts.builder() + .claim("kind", kind) + .claim("uid", uid) + .issuedAt(now) + .expiration(expiration) + .signWith(key, Jwts.SIG.HS256) + .compact(); + } + + public String generateIdentityToken(String uid, String kind) { + return generateIdentityToken( + uid, + kind, + DEFAULT_IDENTITY_TOKEN_EXPIRY_SECONDS + ); + } + + public String generateIdentityToken(String uid) { + return generateIdentityToken( + uid, + "USER", + DEFAULT_IDENTITY_TOKEN_EXPIRY_SECONDS + ); + } } diff --git a/dashx/src/main/java/com/dashx/DashXConfig.java b/dashx/src/main/java/com/dashx/DashXConfig.java index 8e34cfa..cdda6f4 100644 --- a/dashx/src/main/java/com/dashx/DashXConfig.java +++ b/dashx/src/main/java/com/dashx/DashXConfig.java @@ -3,6 +3,7 @@ import com.dashx.exception.DashXConfigurationException; public final class DashXConfig { + private final String baseUrl; private final String publicKey; private final String privateKey; @@ -56,6 +57,7 @@ public Integer getMaxIdleTime() { } public static class Builder { + private String baseUrl = Constants.DEFAULT_BASE_URL; private String publicKey; private String privateKey; @@ -121,26 +123,41 @@ public Builder maxIdleTime(Integer maxIdleTime) { public DashXConfig build() { if (publicKey == null) { - throw new DashXConfigurationException("publicKey must not be null"); + throw new DashXConfigurationException( + "publicKey must not be null" + ); } if (privateKey == null) { - throw new DashXConfigurationException("privateKey must not be null"); + throw new DashXConfigurationException( + "privateKey must not be null" + ); } if (targetEnvironment == null) { - throw new DashXConfigurationException("targetEnvironment must not be null"); + throw new DashXConfigurationException( + "targetEnvironment must not be null" + ); } if (connectionTimeout != null && connectionTimeout <= 0) { - throw new DashXConfigurationException("connectionTimeout must be positive, got: " + connectionTimeout); + throw new DashXConfigurationException( + "connectionTimeout must be positive, got: " + + connectionTimeout + ); } if (responseTimeout != null && responseTimeout <= 0) { - throw new DashXConfigurationException("responseTimeout must be positive, got: " + responseTimeout); + throw new DashXConfigurationException( + "responseTimeout must be positive, got: " + responseTimeout + ); } if (maxConnections != null && maxConnections <= 0) { - throw new DashXConfigurationException("maxConnections must be positive, got: " + maxConnections); + throw new DashXConfigurationException( + "maxConnections must be positive, got: " + maxConnections + ); } if (maxIdleTime != null && maxIdleTime <= 0) { - throw new DashXConfigurationException("maxIdleTime must be positive, got: " + maxIdleTime); + throw new DashXConfigurationException( + "maxIdleTime must be positive, got: " + maxIdleTime + ); } return new DashXConfig(this); diff --git a/dashx/src/main/java/com/dashx/DashXGraphQLClient.java b/dashx/src/main/java/com/dashx/DashXGraphQLClient.java index 61ab398..b447243 100644 --- a/dashx/src/main/java/com/dashx/DashXGraphQLClient.java +++ b/dashx/src/main/java/com/dashx/DashXGraphQLClient.java @@ -23,6 +23,7 @@ * Uses Spring WebFlux's reactive WebClient under the hood for non-blocking I/O. */ public class DashXGraphQLClient { + private final WebClientGraphQLClient webClientGraphQLClient; private final ConnectionProvider connectionProvider; @@ -34,40 +35,54 @@ public class DashXGraphQLClient { * @param headers HTTP headers to include with every request (e.g., authentication keys, environment) * @param config configuration object containing timeout and connection pool settings */ - public DashXGraphQLClient(URL url, MultiValueMap headers, DashXConfig config) { + public DashXGraphQLClient( + URL url, + MultiValueMap headers, + DashXConfig config + ) { // Use config values or defaults (all timeout values are in milliseconds) - int connectionTimeout = config != null && config.getConnectionTimeout() != null - ? config.getConnectionTimeout() : 10000; - int responseTimeout = config != null && config.getResponseTimeout() != null - ? config.getResponseTimeout() : 30000; - int maxConnections = config != null && config.getMaxConnections() != null - ? config.getMaxConnections() : 500; - int maxIdleTime = config != null && config.getMaxIdleTime() != null - ? config.getMaxIdleTime() : 20000; + int connectionTimeout = + config != null && config.getConnectionTimeout() != null + ? config.getConnectionTimeout() + : 10000; + int responseTimeout = + config != null && config.getResponseTimeout() != null + ? config.getResponseTimeout() + : 30000; + int maxConnections = + config != null && config.getMaxConnections() != null + ? config.getMaxConnections() + : 500; + int maxIdleTime = + config != null && config.getMaxIdleTime() != null + ? config.getMaxIdleTime() + : 20000; // Configure connection pool this.connectionProvider = ConnectionProvider.builder("dashx-pool") - .maxConnections(maxConnections) - .maxIdleTime(Duration.ofMillis(maxIdleTime)) - .maxLifeTime(Duration.ofSeconds(60)) - .pendingAcquireTimeout(Duration.ofSeconds(60)) - .evictInBackground(Duration.ofSeconds(120)) - .build(); + .maxConnections(maxConnections) + .maxIdleTime(Duration.ofMillis(maxIdleTime)) + .maxLifeTime(Duration.ofSeconds(60)) + .pendingAcquireTimeout(Duration.ofSeconds(60)) + .evictInBackground(Duration.ofSeconds(120)) + .build(); // Configure HTTP client with timeouts and keep-alive HttpClient httpClient = HttpClient.create(connectionProvider) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout) - .responseTimeout(Duration.ofMillis(responseTimeout)) - .keepAlive(true); + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout) + .responseTimeout(Duration.ofMillis(responseTimeout)) + .keepAlive(true); // Build WebClient with configured HTTP client WebClient webClient = WebClient.builder() - .baseUrl(url.toString()) - .defaultHeaders(httpHeaders -> httpHeaders.addAll(headers)) - .clientConnector(new ReactorClientHttpConnector(httpClient)) - .build(); + .baseUrl(url.toString()) + .defaultHeaders(httpHeaders -> httpHeaders.addAll(headers)) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); - this.webClientGraphQLClient = MonoGraphQLClient.createWithWebClient(webClient); + this.webClientGraphQLClient = MonoGraphQLClient.createWithWebClient( + webClient + ); } /** @@ -79,17 +94,21 @@ public DashXGraphQLClient(URL url, MultiValueMap headers, DashXC * @return a Mono that emits the GraphQLResponse on success, or an error if GraphQL errors occurred * @throws DashXGraphQLException if the GraphQL response contains errors */ - public Mono execute(String query, Map variables) { - return this.webClientGraphQLClient.reactiveExecuteQuery(query, variables) - .flatMap(response -> { - List errors = response.getErrors(); + public Mono execute( + String query, + Map variables + ) { + return this.webClientGraphQLClient + .reactiveExecuteQuery(query, variables) + .flatMap(response -> { + List errors = response.getErrors(); - if (errors != null && !errors.isEmpty()) { - return Mono.error(new DashXGraphQLException(errors)); - } + if (errors != null && !errors.isEmpty()) { + return Mono.error(new DashXGraphQLException(errors)); + } - return Mono.just(response); - }); + return Mono.just(response); + }); } /** diff --git a/dashx/src/main/java/com/dashx/exception/DashXConfigurationException.java b/dashx/src/main/java/com/dashx/exception/DashXConfigurationException.java index 40ba991..91c2eea 100644 --- a/dashx/src/main/java/com/dashx/exception/DashXConfigurationException.java +++ b/dashx/src/main/java/com/dashx/exception/DashXConfigurationException.java @@ -5,6 +5,7 @@ * This includes invalid URLs, missing required configuration, or invalid configuration values. */ public class DashXConfigurationException extends DashXException { + /** * Constructs a new configuration exception with the specified detail message. * diff --git a/dashx/src/main/java/com/dashx/exception/DashXException.java b/dashx/src/main/java/com/dashx/exception/DashXException.java index c8c0473..2ac577c 100644 --- a/dashx/src/main/java/com/dashx/exception/DashXException.java +++ b/dashx/src/main/java/com/dashx/exception/DashXException.java @@ -5,6 +5,7 @@ * All custom exceptions in the DashX SDK should extend this class. */ public class DashXException extends RuntimeException { + /** * Constructs a new DashX exception with the specified detail message. * diff --git a/dashx/src/main/java/com/dashx/exception/DashXGraphQLException.java b/dashx/src/main/java/com/dashx/exception/DashXGraphQLException.java index e891c74..d0e152a 100644 --- a/dashx/src/main/java/com/dashx/exception/DashXGraphQLException.java +++ b/dashx/src/main/java/com/dashx/exception/DashXGraphQLException.java @@ -8,6 +8,7 @@ * Contains the list of GraphQL errors returned by the server. */ public class DashXGraphQLException extends DashXException { + private final List errors; /** @@ -56,12 +57,16 @@ private static String formatErrorMessage(List errors) { } StringBuilder sb = new StringBuilder("GraphQL errors occurred:\n"); + for (int i = 0; i < errors.size(); i++) { - sb.append(i + 1).append(". ").append(errors.get(i).getMessage()); + sb.append(i + 1) + .append(". ") + .append(errors.get(i).getMessage()); if (i < errors.size() - 1) { sb.append("\n"); } } + return sb.toString(); } } diff --git a/dashx/src/main/java/com/dashx/exception/DashXValidationException.java b/dashx/src/main/java/com/dashx/exception/DashXValidationException.java index 9f6ebc3..cf42e62 100644 --- a/dashx/src/main/java/com/dashx/exception/DashXValidationException.java +++ b/dashx/src/main/java/com/dashx/exception/DashXValidationException.java @@ -5,6 +5,7 @@ * This includes null checks, empty string validation, and other input parameter validation. */ public class DashXValidationException extends DashXException { + /** * Constructs a new validation exception with the specified detail message. * diff --git a/dashx/src/test/java/com/dashx/DashXConfigTest.java b/dashx/src/test/java/com/dashx/DashXConfigTest.java index e0561e2..175aac9 100644 --- a/dashx/src/test/java/com/dashx/DashXConfigTest.java +++ b/dashx/src/test/java/com/dashx/DashXConfigTest.java @@ -1,18 +1,19 @@ package com.dashx; +import static org.junit.jupiter.api.Assertions.*; + import com.dashx.exception.DashXConfigurationException; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; class DashXConfigTest { @Test void testBuilderWithRequiredFields() { DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("test") - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("test") + .build(); assertEquals("test-public-key", config.getPublicKey()); assertEquals("test-private-key", config.getPrivateKey()); @@ -23,15 +24,15 @@ void testBuilderWithRequiredFields() { @Test void testBuilderWithAllFields() { DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("production") - .baseUrl("https://custom.api.com/graphql") - .connectionTimeout(15000) - .responseTimeout(45000) - .maxConnections(1000) - .maxIdleTime(30000) - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("production") + .baseUrl("https://custom.api.com/graphql") + .connectionTimeout(15000) + .responseTimeout(45000) + .maxConnections(1000) + .maxIdleTime(30000) + .build(); assertEquals("test-public-key", config.getPublicKey()); assertEquals("test-private-key", config.getPrivateKey()); @@ -46,10 +47,10 @@ void testBuilderWithAllFields() { @Test void testBuilderWithDefaults() { DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("test") - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("test") + .build(); // Check default values assertEquals(10000, config.getConnectionTimeout()); @@ -62,9 +63,9 @@ void testBuilderWithDefaults() { void testBuilderThrowsExceptionWhenPublicKeyIsNull() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .privateKey("test-private-key") - .targetEnvironment("test") - .build(); + .privateKey("test-private-key") + .targetEnvironment("test") + .build(); }); } @@ -72,9 +73,9 @@ void testBuilderThrowsExceptionWhenPublicKeyIsNull() { void testBuilderThrowsExceptionWhenPrivateKeyIsNull() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("test-public-key") - .targetEnvironment("test") - .build(); + .publicKey("test-public-key") + .targetEnvironment("test") + .build(); }); } @@ -82,9 +83,9 @@ void testBuilderThrowsExceptionWhenPrivateKeyIsNull() { void testBuilderThrowsExceptionWhenTargetEnvironmentIsNull() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .build(); }); } @@ -92,11 +93,11 @@ void testBuilderThrowsExceptionWhenTargetEnvironmentIsNull() { void testBuilderThrowsExceptionForNegativeConnectionTimeout() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test") - .connectionTimeout(-1) - .build(); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test") + .connectionTimeout(-1) + .build(); }); } @@ -104,11 +105,11 @@ void testBuilderThrowsExceptionForNegativeConnectionTimeout() { void testBuilderThrowsExceptionForZeroResponseTimeout() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test") - .responseTimeout(0) - .build(); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test") + .responseTimeout(0) + .build(); }); } @@ -116,11 +117,11 @@ void testBuilderThrowsExceptionForZeroResponseTimeout() { void testBuilderThrowsExceptionForNegativeMaxConnections() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test") - .maxConnections(-5) - .build(); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test") + .maxConnections(-5) + .build(); }); } @@ -128,11 +129,11 @@ void testBuilderThrowsExceptionForNegativeMaxConnections() { void testBuilderThrowsExceptionForZeroMaxIdleTime() { assertThrows(DashXConfigurationException.class, () -> { new DashXConfig.Builder() - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test") - .maxIdleTime(0) - .build(); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test") + .maxIdleTime(0) + .build(); }); } @@ -140,9 +141,9 @@ void testBuilderThrowsExceptionForZeroMaxIdleTime() { void testBuilderChaining() { DashXConfig.Builder builder = new DashXConfig.Builder(); DashXConfig.Builder result = builder - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test"); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test"); assertSame(builder, result.publicKey("key")); assertNotNull(result); diff --git a/dashx/src/test/java/com/dashx/DashXGraphQLClientTest.java b/dashx/src/test/java/com/dashx/DashXGraphQLClientTest.java index 49e194f..ffc6b1e 100644 --- a/dashx/src/test/java/com/dashx/DashXGraphQLClientTest.java +++ b/dashx/src/test/java/com/dashx/DashXGraphQLClientTest.java @@ -1,9 +1,17 @@ package com.dashx; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + import com.dashx.exception.DashXGraphQLException; import com.netflix.graphql.dgs.client.GraphQLError; import com.netflix.graphql.dgs.client.GraphQLResponse; import com.netflix.graphql.dgs.client.WebClientGraphQLClient; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -13,15 +21,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class DashXGraphQLClientTest { @@ -41,12 +40,16 @@ void testClientCreationWithValidConfig() throws MalformedURLException { headers.add("X-Public-Key", "test-key"); DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("test") - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("test") + .build(); - DashXGraphQLClient client = new DashXGraphQLClient(url, headers, config); + DashXGraphQLClient client = new DashXGraphQLClient( + url, + headers, + config + ); assertNotNull(client); } @@ -68,16 +71,20 @@ void testClientCreationWithCustomTimeouts() throws MalformedURLException { MultiValueMap headers = new LinkedMultiValueMap<>(); DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("test") - .connectionTimeout(15000) - .responseTimeout(45000) - .maxConnections(1000) - .maxIdleTime(30000) - .build(); - - DashXGraphQLClient client = new DashXGraphQLClient(url, headers, config); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("test") + .connectionTimeout(15000) + .responseTimeout(45000) + .maxConnections(1000) + .maxIdleTime(30000) + .build(); + + DashXGraphQLClient client = new DashXGraphQLClient( + url, + headers, + config + ); assertNotNull(client); } @@ -97,12 +104,16 @@ void testConfigurationDefaults() throws MalformedURLException { // Create with minimal config to test defaults DashXConfig config = new DashXConfig.Builder() - .publicKey("key") - .privateKey("secret") - .targetEnvironment("test") - .build(); - - DashXGraphQLClient client = new DashXGraphQLClient(url, headers, config); + .publicKey("key") + .privateKey("secret") + .targetEnvironment("test") + .build(); + + DashXGraphQLClient client = new DashXGraphQLClient( + url, + headers, + config + ); // Verify client was created successfully assertNotNull(client); diff --git a/dashx/src/test/java/com/dashx/DashXValidationTest.java b/dashx/src/test/java/com/dashx/DashXValidationTest.java index c20bba7..b372e2d 100644 --- a/dashx/src/test/java/com/dashx/DashXValidationTest.java +++ b/dashx/src/test/java/com/dashx/DashXValidationTest.java @@ -1,17 +1,16 @@ package com.dashx; +import static org.junit.jupiter.api.Assertions.*; + import com.dashx.exception.DashXConfigurationException; import com.dashx.exception.DashXValidationException; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class DashXValidationTest { @@ -24,10 +23,10 @@ void setUp() { // Configure with valid settings DashXConfig config = new DashXConfig.Builder() - .publicKey("test-public-key") - .privateKey("test-private-key") - .targetEnvironment("test") - .build(); + .publicKey("test-public-key") + .privateKey("test-private-key") + .targetEnvironment("test") + .build(); dashx.configure(config); } @@ -62,45 +61,72 @@ void testUnconfiguredClientThrowsException() { void testIdentifyWithNullOptionsThrowsException() { CompletableFuture future = dashx.identify(null); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); - assertTrue(exception.getCause().getMessage().contains("cannot be called with null")); + assertTrue( + exception + .getCause() + .getMessage() + .contains("cannot be called with null") + ); } @Test void testTrackWithNullEventThrowsException() { CompletableFuture future = dashx.track(null, "user123"); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); - assertTrue(exception.getCause().getMessage().contains("Event name cannot be null")); + assertTrue( + exception + .getCause() + .getMessage() + .contains("Event name cannot be null") + ); } @Test void testTrackWithEmptyEventThrowsException() { CompletableFuture future = dashx.track("", "user123"); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); - assertTrue(exception.getCause().getMessage().contains("Event name cannot be null or empty")); + assertTrue( + exception + .getCause() + .getMessage() + .contains("Event name cannot be null or empty") + ); } @Test void testTrackWithWhitespaceEventThrowsException() { CompletableFuture future = dashx.track(" ", "user123"); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); } @@ -109,21 +135,32 @@ void testTrackWithWhitespaceEventThrowsException() { void testGetAssetWithNullIdThrowsException() { CompletableFuture future = dashx.getAsset(null); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); - assertTrue(exception.getCause().getMessage().contains("Asset ID cannot be null")); + assertTrue( + exception + .getCause() + .getMessage() + .contains("Asset ID cannot be null") + ); } @Test void testGetAssetWithEmptyIdThrowsException() { CompletableFuture future = dashx.getAsset(""); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); } @@ -132,21 +169,32 @@ void testGetAssetWithEmptyIdThrowsException() { void testSearchRecordsWithNullResourceThrowsException() { CompletableFuture future = dashx.searchRecords(null); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); - assertTrue(exception.getCause().getMessage().contains("Resource cannot be null")); + assertTrue( + exception + .getCause() + .getMessage() + .contains("Resource cannot be null") + ); } @Test void testSearchRecordsWithEmptyResourceThrowsException() { CompletableFuture future = dashx.searchRecords(""); - ExecutionException exception = assertThrows(ExecutionException.class, () -> { - future.get(); - }); + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> { + future.get(); + } + ); assertTrue(exception.getCause() instanceof DashXValidationException); } diff --git a/dashx/src/test/java/com/dashx/exception/DashXExceptionTest.java b/dashx/src/test/java/com/dashx/exception/DashXExceptionTest.java index 13435e7..2c98680 100644 --- a/dashx/src/test/java/com/dashx/exception/DashXExceptionTest.java +++ b/dashx/src/test/java/com/dashx/exception/DashXExceptionTest.java @@ -1,13 +1,13 @@ package com.dashx.exception; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + import com.netflix.graphql.dgs.client.GraphQLError; -import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.Test; class DashXExceptionTest { @@ -41,7 +41,9 @@ void testDashXExceptionWithCause() { @Test void testDashXValidationException() { String message = "Validation failed"; - DashXValidationException exception = new DashXValidationException(message); + DashXValidationException exception = new DashXValidationException( + message + ); assertEquals(message, exception.getMessage()); assertTrue(exception instanceof DashXException); @@ -50,7 +52,9 @@ void testDashXValidationException() { @Test void testDashXConfigurationException() { String message = "Configuration error"; - DashXConfigurationException exception = new DashXConfigurationException(message); + DashXConfigurationException exception = new DashXConfigurationException( + message + ); assertEquals(message, exception.getMessage()); assertTrue(exception instanceof DashXException); @@ -100,7 +104,10 @@ void testDashXGraphQLExceptionWithCustomMessage() { List errors = Collections.singletonList(error); String customMessage = "Custom error message"; - DashXGraphQLException exception = new DashXGraphQLException(customMessage, errors); + DashXGraphQLException exception = new DashXGraphQLException( + customMessage, + errors + ); assertEquals(customMessage, exception.getMessage()); assertEquals(errors, exception.getErrors()); From 977eae3b78d7987eda9693ae56b680549d93085f Mon Sep 17 00:00:00 2001 From: Gulshan Dhingra Date: Thu, 18 Jun 2026 17:59:29 +0530 Subject: [PATCH 2/2] Update version number --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2799e5c..c45c0a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format [versions] -dashx = "1.4.2" +dashx = "1.4.3" group = "com.dashx" junit-jupiter = "5.11.3"