";
+
+ // Check if the token is either not initialized or has expired
+ if (Token.isExpired(token)) {
+ // Create a BearerToken object using the credentials string
+ BearerToken bearerToken = BearerToken.builder()
+ .setCredentials(fileContents) // Set credentials from the string
+ .build();
+
+ // Generate a new Bearer Token
+ token = bearerToken.getBearerToken();
+ }
+
+ // Print the generated Bearer Token to the console
+ System.out.println("Generated Bearer Token (from string): " + token);
+ } catch (SkyflowException e) {
+ // Handle any exceptions encountered during the token generation process
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java
new file mode 100644
index 00000000..30d8f9ba
--- /dev/null
+++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java
@@ -0,0 +1,72 @@
+package com.example.serviceaccount;
+
+import com.skyflow.errors.SkyflowException;
+import com.skyflow.serviceaccount.util.BearerToken;
+
+import java.io.File;
+
+/**
+ * This example demonstrates how to generate Bearer tokens in two different ways:
+ * 1. Using a credentials file specified by its file path.
+ * 2. Using the credentials as a string.
+ *
+ * The code also showcases multithreaded token generation with a shared context (`ctx`),
+ * where each thread generates and prints tokens repeatedly.
+ */
+public class BearerTokenGenerationUsingThreadsExample {
+ public static void main(String[] args) {
+ // Example 1: Generate Bearer token using a credentials file path
+ try {
+ // Step 1: Specify the path to the credentials file
+ String filePath = ""; // Replace with the actual file path
+
+ // Step 2: Create a BearerToken object using the file path
+ final BearerToken bearerToken = BearerToken.builder()
+ .setCredentials(new File(filePath)) // Provide the credentials file
+ .setCtx("abc") // Specify a context string ("abc" in this case)
+ .build();
+
+ // Step 3: Create and start a thread to repeatedly generate and print tokens
+ Thread t = new Thread(() -> {
+ for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times
+ try {
+ System.out.println(bearerToken.getBearerToken()); // Print the Bearer token
+ } catch (SkyflowException e) { // Handle exceptions during token generation
+ Thread.currentThread().interrupt(); // Interrupt the thread on error
+ throw new RuntimeException(e); // Wrap and propagate the exception
+ }
+ }
+ });
+ t.start(); // Start the thread
+ } catch (Exception e) { // Handle exceptions during BearerToken creation
+ e.printStackTrace();
+ }
+
+ // Example 2: Generate Bearer token using credentials as a string
+ try {
+ // Step 1: Specify the credentials as a string (file contents)
+ String fileContents = ""; // Replace with actual file contents
+
+ // Step 2: Create a BearerToken object using the credentials string
+ final BearerToken bearerToken = BearerToken.builder()
+ .setCredentials(fileContents) // Provide the credentials as a string
+ .setCtx("abc") // Specify a context string ("abc" in this case)
+ .build();
+
+ // Step 3: Create and start a thread to repeatedly generate and print tokens
+ Thread t = new Thread(() -> {
+ for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times
+ try {
+ System.out.println(bearerToken.getBearerToken()); // Print the Bearer token
+ } catch (SkyflowException e) { // Handle exceptions during token generation
+ Thread.currentThread().interrupt(); // Interrupt the thread on error
+ throw new RuntimeException(e); // Wrap and propagate the exception
+ }
+ }
+ });
+ t.start(); // Start the thread
+ } catch (Exception e) { // Handle exceptions during BearerToken creation
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java
new file mode 100644
index 00000000..fcb2a407
--- /dev/null
+++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java
@@ -0,0 +1,73 @@
+package com.example.serviceaccount;
+
+import com.skyflow.errors.SkyflowException;
+import com.skyflow.serviceaccount.util.BearerToken;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Example program to generate a Bearer Token using Skyflow's BearerToken utility.
+ * The token is generated using three approaches:
+ * 1. By providing a string context.
+ * 2. By providing a JSON object context (Map) for conditional data access policies.
+ * 3. By providing the credentials as a string with context.
+ */
+public class BearerTokenGenerationWithContextExample {
+ public static void main(String[] args) {
+ String bearerToken = null;
+
+ // Approach 1: Bearer token with string context
+ // Use a simple string identifier when your policy references a single context value.
+ try {
+ String filePath = "";
+ BearerToken token = BearerToken.builder()
+ .setCredentials(new File(filePath))
+ .setCtx("user_12345")
+ .build();
+
+ bearerToken = token.getBearerToken();
+ System.out.println("Bearer token (string context): " + bearerToken);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+
+ // Approach 2: Bearer token with JSON object context
+ // Use a structured Map when your policy needs multiple context values.
+ // Each key maps to a Skyflow CEL policy variable under request.context.*
+ // For example, the map below enables policies like:
+ // request.context.role == "admin" && request.context.department == "finance"
+ try {
+ String filePath = "";
+ Map ctx = new HashMap<>();
+ ctx.put("role", "admin");
+ ctx.put("department", "finance");
+ ctx.put("user_id", "user_12345");
+
+ BearerToken token = BearerToken.builder()
+ .setCredentials(new File(filePath))
+ .setCtx(ctx)
+ .build();
+
+ bearerToken = token.getBearerToken();
+ System.out.println("Bearer token (object context): " + bearerToken);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+
+ // Approach 3: Bearer token with string context from credentials string
+ try {
+ String fileContents = "";
+ BearerToken token = BearerToken.builder()
+ .setCredentials(fileContents)
+ .setCtx("user_12345")
+ .build();
+
+ bearerToken = token.getBearerToken();
+ System.out.println("Bearer token (creds string): " + bearerToken);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java
new file mode 100644
index 00000000..3cba8bd5
--- /dev/null
+++ b/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java
@@ -0,0 +1,66 @@
+package com.example.serviceaccount;
+
+import com.skyflow.errors.SkyflowException;
+import com.skyflow.serviceaccount.util.BearerToken;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * This example demonstrates how to generate a Scoped Bearer Token in two ways:
+ * 1. Using a credentials file specified by its file path.
+ * 2. Using the credentials as a string.
+ *
+ * Scoped tokens are generated by assigning specific roles for access control.
+ */
+public class ScopedTokenGenerationExample {
+ public static void main(String[] args) {
+ String scopedToken = null; // Variable to store the generated Scoped Bearer Token
+
+ // Example 1: Generate Scoped Token using a credentials file path
+ try {
+ // Step 1: Specify the roles required for the scoped token
+ ArrayList roles = new ArrayList<>();
+ roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID
+
+ // Step 2: Specify the path to the credentials file
+ String filePath = ""; // Replace with the actual file path
+
+ // Step 3: Create a BearerToken object using the file path and roles
+ BearerToken bearerToken = BearerToken.builder()
+ .setCredentials(new File(filePath)) // Provide the credentials file
+ .setRoles(roles) // Set the roles for the scoped token
+ .build();
+
+ // Step 4: Generate and print the Scoped Bearer Token
+ scopedToken = bearerToken.getBearerToken();
+ System.out.println("Scoped Token (using file path): " + scopedToken);
+ } catch (SkyflowException e) { // Handle exceptions during token generation
+ System.out.println("Error occurred while generating Scoped Token using file path:");
+ e.printStackTrace();
+ }
+
+ // Example 2: Generate Scoped Token using credentials as a string
+ try {
+ // Step 1: Specify the roles required for the scoped token
+ ArrayList roles = new ArrayList<>();
+ roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID
+
+ // Step 2: Specify the credentials as a string (file contents)
+ String fileContents = ""; // Replace with actual file contents
+
+ // Step 3: Create a BearerToken object using the credentials string and roles
+ BearerToken bearerToken = BearerToken.builder()
+ .setCredentials(fileContents) // Provide the credentials as a string
+ .setRoles(roles) // Set the roles for the scoped token
+ .build();
+
+ // Step 4: Generate and print the Scoped Bearer Token
+ scopedToken = bearerToken.getBearerToken();
+ System.out.println("Scoped Token (using credentials string): " + scopedToken);
+ } catch (SkyflowException e) { // Handle exceptions during token generation
+ System.out.println("Error occurred while generating Scoped Token using credentials string:");
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java
new file mode 100644
index 00000000..517d5cc8
--- /dev/null
+++ b/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java
@@ -0,0 +1,89 @@
+package com.example.serviceaccount;
+
+import com.skyflow.errors.SkyflowException;
+import com.skyflow.serviceaccount.util.SignedDataTokenResponse;
+import com.skyflow.serviceaccount.util.SignedDataTokens;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This example demonstrates how to generate Signed Data Tokens using:
+ * 1. String context.
+ * 2. JSON object context (Map) for conditional data access policies.
+ * 3. Credentials string with context.
+ */
+public class SignedTokenGenerationExample {
+ public static void main(String[] args) {
+ List signedTokenValues;
+
+ // Example 1: Signed data tokens with string context
+ try {
+ String filePath = "";
+ String context = "user_12345";
+ ArrayList dataTokens = new ArrayList<>();
+ dataTokens.add("YOUR_DATA_TOKEN_1");
+
+ SignedDataTokens signedToken = SignedDataTokens.builder()
+ .setCredentials(new File(filePath))
+ .setCtx(context)
+ .setTimeToLive(30)
+ .setDataTokens(dataTokens)
+ .build();
+
+ signedTokenValues = signedToken.getSignedDataTokens();
+ System.out.println("Signed Tokens (string context): " + signedTokenValues);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+
+ // Example 2: Signed data tokens with JSON object context
+ // Each key maps to a Skyflow CEL policy variable under request.context.*
+ // For example: request.context.role == "analyst" && request.context.department == "research"
+ try {
+ String filePath = "";
+ Map ctx = new HashMap<>();
+ ctx.put("role", "analyst");
+ ctx.put("department", "research");
+ ctx.put("user_id", "user_67890");
+
+ ArrayList dataTokens = new ArrayList<>();
+ dataTokens.add("YOUR_DATA_TOKEN_1");
+
+ SignedDataTokens signedToken = SignedDataTokens.builder()
+ .setCredentials(new File(filePath))
+ .setCtx(ctx)
+ .setTimeToLive(30)
+ .setDataTokens(dataTokens)
+ .build();
+
+ signedTokenValues = signedToken.getSignedDataTokens();
+ System.out.println("Signed Tokens (object context): " + signedTokenValues);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+
+ // Example 3: Signed data tokens from credentials string
+ try {
+ String fileContents = "";
+ String context = "user_12345";
+ ArrayList dataTokens = new ArrayList<>();
+ dataTokens.add("YOUR_DATA_TOKEN_1");
+
+ SignedDataTokens signedToken = SignedDataTokens.builder()
+ .setCredentials(fileContents)
+ .setCtx(context)
+ .setTimeToLive(30)
+ .setDataTokens(dataTokens)
+ .build();
+
+ signedTokenValues = signedToken.getSignedDataTokens();
+ System.out.println("Signed Tokens (creds string): " + signedTokenValues);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java
index 6dd6cf29..e4261a00 100644
--- a/flowvault/src/main/java/com/skyflow/utils/Utils.java
+++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java
@@ -2,9 +2,7 @@
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import com.google.gson.JsonObject;
import com.skyflow.config.VaultConfig;
@@ -61,12 +59,8 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
public final class Utils extends BaseUtils {
@@ -445,7 +439,20 @@ public static List handleBulkInsertBatchException(
} else {
int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0;
for (int j = 0; j < batch.size(); j++) {
- BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null);
+ String message = null;
+ if (cause != null && cause.getMessage() != null){
+ message = cause.getMessage();
+ }
+ if (cause != null && cause.getLocalizedMessage() !=null) {
+ message = cause.getLocalizedMessage();
+ }
+ if (cause != null && cause.getCause() !=null) {
+ message = cause.getCause().toString();
+ }
+ if (message == null || message.isEmpty() || message.trim().isEmpty()){
+ message = ex.getMessage();
+ }
+ BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, message, null);
allRecords.add(err);
indexNumber++;
}
@@ -512,8 +519,21 @@ public static List handleBulkDetokenizeBatchExcept
}
} else {
int indexNumber = batchNumber * batchSize;
+ String message = null;
+ if (cause != null && cause.getMessage() != null){
+ message = cause.getMessage();
+ }
+ if (cause != null && cause.getLocalizedMessage() !=null) {
+ message = cause.getLocalizedMessage();
+ }
+ if (cause != null && cause.getCause() !=null) {
+ message = cause.getCause().toString();
+ }
+ if (message == null || message.isEmpty() || message.trim().isEmpty()){
+ message = ex.getMessage();
+ }
for (int j = 0; j < batch.getTokens().get().size(); j++) {
- BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null);
+ BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, message, null);
allRecords.add(err);
indexNumber++;
}
@@ -712,6 +732,10 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo
int recordsSize = record.size();
for (int index = 0; index < recordsSize; index++) {
V1RecordResponseObject current = record.get(index);
+ String reqID = null;
+ if(current.getError().isPresent()){
+ reqID = extractRequestId(headers);
+ }
records.add(new BulkInsertResponseRecord(
indexNumber,
current.getTableName().orElse(null),
@@ -720,7 +744,7 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo
current.getHashedData().orElse(null),
current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200),
current.getError().orElse(null),
- null));
+ reqID));
indexNumber++;
}
formattedResponse = new BulkInsertResponse(records);
@@ -736,15 +760,27 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni
int recordsSize = record.size();
for (int index = 0; index < recordsSize; index++) {
V1FlowDetokenizeResponseObject current = record.get(index);
+ Map data = null;
+ if(current.getMetadata().isPresent()){
+ data = current.getMetadata().get();
+ if (data.containsKey("skyflowID")) {
+ Object value = data.remove("skyflowID");
+ data.put("skyflowId", value);
+ }
+ }
+ String reqID = null;
+ if(current.getError().isPresent()){
+ reqID = extractRequestId(headers);
+ }
records.add(new BulkDetokenizeResponseRecord(
indexNumber,
current.getToken().orElse(null),
current.getValue().orElse(null),
current.getTokenGroupName().orElse(null),
- current.getMetadata().orElse(null),
+ data,
current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200),
current.getError().orElse(null),
- null));
+ reqID));
indexNumber++;
}
return new BulkDetokenizeResponse(records);
diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java
index 07ed1f47..a3aac9ba 100644
--- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java
+++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java
@@ -47,10 +47,10 @@
import com.skyflow.vault.data.BulkInsertResponseRecord;
import com.skyflow.vault.data.BulkTokenizeRequest;
import com.skyflow.vault.data.BulkTokenizeResponse;
+import com.skyflow.vault.data.BulkDetokenizeOptions;
+import com.skyflow.vault.data.BulkInsertOptions;
import com.skyflow.vault.data.DeleteTokensOptions;
-import com.skyflow.vault.data.DetokenizeOptions;
import com.skyflow.vault.data.ErrorRecord;
-import com.skyflow.vault.data.InsertOptions;
import com.skyflow.vault.data.InsertRequestRecord;
import com.skyflow.vault.data.RequestContext;
import com.skyflow.vault.data.RequestInterceptor;
@@ -96,7 +96,7 @@ public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws Sky
return bulkInsert(insertRequest, null);
}
- public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException {
+ public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException {
LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog());
try {
LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog());
@@ -126,7 +126,7 @@ public CompletableFuture bulkInsertAsync(BulkInsertRequest i
return bulkInsertAsync(insertRequest, null);
}
- public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException {
+ public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException {
LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog());
try {
LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog());
@@ -164,7 +164,7 @@ public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeReq
return bulkDetokenize(detokenizeRequest, null);
}
- public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException {
+ public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException {
LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog());
try {
LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog());
@@ -191,7 +191,7 @@ public CompletableFuture bulkDetokenizeAsync(BulkDetoken
return bulkDetokenizeAsync(detokenizeRequest, null);
}
- public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException {
+ public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException {
LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog());
ExecutorService executor = null;
try {
diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java
new file mode 100644
index 00000000..12d57ca5
--- /dev/null
+++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java
@@ -0,0 +1,31 @@
+package com.skyflow.vault.data;
+
+// Bulk counterpart of DetokenizeOptions. Carries no extra state today; the interceptor
+// field is inherited.
+public class BulkDetokenizeOptions extends DetokenizeOptions {
+
+ protected BulkDetokenizeOptions(Builder builder) {
+ super(builder);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends DetokenizeOptions.Builder {
+
+ private Builder() {
+ }
+
+ @Override
+ public Builder interceptor(RequestInterceptor interceptor) {
+ super.interceptor(interceptor);
+ return this;
+ }
+
+ @Override
+ public BulkDetokenizeOptions build() {
+ return new BulkDetokenizeOptions(this);
+ }
+ }
+}
diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java
new file mode 100644
index 00000000..6dfea493
--- /dev/null
+++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java
@@ -0,0 +1,31 @@
+package com.skyflow.vault.data;
+
+// Bulk counterpart of InsertOptions. Carries no extra state today; the interceptor
+// field is inherited.
+public class BulkInsertOptions extends InsertOptions {
+
+ protected BulkInsertOptions(Builder builder) {
+ super(builder);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends InsertOptions.Builder {
+
+ private Builder() {
+ }
+
+ @Override
+ public Builder interceptor(RequestInterceptor interceptor) {
+ super.interceptor(interceptor);
+ return this;
+ }
+
+ @Override
+ public BulkInsertOptions build() {
+ return new BulkInsertOptions(this);
+ }
+ }
+}
diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java
index 267bd9bd..ddc18bdf 100644
--- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java
+++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java
@@ -1,9 +1,9 @@
package com.skyflow.vault.data;
-public final class DetokenizeOptions {
+public class DetokenizeOptions {
private final RequestInterceptor interceptor;
- private DetokenizeOptions(Builder builder) {
+ protected DetokenizeOptions(Builder builder) {
this.interceptor = builder.interceptor;
}
@@ -15,9 +15,12 @@ public static Builder builder() {
return new Builder();
}
- public static final class Builder {
+ public static class Builder {
private RequestInterceptor interceptor;
+ protected Builder() {
+ }
+
public Builder interceptor(RequestInterceptor interceptor) {
this.interceptor = interceptor;
return this;
diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java
index 73262b98..a3d5aa79 100644
--- a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java
+++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java
@@ -1,9 +1,9 @@
package com.skyflow.vault.data;
-public final class InsertOptions {
+public class InsertOptions {
private final RequestInterceptor interceptor;
- private InsertOptions(Builder builder) {
+ protected InsertOptions(Builder builder) {
this.interceptor = builder.interceptor;
}
@@ -15,9 +15,12 @@ public static Builder builder() {
return new Builder();
}
- public static final class Builder {
+ public static class Builder {
private RequestInterceptor interceptor;
+ protected Builder() {
+ }
+
public Builder interceptor(RequestInterceptor interceptor) {
this.interceptor = interceptor;
return this;
diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java
index 30697f12..d220084a 100644
--- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java
+++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java
@@ -781,6 +781,37 @@ public void testHandleBulkInsertBatchException_genericExceptionHasNoRequestId()
Assert.assertEquals("boom", records.get(0).getError());
}
+ @Test
+ public void testHandleBulkInsertBatchException_nonApiCauseUsesCauseMessage() {
+ // Cause is non-null but not an ApiClientApiException: the message ladder should
+ // pick up the cause's own message rather than the outer wrapper's.
+ RuntimeException ex = new RuntimeException("wrapper", new IllegalStateException("inner boom"));
+ List batch = Collections.singletonList(
+ V1InsertRecordData.builder().data(new HashMap<>()).build());
+
+ List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50);
+
+ Assert.assertEquals(1, records.size());
+ Assert.assertEquals(500, records.get(0).getHttpCode());
+ Assert.assertEquals("inner boom", records.get(0).getError());
+ }
+
+ @Test
+ public void testHandleBulkInsertBatchException_nonApiCauseWithNestedCauseUsesNestedToString() {
+ // When the cause itself wraps another throwable, the ladder resolves the message
+ // down to the nested cause's toString().
+ RuntimeException ex = new RuntimeException("wrapper",
+ new IllegalStateException("inner boom", new IllegalArgumentException("root cause")));
+ List batch = Collections.singletonList(
+ V1InsertRecordData.builder().data(new HashMap<>()).build());
+
+ List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50);
+
+ Assert.assertEquals(1, records.size());
+ Assert.assertEquals(500, records.get(0).getHttpCode());
+ Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError());
+ }
+
// ── createInsertErrorRecord / createDetokenizeErrorRecord branch coverage ─
@Test
@@ -1281,6 +1312,24 @@ public void testHandleBulkDetokenizeBatchException_genericException() {
Assert.assertEquals("boom", errors.get(0).getError());
}
+ @Test
+ public void testHandleBulkDetokenizeBatchException_nonApiCauseUsesCauseMessage() {
+ // Cause is non-null but not an ApiClientApiException: the message ladder resolves the
+ // nested cause's toString() rather than the outer wrapper's message.
+ RuntimeException ex = new RuntimeException("wrapper",
+ new IllegalStateException("inner boom", new IllegalArgumentException("root cause")));
+ V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+
+ List records = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50);
+
+ Assert.assertEquals(1, records.size());
+ Assert.assertEquals(500, records.get(0).getHttpCode());
+ Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError());
+ }
+
// ── handleBulkDeleteTokensBatchException ──────────────────────────────────
@Test
@@ -1341,6 +1390,153 @@ public void testHandleBulkDeleteTokensBatchException_genericException() {
Assert.assertEquals("t1", errors.get(0).getToken());
}
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_errorFieldAsObjectUsesHelper() {
+ // Structured error envelope {"error": {message, httpCode}} → parsed per token via the helper.
+ Map errorObject = new HashMap<>();
+ errorObject.put("message", "vault not found");
+ errorObject.put("httpCode", 404);
+ Map body = new HashMap<>();
+ body.put("error", errorObject);
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 404, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(Integer.valueOf(404), errors.get(0).getHttpCode());
+ Assert.assertEquals("vault not found", errors.get(0).getError());
+ Assert.assertEquals("t1", errors.get(0).getToken());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_errorFieldNeitherMapNorStringUsesApiMessage() {
+ Map body = new HashMap<>();
+ body.put("error", 500);
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 500, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("delete failed", errors.get(0).getError());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_bodyWithNeitherTokensNorErrorKey() {
+ // A map body matching neither branch falls through to the batch-wide fallback.
+ Map body = new HashMap<>();
+ body.put("unexpected", "shape");
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 503, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Arrays.asList("t1", "t2"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 1, 50);
+
+ Assert.assertEquals(2, errors.size());
+ Assert.assertEquals(Integer.valueOf(503), errors.get(0).getHttpCode());
+ Assert.assertEquals("delete failed", errors.get(0).getError());
+ // startIndex = batchNumber * batchSize = 50
+ Assert.assertEquals(50, errors.get(0).getIndex());
+ Assert.assertEquals(51, errors.get(1).getIndex());
+ Assert.assertEquals("t2", errors.get(1).getToken());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_tokensNotAListFallsBackToBatchWideError() {
+ Map body = new HashMap<>();
+ body.put("tokens", "not-a-list");
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("delete failed", errors.get(0).getError());
+ Assert.assertEquals("t1", errors.get(0).getToken());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_nonMapEntriesAreSkipped() {
+ Map body = new HashMap<>();
+ body.put("tokens", Arrays.asList("not-a-map", null));
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ // No entry parsed, so the batch-wide fallback fires instead.
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("delete failed", errors.get(0).getError());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_recordEchoesValueAndReadsHttpCodeAndMessage() {
+ // createDeleteTokensErrorRecord: http_code key, "message" key, and an echoed "value" token.
+ Map tokenMap = new HashMap<>();
+ tokenMap.put("http_code", 409);
+ tokenMap.put("message", "already deleted");
+ tokenMap.put("value", "echoed-token");
+ Map body = new HashMap<>();
+ body.put("tokens", Collections.singletonList(tokenMap));
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 409, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("requested-token"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(Integer.valueOf(409), errors.get(0).getHttpCode());
+ Assert.assertEquals("already deleted", errors.get(0).getError());
+ // the echoed "value" wins over the token from the request batch
+ Assert.assertEquals("echoed-token", errors.get(0).getToken());
+ }
+
+ @Test
+ public void testHandleBulkDeleteTokensBatchException_recordUsesStatusCodeAndUnknownError() {
+ // createDeleteTokensErrorRecord: statusCode key and the no-error/no-message fallback.
+ Map tokenMap = new HashMap<>();
+ tokenMap.put("statusCode", 410);
+ Map body = new HashMap<>();
+ body.put("tokens", Collections.singletonList(tokenMap));
+ ApiClientApiException apiEx = new ApiClientApiException("delete failed", 410, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder()
+ .vaultId("vault123")
+ .tokens(Collections.singletonList("t1"))
+ .build();
+ List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(Integer.valueOf(410), errors.get(0).getHttpCode());
+ Assert.assertEquals("Unknown error", errors.get(0).getError());
+ // no echoed value, so the requested token is reported
+ Assert.assertEquals("t1", errors.get(0).getToken());
+ }
+
// ── handleBulkTokenizeBatchException ───────────────────────────────────────
private static List tokenizeBatch(String value, String... groups) {
@@ -1416,6 +1612,75 @@ public void testHandleBulkTokenizeBatchException_noTokenGroupsStillReportsOneEnt
Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError());
}
+ @Test
+ public void testHandleBulkTokenizeBatchException_errorBodyWithResponseArrayRebuildsRecords() {
+ // A 4xx whose body echoes the per-row "response" array is rebuilt via tokenizeRecordsFromErrorBody
+ // rather than summarized by the bare status code.
+ Map tokenRow = new HashMap<>();
+ tokenRow.put("tokenGroupName", "group1");
+ tokenRow.put("error", "BYOT token should contain one token group");
+ tokenRow.put("httpCode", 400);
+ Map responseRow = new HashMap<>();
+ responseRow.put("value", "v1");
+ responseRow.put("tokens", Collections.singletonList(tokenRow));
+ Map body = new HashMap<>();
+ body.put("response", Collections.singletonList(responseRow));
+ ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ List errors = Utils.handleBulkTokenizeBatchException(
+ wrapper, tokenizeBatch("v1", "group1"), 0);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("v1", errors.get(0).getValue());
+ Assert.assertEquals(1, errors.get(0).getTokens().size());
+ Assert.assertEquals("group1", errors.get(0).getTokens().get(0).getTokenGroupName());
+ Assert.assertEquals("BYOT token should contain one token group",
+ errors.get(0).getTokens().get(0).getError());
+ Assert.assertEquals(Integer.valueOf(400), errors.get(0).getTokens().get(0).getHttpCode());
+ }
+
+ @Test
+ public void testHandleBulkTokenizeBatchException_errorFieldAsObjectUsesStructuredMessage() {
+ // extractBatchErrorMessage reads {"error": {message}} when the body has no per-row response.
+ Map errorObject = new HashMap<>();
+ errorObject.put("message", "vault not found");
+ Map body = new HashMap<>();
+ body.put("error", errorObject);
+ ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 404, body);
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ List errors = Utils.handleBulkTokenizeBatchException(
+ wrapper, tokenizeBatch("v1", "group1"), 0);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("vault not found", errors.get(0).getTokens().get(0).getError());
+ Assert.assertEquals(Integer.valueOf(404), errors.get(0).getTokens().get(0).getHttpCode());
+ }
+
+ @Test
+ public void testHandleBulkTokenizeBatchException_nonMapBodyUsesApiMessage() {
+ // Body is not a map, so extractBatchErrorMessage falls back to the exception's own message.
+ ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 500, "raw string body");
+ RuntimeException wrapper = new RuntimeException(apiEx);
+
+ List errors = Utils.handleBulkTokenizeBatchException(
+ wrapper, tokenizeBatch("v1", "group1"), 0);
+
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals("tokenize failed", errors.get(0).getTokens().get(0).getError());
+ Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode());
+ }
+
+ @Test
+ public void testHandleBulkTokenizeBatchException_nullBatchReturnsEmpty() {
+ RuntimeException ex = new RuntimeException("boom");
+
+ List errors = Utils.handleBulkTokenizeBatchException(ex, null, 0);
+
+ Assert.assertTrue(errors.isEmpty());
+ }
+
// ── formatBulkInsertResponse ───────────────────────────────────────────────
@Test
diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java
index b37d809a..d9b7d30f 100644
--- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java
+++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java
@@ -34,9 +34,9 @@
import com.skyflow.vault.data.BulkInsertResponseRecord;
import com.skyflow.vault.data.BulkTokenizeRequest;
import com.skyflow.vault.data.BulkTokenizeResponse;
+import com.skyflow.vault.data.BulkDetokenizeOptions;
+import com.skyflow.vault.data.BulkInsertOptions;
import com.skyflow.vault.data.DeleteTokensOptions;
-import com.skyflow.vault.data.DetokenizeOptions;
-import com.skyflow.vault.data.InsertOptions;
import com.skyflow.vault.data.InsertRequestRecord;
import com.skyflow.vault.data.RequestInterceptor;
import com.skyflow.vault.data.TokenGroupRedactions;
@@ -196,7 +196,7 @@ public void testBulkInsert_interceptorAddsCustomHeader() throws Exception {
BulkInsertRequest request = BulkInsertRequest.builder().records(records).build();
RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountId, "acct-123");
- InsertOptions options = InsertOptions.builder().interceptor(interceptor).build();
+ BulkInsertOptions options = BulkInsertOptions.builder().interceptor(interceptor).build();
controller.bulkInsert(request, options);
@@ -931,7 +931,7 @@ public void testBulkInsert_interceptorInvokedOncePerBatchWithDistinctContext() t
.build();
CountingInterceptor interceptor = new CountingInterceptor();
- controller.bulkInsert(request, InsertOptions.builder().interceptor(interceptor).build());
+ controller.bulkInsert(request, BulkInsertOptions.builder().interceptor(interceptor).build());
ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class);
Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(any(), captor.capture());
@@ -1000,7 +1000,7 @@ public void testBulkDetokenize_interceptorInvokedOncePerBatchWithDistinctContext
BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(multiBatchTokens()).build();
CountingInterceptor interceptor = new CountingInterceptor();
- controller.bulkDetokenize(request, DetokenizeOptions.builder().interceptor(interceptor).build());
+ controller.bulkDetokenize(request, BulkDetokenizeOptions.builder().interceptor(interceptor).build());
ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class);
Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(any(), captor.capture());
diff --git a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java
index f88c7a39..6f16c1dd 100644
--- a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java
+++ b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java
@@ -5,7 +5,8 @@
/**
* Tests for the single-field options/builder classes: {@link InsertOptions},
- * {@link DetokenizeOptions}, {@link TokenizeOptions} and {@link DeleteTokensOptions}.
+ * {@link DetokenizeOptions}, {@link TokenizeOptions} and {@link DeleteTokensOptions},
+ * plus the bulk specializations {@link BulkInsertOptions} and {@link BulkDetokenizeOptions}.
* Each class simply wraps a {@link RequestInterceptor} with no validation.
*/
public class OptionsTests {
@@ -41,6 +42,37 @@ public void testDetokenizeOptions_withoutInterceptor() {
Assert.assertNull(options.getInterceptor());
}
+ // ── BulkInsertOptions ────────────────────────────────────────────────────
+
+ @Test
+ public void testBulkInsertOptions_withInterceptor() {
+ BulkInsertOptions options = BulkInsertOptions.builder().interceptor(INTERCEPTOR).build();
+ Assert.assertSame(INTERCEPTOR, options.getInterceptor());
+ // BulkInsertOptions is a specialization of InsertOptions, so it flows anywhere the base does
+ Assert.assertTrue(options instanceof InsertOptions);
+ }
+
+ @Test
+ public void testBulkInsertOptions_withoutInterceptor() {
+ BulkInsertOptions options = BulkInsertOptions.builder().build();
+ Assert.assertNull(options.getInterceptor());
+ }
+
+ // ── BulkDetokenizeOptions ────────────────────────────────────────────────
+
+ @Test
+ public void testBulkDetokenizeOptions_withInterceptor() {
+ BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().interceptor(INTERCEPTOR).build();
+ Assert.assertSame(INTERCEPTOR, options.getInterceptor());
+ Assert.assertTrue(options instanceof DetokenizeOptions);
+ }
+
+ @Test
+ public void testBulkDetokenizeOptions_withoutInterceptor() {
+ BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().build();
+ Assert.assertNull(options.getInterceptor());
+ }
+
// ── TokenizeOptions ──────────────────────────────────────────────────────
@Test