From 325bfd990ab3406a473ed05de3202723262641d1 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 16:49:45 +0530 Subject: [PATCH 01/15] SK-3061: revert fields->tokens rename, restore data field on bulkInsert response The flowvault SDK renamed the API's `tokens` response key to `fields`, so callers had to use getFields() instead of the API-matching getTokens(). Separately, the API's `data` field was silently dropped from the response entirely. Both were flagged in Slack by a customer comparing SDK output to the raw API contract. - InsertResponseRecord/BulkInsertResponseRecord: add tokens/getTokens() matching the API. getFields() stays as a @Deprecated alias that logs a warning and delegates to getTokens() - existing callers keep working unchanged. - Add data/getData(), wired from V1RecordResponseObject.getData() in Utils.formatBulkInsertResponse (the wire type already carried it; nothing read it). - Old constructor overloads (without `data`) are kept, also @Deprecated, rather than changing existing constructor signatures - this avoids a binary/source-incompatible change against the japicmp baseline in flowvault/pom.xml. - detokenize/deleteTokens are untouched; they never had this issue. - Updated README and tests accordingly, including a dedicated test for the deprecated constructor + getFields() alias. Co-Authored-By: Claude Sonnet 5 --- .../main/java/com/skyflow/logs/InfoLogs.java | 3 +- flowvault/README.md | 12 ++++-- .../main/java/com/skyflow/utils/Utils.java | 9 ++-- .../vault/data/BulkInsertResponseRecord.java | 15 ++++++- .../vault/data/InsertResponseRecord.java | 36 ++++++++++++++-- .../java/com/skyflow/utils/UtilsTests.java | 6 +++ .../controller/VaultControllerTests.java | 8 ++-- .../skyflow/vault/data/BulkResponseTests.java | 29 +++++++------ .../vault/data/ResponseComponentTests.java | 41 +++++++++++++++---- 9 files changed, 122 insertions(+), 37 deletions(-) diff --git a/common/src/main/java/com/skyflow/logs/InfoLogs.java b/common/src/main/java/com/skyflow/logs/InfoLogs.java index 6d6a8211..b4045229 100644 --- a/common/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/common/src/main/java/com/skyflow/logs/InfoLogs.java @@ -115,7 +115,8 @@ public enum InfoLogs { DEPRECATED_UPDATE_LOG_LEVEL("[DEPRECATED] Method 'updateLogLevel()' is deprecated and will be removed in an upcoming release. Use 'setLogLevel()' instead."), DEPRECATED_CREDENTIAL_CLIENT_ID("[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), DEPRECATED_CREDENTIAL_KEY_ID("[DEPRECATED] Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), - DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead.") + DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."), + DEPRECATED_INSERT_FIELDS_GETTER("[DEPRECATED] Method 'getFields()' is deprecated and will be removed in an upcoming release. Use 'getTokens()' instead.") ; diff --git a/flowvault/README.md b/flowvault/README.md index 442a1fbf..05cb32e0 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -435,7 +435,8 @@ Sample response: "requestId": null, "tableName": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "fields": { "card_number": "5484-7829-1702-9110", "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" }, + "tokens": { "card_number": "5484-7829-1702-9110", "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" }, + "data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" }, "hashedData": null, "httpCode": 200, "error": null @@ -445,7 +446,8 @@ Sample response: "requestId": "a1b2c3d4-...", "tableName": "table2", "skyflowId": null, - "fields": null, + "tokens": null, + "data": null, "hashedData": null, "httpCode": 400, "error": "Insert failed. Column email is invalid." @@ -454,7 +456,9 @@ Sample response: } ``` -Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getFields()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. +Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +> **Deprecation notice:** `getFields()` is deprecated in favor of `getTokens()` — it is kept only for backward compatibility and will be removed in a future release. Update call sites to `getTokens()`. Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequestRecord`s worth resubmitting — see [Retrying the failed records](#retrying-the-failed-records). @@ -721,7 +725,7 @@ Every bulk response exposes `getSummary()` and `getRecords()`. The records list | `getError()` | failures only | Error message for this item. `null` means this item succeeded. | | `getRequestId()` | failures only | The `x-request-id` of the batch this item was in — quote it in support escalations. Items from the same batch share one id. | -The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getFields()` for insert, `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. +The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getTokens()`/`getData()` for insert (`getFields()` is a deprecated alias for `getTokens()`), `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. Summaries per operation: diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index d6011dec..c607ace3 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -351,7 +351,7 @@ public static BulkInsertResponseRecord createInsertErrorRecord(Map handleBulkInsertBatchException( } else { errorMessage = apiException.getMessage(); } - err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), errorMessage, requestId); } allRecords.add(err); @@ -432,7 +432,7 @@ public static List handleBulkInsertBatchException( if (allRecords.isEmpty()) { for (int j = 0; j < batch.size(); j++) { - allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); + allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); indexNumber++; } } @@ -452,7 +452,7 @@ public static List handleBulkInsertBatchException( if (message == null || message.isEmpty() || message.trim().isEmpty()){ message = ex.getMessage(); } - BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, message, null); + BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -761,6 +761,7 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo current.getTableName().orElse(null), current.getSkyflowId().orElse(null), current.getTokens().orElse(null), + current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java index 1ea6b757..84f105e9 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -10,10 +10,21 @@ public class BulkInsertResponseRecord extends InsertResponseRecord { private final int index; private final String requestId; + /** + * @deprecated Use {@link #BulkInsertResponseRecord(int, String, String, Map, Map, Map, int, String, String)} + * instead, which also lets you populate {@code data}. This overload always leaves {@code data} null. + */ + @Deprecated(since = "1.0.2", forRemoval = true) public BulkInsertResponseRecord(int index, String tableName, String skyflowId, - Map fields, Map hashedData, + Map tokens, Map hashedData, int httpCode, String error, String requestId) { - super(tableName, skyflowId, fields, hashedData, httpCode, error); + this(index, tableName, skyflowId, tokens, null, hashedData, httpCode, error, requestId); + } + + public BulkInsertResponseRecord(int index, String tableName, String skyflowId, + Map tokens, Map data, Map hashedData, + int httpCode, String error, String requestId) { + super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); this.index = index; this.requestId = requestId; } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java index 442d3749..02635a18 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -1,20 +1,35 @@ package com.skyflow.vault.data; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.logger.LogUtil; + import java.util.Map; public class InsertResponseRecord { private final String tableName; private final String skyflowId; - private final Map fields; + private final Map tokens; + private final Map data; private final Map hashedData; private final int httpCode; private final String error; - public InsertResponseRecord(String tableName, String skyflowId, Map fields, + /** + * @deprecated Use {@link #InsertResponseRecord(String, String, Map, Map, Map, int, String)} instead, + * which also lets you populate {@code data}. This overload always leaves {@code data} null. + */ + @Deprecated(since = "1.0.2", forRemoval = true) + public InsertResponseRecord(String tableName, String skyflowId, Map tokens, Map hashedData, int httpCode, String error) { + this(tableName, skyflowId, tokens, null, hashedData, httpCode, error); + } + + public InsertResponseRecord(String tableName, String skyflowId, Map tokens, + Map data, Map hashedData, int httpCode, String error) { this.tableName = tableName; this.skyflowId = skyflowId; - this.fields = fields; + this.tokens = tokens; + this.data = data; this.hashedData = hashedData; this.httpCode = httpCode; this.error = error; @@ -28,8 +43,21 @@ public String getSkyflowId() { return skyflowId; } + public Map getTokens() { + return tokens; + } + + /** + * @deprecated Response key 'fields' is deprecated. Use {@link #getTokens()} instead. + */ + @Deprecated(since = "1.0.2", forRemoval = true) public Map getFields() { - return fields; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_INSERT_FIELDS_GETTER.getLog()); + return getTokens(); + } + + public Map getData() { + return data; } public Map getHashedData() { diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 4e034bfd..ae41587e 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -1687,9 +1687,12 @@ public void testHandleBulkTokenizeBatchException_nullBatchReturnsEmpty() { public void testFormatBulkInsertResponse_success() { Map tokens = new HashMap<>(); tokens.put("name", "tok-abc"); + Map data = new HashMap<>(); + data.put("name", "john"); V1RecordResponseObject record = V1RecordResponseObject.builder() .skyflowId("sky-id-1") .tokens(tokens) + .data(data) .build(); V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); @@ -1698,7 +1701,10 @@ public void testFormatBulkInsertResponse_success() { Assert.assertEquals(1, result.getRecords().size()); BulkInsertResponseRecord inserted = result.getRecords().get(0); Assert.assertEquals("sky-id-1", inserted.getSkyflowId()); + Assert.assertEquals(tokens, inserted.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. Assert.assertEquals(tokens, inserted.getFields()); + Assert.assertEquals(data, inserted.getData()); Assert.assertEquals(0, inserted.getIndex()); Assert.assertEquals(200, inserted.getHttpCode()); Assert.assertNull(inserted.getError()); 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 72918e46..63504d4b 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -704,9 +704,11 @@ public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { Assert.assertEquals(1, response.getRecords().size()); BulkInsertResponseRecord inserted = response.getRecords().get(0); - Assert.assertNotNull(inserted.getFields()); - // The token map is surfaced verbatim as `fields`, so a List token shape survives intact. - Object field1Tokens = inserted.getFields().get("field1"); + Assert.assertNotNull(inserted.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(inserted.getTokens(), inserted.getFields()); + // The token map is surfaced verbatim as `tokens`, so a List token shape survives intact. + Object field1Tokens = inserted.getTokens().get("field1"); Assert.assertTrue(field1Tokens instanceof List); Assert.assertEquals(1, ((List) field1Tokens).size()); Map field1Token = (Map) ((List) field1Tokens).get(0); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java index fc6b4e65..cd347df8 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -40,8 +40,8 @@ public void testBulkInsertResponse_oneArgConstructorLeavesSummaryAndRetryDepende @Test public void testBulkInsertResponse_twoArgConstructorComputesSummary() { List records = Arrays.asList( - new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null), - new BulkInsertResponseRecord(1, null, null, null, null, 400, "failed", null)); + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, null, 200, null, null), + new BulkInsertResponseRecord(1, null, null, null, null, null, 400, "failed", null)); List originalPayload = new ArrayList<>(Arrays.asList( BulkInsertRequestRecord.builder().tableName("table1").build(), BulkInsertRequestRecord.builder().tableName("table1").build())); @@ -56,13 +56,15 @@ public void testBulkInsertResponse_twoArgConstructorComputesSummary() { @Test public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { - Map fields = new HashMap<>(); - fields.put("name", "token-name"); + Map tokens = new HashMap<>(); + tokens.put("name", "token-name"); + Map data = new HashMap<>(); + data.put("name", "john"); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-name"); BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 7, "table1", "id-1", fields, hashedData, 200, null, null); + 7, "table1", "id-1", tokens, data, hashedData, 200, null, null); BulkInsertResponse response = new BulkInsertResponse(Collections.singletonList(record)); @@ -70,7 +72,10 @@ public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { Assert.assertEquals(7, actual.getIndex()); Assert.assertEquals("table1", actual.getTableName()); Assert.assertEquals("id-1", actual.getSkyflowId()); - Assert.assertEquals(fields, actual.getFields()); + Assert.assertEquals(tokens, actual.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(tokens, actual.getFields()); + Assert.assertEquals(data, actual.getData()); Assert.assertEquals(hashedData, actual.getHashedData()); Assert.assertEquals(200, actual.getHttpCode()); Assert.assertNull(actual.getError()); @@ -86,10 +91,10 @@ public void testBulkInsertResponse_getRecordsToRetryFiltersRetryableStatusCodesO Arrays.asList(record0, record1, record2, record3)); List records = Arrays.asList( - new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null), // retryable (lower bound) - new BulkInsertResponseRecord(1, null, null, null, null, 400, "bad request", null), // not retryable - new BulkInsertResponseRecord(2, null, null, null, null, 599, "server error", null), // retryable (upper bound) - new BulkInsertResponseRecord(3, null, null, null, null, 529, "special case", null)); // explicitly excluded + new BulkInsertResponseRecord(0, null, null, null, null, null, 500, "server error", null), // retryable (lower bound) + new BulkInsertResponseRecord(1, null, null, null, null, null, 400, "bad request", null), // not retryable + new BulkInsertResponseRecord(2, null, null, null, null, null, 599, "server error", null), // retryable (upper bound) + new BulkInsertResponseRecord(3, null, null, null, null, null, 529, "special case", null)); // explicitly excluded BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); @@ -111,7 +116,7 @@ public void testBulkInsertResponse_toStringNotNull() { @Test public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInternals() { List records = Collections.singletonList( - new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null)); + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, null, 200, null, null)); List originalPayload = new ArrayList( Collections.singletonList(BulkInsertRequestRecord.builder().tableName("table1").build())); @@ -133,7 +138,7 @@ public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInte public void testBulkInsertResponse_getRecordsToRetryOnPerBatchResponseDoesNotThrow() { // The 1-arg constructor leaves originalPayload null. A 5xx record must not NPE here. List records = Collections.singletonList( - new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null)); + new BulkInsertResponseRecord(0, null, null, null, null, null, 500, "server error", null)); BulkInsertResponse response = new BulkInsertResponse(records); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 273c4c85..780711e1 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -26,46 +26,73 @@ public class ResponseComponentTests { @Test public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { - Map fields = new HashMap<>(); - fields.put("name", "tok-1"); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-1"); + Map data = new HashMap<>(); + data.put("name", "john"); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-1"); BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 2, "persons", "skyflow-id-1", fields, hashedData, 200, null, null); + 2, "persons", "skyflow-id-1", tokens, data, hashedData, 200, null, null); Assert.assertEquals(2, record.getIndex()); Assert.assertEquals("persons", record.getTableName()); Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); - Assert.assertEquals(fields, record.getFields()); + Assert.assertEquals(tokens, record.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(tokens, record.getFields()); + Assert.assertEquals(data, record.getData()); Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); } + @Test + @SuppressWarnings("deprecation") + public void testBulkInsertResponseRecord_deprecatedConstructorAndGetFieldsStillWork() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-1"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + // The pre-existing (data-less) constructor overload and getFields() are both deprecated, + // but must keep working unchanged for callers who haven't migrated yet. + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 2, "persons", "skyflow-id-1", tokens, hashedData, 200, null, null); + + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(tokens, record.getFields()); + Assert.assertNull(record.getData()); + Assert.assertEquals(hashedData, record.getHashedData()); + } + @Test public void testBulkInsertResponseRecord_errorCase() { BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 3, null, null, null, null, 500, "Internal Server Error", null); + 3, null, null, null, null, null, 500, "Internal Server Error", null); Assert.assertEquals(3, record.getIndex()); Assert.assertEquals(500, record.getHttpCode()); Assert.assertEquals("Internal Server Error", record.getError()); Assert.assertNull(record.getTableName()); Assert.assertNull(record.getSkyflowId()); + Assert.assertNull(record.getTokens()); Assert.assertNull(record.getFields()); + Assert.assertNull(record.getData()); Assert.assertNull(record.getHashedData()); } @Test public void testBulkInsertResponseRecord_toStringSerializesNulls() { BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 0, "persons", "skyflow-id-2", null, null, 200, null, null); + 0, "persons", "skyflow-id-2", null, null, null, 200, null, null); String json = record.toString(); Assert.assertNotNull(json); Assert.assertTrue(json.contains("skyflow-id-2")); Assert.assertTrue(json.contains("\"index\":0")); - Assert.assertTrue(json.contains("\"fields\":null")); + Assert.assertTrue(json.contains("\"tokens\":null")); + Assert.assertTrue(json.contains("\"data\":null")); } // ── BulkSummary ────────────────────────────────────────────────────────── From ff3d69d7110c373a7d49a4b300b8c3f5581b51d2 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:20:35 +0530 Subject: [PATCH 02/15] SK-3061: fix flowvault README inaccuracies, drop misleading sample, add response iteration to samples README (flowvault/README.md): - Version snippets said 1.0.0; pom.xml is 1.0.1. - CustomHeaderKey enum names were wrong (PascalCase vs actual SCREAMING_SNAKE_CASE) - the sample code block did not compile. - "vault() takes no arguments, use one client per vault" was false: Skyflow.vault(String vaultId) exists and is tested for multi-vault use on one client. Documented it. - "updateType accepts UPDATE (the default)" overstated what the SDK does - it omits the field when unset rather than sending "UPDATE"; reworded to say so. - getHttpStatus() example used "BAD_REQUEST"; the actual hardcoded validation-error string is "Bad Request". Samples: - Deleted BearerTokenExpiryExample.java: despite its name, it never touches BearerToken or Token.isExpired() at all - it's a generic "retry once on 401" wrapper around bulkDetokenize, redundant with both BearerTokenGenerationExample's real expiry-check pattern and the README's own retry guidance. - Rewrote samples/README.md: it referenced DetokenizeExample.java, InsertExample.java, GetByIdExample.java etc. - files that don't exist anywhere in this module (leftover boilerplate from a different samples layout). Replaced with an accurate index of the actual serviceaccount/ and vault/ samples plus correct Maven run instructions. - Added response iteration (summary + per-record/per-token walk + retry) to BulkInsertSync/Async, BulkMultiTableInsertSync/Async, BulkDetokenizeSync/Async, and CustomHeaderExample - they previously only printed the raw response object. BulkTokenizeSync/Async and BulkDeleteTokensSync/Async already had this and are untouched. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 22 +-- flowvault/samples/README.md | 141 ++++++++---------- .../BearerTokenExpiryExample.java | 85 ----------- .../example/vault/BulkDetokenizeAsync.java | 38 ++++- .../com/example/vault/BulkDetokenizeSync.java | 45 +++++- .../com/example/vault/BulkInsertAsync.java | 33 +++- .../com/example/vault/BulkInsertSync.java | 39 ++++- .../vault/BulkMultiTableInsertAsync.java | 33 +++- .../vault/BulkMultiTableInsertSync.java | 36 ++++- .../example/vault/CustomHeaderExample.java | 25 +++- 10 files changed, 297 insertions(+), 200 deletions(-) delete mode 100644 flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java diff --git a/flowvault/README.md b/flowvault/README.md index 05cb32e0..5873f74e 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -4,7 +4,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope > Meant for **Flow DB** vaults. -> **`flowvault` is a new SDK, versioned independently of `skyvault`.** It starts at `1.0.0` while `skyvault` (`com.skyflow:skyflow-java`) is at `2.x`. The two artifacts have separate version lines, so a lower `flowvault` version number does not mean it is older or behind — it is a first release, not a downgrade. Upgrade each artifact on its own. +> **`flowvault` is a new SDK, versioned independently of `skyvault`.** It started at `1.0.0` while `skyvault` (`com.skyflow:skyflow-java`) is at `2.x`. The two artifacts have separate version lines, so a lower `flowvault` version number does not mean it is older or behind — it is a first release, not a downgrade. Upgrade each artifact on its own. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) [![License](https://img.shields.io/github/license/skyflowapi/skyflow-java)](https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE) @@ -58,7 +58,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope ### Gradle users ``` -implementation 'com.skyflow:skyflow-flowvault-java:1.0.0' +implementation 'com.skyflow:skyflow-flowvault-java:1.0.1' ``` ### Maven users @@ -67,7 +67,7 @@ implementation 'com.skyflow:skyflow-flowvault-java:1.0.0' com.skyflow skyflow-flowvault-java - 1.0.0 + 1.0.1 ``` @@ -97,7 +97,7 @@ Skyflow skyflowClient = Skyflow.builder() VaultController vault = skyflowClient.vault(); ``` -`flowvault`'s `vault()` takes no arguments — it always resolves to the first vault added to the builder. Use one client per vault if you need to talk to more than one. +`vault()` with no arguments returns the controller for the first vault added to the builder. To talk to more than one vault from a single client, register each with `addVaultConfig(...)` and fetch each controller by ID: `skyflowClient.vault("")`. # Authenticate @@ -249,6 +249,8 @@ public class InitFlowVaultClient { Every method throws `SkyflowException` on validation errors and returns the builder for chaining. +Once built, `skyflowClient.vault()` returns the first registered vault's controller; `skyflowClient.vault("")` returns the controller for a specific registered vault, which is how one client talks to more than one vault. + ## Timeouts and retries Each HTTP setting resolves **most specific first**: the value on `VaultConfig`, else the client-wide value on `Skyflow.builder()`, else the SDK default. Only `null` means "inherit" — an explicit `0` is a real value and overrides the level below it. @@ -346,7 +348,7 @@ Insert many records — even across different tables — in a single call. Each - `tableName` must be specified at exactly one level: either on the request (`BulkInsertRequest.builder().tableName(...)`) or on **every** record (`BulkInsertRequestRecord.builder().tableName(...)`) — not both, and not neither. - `upsert` is optional, but wherever you supply it, it must sit at the same level as `tableName`. Request-level `tableName` pairs with request-level `upsert`; record-level `tableName` pairs with per-record `upsert`. -- `UpsertOptions` requires `uniqueColumns`. `updateType` accepts `"UPDATE"` (the default) or `"REPLACE"`. +- `UpsertOptions` requires `uniqueColumns`. `updateType` accepts `"UPDATE"` or `"REPLACE"` — if omitted, the SDK sends no `updateType` at all, and the vault treats that the same as `"UPDATE"`. ### Construct a bulk insert request @@ -675,16 +677,16 @@ To include custom HTTP headers on an outgoing bulk request, pass a `RequestInter | `CustomHeaderKey` | HTTP header name | |---|---| -| `SkyflowAccountId` | `x-skyflow-account-id` | -| `SkyflowAccountName` | `x-skyflow-account-name` | -| `RequestIdHeader` | `x-request-id` | +| `SKYFLOW_ACCOUNT_ID` | `x-skyflow-account-id` | +| `SKYFLOW_ACCOUNT_NAME` | `x-skyflow-account-name` | +| `REQUEST_ID_HEADER` | `x-request-id` | ```java import com.skyflow.enums.CustomHeaderKey; import com.skyflow.vault.data.BulkInsertOptions; BulkInsertOptions options = BulkInsertOptions.builder() - .interceptor(context -> context.addHeader(CustomHeaderKey.RequestIdHeader, "")) + .interceptor(context -> context.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "")) .build(); BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest, options); @@ -809,7 +811,7 @@ vault.bulkInsertAsync(insertRequest) |---|---|---| | HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). | | Message | `getMessage()` | Human-readable description of the error. | -| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). | +| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"Bad Request"` for a client-side validation error; for API errors, whatever string the server returns). | | gRPC code | `getGrpcCode()` | gRPC status code from the server. | | Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. | | Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. | diff --git a/flowvault/samples/README.md b/flowvault/samples/README.md index 666b2186..5f62a8fc 100644 --- a/flowvault/samples/README.md +++ b/flowvault/samples/README.md @@ -1,84 +1,75 @@ -# Java SDK samples -Test the SDK by adding `VAULT-ID`, `VAULT-URL`, and `SERVICE-ACCOUNT` details in the required places for each sample. +# FlowVault Java SDK samples + +Runnable samples for the `flowvault` module (`com.skyflow:skyflow-flowvault-java`) — bulk vault +operations against a Flow DB vault, plus the shared service-account/bearer-token utilities. + +See the [flowvault README](../README.md) for the full API reference these samples exercise. ## Prerequisites -- A Skyflow account. If you don't have one, register for one on the [Try Skyflow](https://skyflow.com/try-skyflow) page. -- Java 1.8 or higher. -### Create the vault +- A Skyflow account. If you don't have one, register on the [Try Skyflow](https://skyflow.com/try-skyflow) page. +- Java 8 or higher, and Maven. + +### Create a vault + 1. In a browser, sign in to Skyflow Studio. -2. Create a vault by clicking **Create Vault** > **Start With a Template** > **Quickstart vault**. +2. Create a vault: **Create Vault** > **Start With a Template** > **Quickstart vault**. 3. Once the vault is ready, click the gear icon and select **Edit Vault Details**. -4. Note your **Vault URL** and **Vault ID** values, then click **Cancel**. You'll need these later. +4. Note the **Vault ID** and **Vault URL**. The cluster ID that samples pass to `setClusterId(...)` + is the subdomain of the vault URL — e.g. for `https://a1b2c3d4.vault.skyflowapis.com`, the + cluster ID is `a1b2c3d4`. ### Create a service account -1. In the side navigation click, **IAM** > **Service Accounts** > **New Service Account**. -2. For **Name**, enter "SDK Sample". For **Roles**, choose **Vault Editor**. -3. Click **Create**. Your browser downloads a **credentials.json** file. Keep this file secure, as You'll need it for each of the samples. + +1. In the side navigation: **IAM** > **Service Accounts** > **New Service Account**. +2. Name it, and for **Roles** choose **Vault Editor**. +3. Click **Create**. Your browser downloads a `credentials.json` — keep it secure, you'll need its + path or contents for most samples below. + +## Running a sample + +Each sample is a `public class X { public static void main(String[] args) ... }` under +`src/main/java/com/example/`. Fill in the placeholders (``, ``, +``, etc.) with real values, then compile and run with Maven from +`flowvault/samples/`: + +```bash +mvn compile +mvn org.codehaus.mojo:exec-maven-plugin:3.1.0:java -Dexec.mainClass="com.example.vault.BulkInsertSync" +``` + +Swap `-Dexec.mainClass` for the fully-qualified class name of the sample you want to run. ## The samples -### Detokenize -Detokenize a data token from the vault. Make sure the specified token is for data that exists in the vault. If you need a valid token, use [InsertExample.java](src/main/java/com/example/InsertExample.java) to insert the data, then use this data's token for detokenization. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/DetokenizeExample.java) -1. Replace **** with **VAULT ID** -2. Replace **** with **VAULT URL** -3. Replace **** with **Data Token**. -4. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**.See #Create a service account. -#### Run the sample - - javac DetokenizeExample.java - java DetokenizeExample -### Get a record by ID -Get data using skyflow id. -#### Configure -1. Replace **** with **VAULT ID** -2. Replace **** with **VAULT URL**. -3. Replace **** with **Skyflow id**. -4. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. See #Create a Service Account. -5. Replace **** with **credit_cards**. -#### Run the sample - - javac GetByIdExample.java - java GetByIdExample -### Insert data into a vault -Insert data in the vault. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/InsertExample.java) -1. Replace **** with **VAULT ID**. -2. Replace **** with **VAULT URL**. -3. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. -4. Replace **** with **credit_cards**. -5. Replace **** with **column name**. -6. Replace **** with **valid value corresponding to column name**. -#### Run the sample - - javac InsertExample.java - java InsertExample -### Invoke a connection -Skyflow Connections is a gateway service that uses Skyflow's underlying tokenization capabilities to securely connect to first-party and third-party services. This way, your infrastructure is never directly exposed to sensitive data, and you offload security and compliance requirements to Skyflow. -#### Configure -1. Replace **** with **VAULT ID**. -2. Replace **** with **VAULT URL**. -3. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. -4. Replace **** with **Connection url**. -5. Replace **** with **Path param key**. -6. Replace **** with **Path param value**. -7. Replace **** with **Query param key**. -8. Replace **** with **Query param value**. -9. Replace **** with **Request header key**. -10. Replace **** with **Request header value**. -11. Replace **** with **Request body key**. -12. Replace **** with **Request body value**. -#### Run the sample - - javac InvokeConnectionExample.java - java InvokeConnectionExample - -### Generate a service account bearer token -Generates a bearer token using a file path and content of a service account credentials file. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/TokenGenerationExample.java) -1. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE PATH**. See #Create a service account. -2. Replace **<>** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE CONTENT AS STRING**. -#### Run the sample - - javac TokenGenerationExample.java - java TokenGenerationExample + +### Service account (`com.example.serviceaccount`) + +These use the `BearerToken` / `SignedDataTokens` utilities directly, for callers who want to +manage their own bearer tokens rather than passing a credentials file/string straight to +`Credentials` (see [Authenticate](../README.md#authenticate) for when you'd do which). + +| Sample | Demonstrates | +|---|---| +| [BearerTokenGenerationExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java) | Basic token generation from a credentials file or credentials string, using `Token.isExpired()` to avoid re-minting a still-valid token. | +| [BearerTokenGenerationUsingThreadsExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java) | Sharing one `BearerToken` instance safely across multiple threads. | +| [BearerTokenGenerationWithContextExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java) | Context-aware tokens via `setCtx(String)` and `setCtx(Map)`, for CEL policies keyed on request context. | +| [ScopedTokenGenerationExample.java](src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java) | Scoped tokens via `setRoles(...)`, restricting the token to specific role IDs. | +| [SignedTokenGenerationExample.java](src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java) | Signed data tokens via `SignedDataTokens`, with string and object context variants. | + +### Vault operations (`com.example.vault`) + +| Sample | Demonstrates | +|---|---| +| [BulkInsertSync.java](src/main/java/com/example/vault/BulkInsertSync.java) / [BulkInsertAsync.java](src/main/java/com/example/vault/BulkInsertAsync.java) | Bulk insert into one table (`tableName` at the request level), reading the summary, walking per-record results, and retrying failed records. | +| [BulkMultiTableInsertSync.java](src/main/java/com/example/vault/BulkMultiTableInsertSync.java) / [BulkMultiTableInsertAsync.java](src/main/java/com/example/vault/BulkMultiTableInsertAsync.java) | Bulk insert across multiple tables in one call (`tableName` set per record instead). | +| [BulkTokenizeSync.java](src/main/java/com/example/vault/BulkTokenizeSync.java) / [BulkTokenizeAsync.java](src/main/java/com/example/vault/BulkTokenizeAsync.java) | Tokenizing values against one or more token groups, reading the per-group outcome (a value can partially succeed), and retrying. | +| [BulkDetokenizeSync.java](src/main/java/com/example/vault/BulkDetokenizeSync.java) / [BulkDetokenizeAsync.java](src/main/java/com/example/vault/BulkDetokenizeAsync.java) | Detokenizing tokens with a per-token-group redaction override, reading per-token results, and retrying. | +| [BulkDeleteTokensSync.java](src/main/java/com/example/vault/BulkDeleteTokensSync.java) / [BulkDeleteTokensAsync.java](src/main/java/com/example/vault/BulkDeleteTokensAsync.java) | Deleting tokens, reading per-token results, and retrying. | +| [CustomHeaderExample.java](src/main/java/com/example/vault/CustomHeaderExample.java) | Attaching a custom HTTP header (e.g. a request id) to outgoing requests via a `RequestInterceptor` on the operation's options object. | +| [TimeoutAndRetryConfigExample.java](src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java) | Configuring HTTP timeouts and transport-level retry behavior, per-vault and client-wide, and how the two levels resolve. | + +Every `*Sync`/`*Async` pair above shows the same request-building and response-handling logic; the +`Async` variant just wraps it in `CompletableFuture` callbacks instead of a direct call. Response +handling follows the same shape everywhere: read `getSummary()` for totals, then walk `getRecords()` +checking `getError() == null` per entry — see [Error Handling](../README.md#error-handling) for the +full model these samples are built on. \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java deleted file mode 100644 index 84e8ff55..00000000 --- a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.example.serviceaccount; - -import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; -import com.skyflow.enums.LogLevel; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.BulkDetokenizeRequest; -import com.skyflow.vault.data.BulkDetokenizeResponse; - -import java.util.ArrayList; -import java.util.List; - -/** - * This example demonstrates how to configure and use the Skyflow SDK - * to detokenize sensitive data stored in a Skyflow vault. - * It includes setting up credentials, configuring the vault, and - * making a bulk detokenization request. The code also implements a retry - * mechanism to handle unauthorized access errors (HTTP 401), e.g. when - * the bearer token minted from the credentials has expired. - */ -public class BearerTokenExpiryExample { - public static void main(String[] args) { - try { - // Setting up credentials for accessing the Skyflow vault - Credentials vaultCredentials = new Credentials(); - vaultCredentials.setCredentialsString(""); - - // Configuring the Skyflow vault with necessary details - VaultConfig vaultConfig = new VaultConfig(); - vaultConfig.setVaultId(""); // Vault ID - vaultConfig.setClusterId(""); // Cluster ID - vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) - vaultConfig.setCredentials(vaultCredentials); // Setting credentials - - // Creating a Skyflow client instance with the configured vault - Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR - .addVaultConfig(vaultConfig) // Adding vault configuration - .build(); - - // Attempting to detokenize data using the Skyflow client - try { - detokenizeData(skyflowClient); - } catch (SkyflowException e) { - // Retry detokenization if the error is due to unauthorized access (HTTP 401) - if (e.getHttpCode() == 401) { - detokenizeData(skyflowClient); - } else { - // Rethrow the exception for other error codes - throw e; - } - } - } catch (SkyflowException e) { - // Handling any exceptions that occur during the process - System.out.println("An error occurred: " + e.getMessage()); - } - } - - /** - * Method to detokenize data using the Skyflow client. - * It sends a bulk detokenization request with a list of tokens and prints the response. - * - * @param skyflowClient The Skyflow client instance used for detokenization. - * @throws SkyflowException If an error occurs during the detokenization process. - */ - public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { - // Creating a list of tokens to be detokenized - List tokens = new ArrayList<>(); - tokens.add(""); // First token - tokens.add(""); // Second token - - // Building a bulk detokenization request with the token list - BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() - .tokens(tokens) // Adding tokens to the request - .build(); - - // Sending the detokenization request and receiving the response - BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); - - // Printing the detokenized response - System.out.println(detokenizeResponse); - } -} diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java index c6354fe8..d5a26679 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java @@ -8,6 +8,7 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; import com.skyflow.vault.data.TokenGroupRedactions; import java.util.ArrayList; @@ -22,7 +23,8 @@ * 2. Creating a list of tokens to detokenize * 3. Configuring token group redactions * 4. Building and executing an async bulk detokenize request - * 5. Handling the detokenize response or errors using CompletableFuture + * 5. Reading the per-token outcome and the summary from the response + * 6. Handling the detokenize response or errors using CompletableFuture */ public class BulkDetokenizeAsync { @@ -46,7 +48,9 @@ public static void main(String[] args) { .addVaultConfig(vaultConfig) .build(); - // Step 4: Prepare list of tokens to detokenize + // Step 4: Prepare list of tokens to detokenize. The SDK assigns each token an index + // from its position in this list and returns it on the matching response record, so + // results stay correlated even though the batches complete out of order. List tokens = new ArrayList<>(); tokens.add(""); tokens.add(""); @@ -70,13 +74,37 @@ public static void main(String[] args) { skyflowClient.vault().bulkDetokenizeAsync(detokenizeRequest); future.thenAccept(response -> { System.out.println("Async bulk detokenize resolved with response:\t" + response); + + // Read the summary. totalTokens counts the tokens you submitted, and the other + // two classify each one, so together they sum to that count. + System.out.println("total tokens:\t" + response.getSummary().getTotalTokens()); + System.out.println("detokenized:\t" + response.getSummary().getTotalDetokenized()); + System.out.println("failed:\t\t" + response.getSummary().getTotalFailed()); + + // Walk the per-token outcomes. A record succeeded when its error is null; + // requestId identifies the batch an error came from and is set on failures only. + for (BulkDetokenizeResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> value=%s group=%s%n", + record.getIndex(), record.getToken(), record.getValue(), record.getTokenGroupName()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Tokens that failed with a retryable status (5xx other than 529) can be resubmitted + if (!response.getTokensToRetry().isEmpty()) { + System.out.println("tokens to retry:\t" + response.getTokensToRetry()); + } }).exceptionally(throwable -> { System.err.println("Async bulk detokenize rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); - } catch (SkyflowException e) { + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints + } catch (Exception e) { // Step 8: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java index 957c23ff..8f75257e 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java @@ -8,6 +8,7 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; import com.skyflow.vault.data.TokenGroupRedactions; import java.util.ArrayList; @@ -20,7 +21,8 @@ * 2. Creating a list of tokens to detokenize * 3. Configuring token group redactions * 4. Building and executing a bulk detokenize request - * 5. Handling the detokenize response or any potential errors + * 5. Reading the per-token outcome and the summary from the response + * 6. Handling the detokenize response or any potential errors */ public class BulkDetokenizeSync { @@ -44,7 +46,10 @@ public static void main(String[] args) { .addVaultConfig(vaultConfig) .build(); - // Step 4: Prepare list of tokens to detokenize + // Step 4: Prepare list of tokens to detokenize. The SDK assigns each token an index + // from its position in this list and returns it on the matching response record, so + // results stay correlated even though large requests are split into batches that run + // concurrently. List tokens = new ArrayList<>(); tokens.add(""); tokens.add(""); @@ -66,9 +71,41 @@ public static void main(String[] args) { // Step 7: Execute the bulk detokenize operation and print the response BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); System.out.println(detokenizeResponse); + + // Step 8: Read the summary. totalTokens counts the tokens you submitted, and the + // other two classify each one, so together they sum to that count. + System.out.println("total tokens:\t" + detokenizeResponse.getSummary().getTotalTokens()); + System.out.println("detokenized:\t" + detokenizeResponse.getSummary().getTotalDetokenized()); + System.out.println("failed:\t\t" + detokenizeResponse.getSummary().getTotalFailed()); + + // Step 9: Walk the per-token outcomes. A record succeeded when its error is null; + // requestId identifies the batch an error came from and is set on failures only. + for (BulkDetokenizeResponseRecord record : detokenizeResponse.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> value=%s group=%s%n", + record.getIndex(), record.getToken(), record.getValue(), record.getTokenGroupName()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Step 10: Optionally retry the tokens that failed with a retryable status (5xx other + // than 529). A token that simply does not exist fails with a 4xx, so it is not included. + List tokensToRetry = detokenizeResponse.getTokensToRetry(); + if (!tokensToRetry.isEmpty()) { + System.out.println("retrying:\t" + tokensToRetry); + BulkDetokenizeResponse retryResponse = skyflowClient.vault().bulkDetokenize( + BulkDetokenizeRequest.builder() + .tokens(tokensToRetry) + .tokenGroupRedactions(tokenGroupRedactions) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 8: Handle any errors that occur during the process + // Step 11: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java index dff0deca..daf25202 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java @@ -8,6 +8,7 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.UpsertOptions; @@ -23,7 +24,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing an async bulk insert request - * 4. Handling the insert response or errors using CompletableFuture + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or errors using CompletableFuture */ public class BulkInsertAsync { @@ -73,7 +75,7 @@ public static void main(String[] args) { insertRecords.add(insertRecord2); // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,16 +94,37 @@ public static void main(String[] args) { // Step 9: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Records that failed with a retryable status (5xx other than 529) come back + // unchanged and can be resubmitted as-is. + if (!response.getRecordsToRetry().isEmpty()) { + System.out.println("records to retry:\t" + response.getRecordsToRetry().size()); + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 10: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java index c3ff5d60..3a059e99 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java @@ -9,6 +9,7 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.UpsertOptions; @@ -22,7 +23,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing a bulk insert request - * 4. Handling the insert response or any potential errors + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or any potential errors */ public class BulkInsertSync { @@ -72,7 +74,7 @@ public static void main(String[] args) { insertRecords.add(insertRecord2); // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,9 +94,38 @@ public static void main(String[] args) { // Step 9: Execute the bulk insert operation and print the response BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); System.out.println(response); + + // Step 10: Read the summary, then walk the per-record outcomes. A record succeeded + // when its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Step 11: Optionally retry the records that failed with a retryable status (5xx other + // than 529). Your original records come back unchanged and can be resubmitted as-is. + List recordsToRetry = response.getRecordsToRetry(); + if (!recordsToRetry.isEmpty()) { + BulkInsertResponse retryResponse = skyflowClient.vault().bulkInsert( + BulkInsertRequest.builder() + .tableName("") + .upsert(upsert) + .records(new ArrayList<>(recordsToRetry)) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 10: Handle any errors that occur during the process + // Step 12: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java index c0f15834..8d9890f3 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -8,6 +8,7 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.UpsertOptions; @@ -23,7 +24,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing an async bulk insert request - * 4. Handling the insert response or errors using CompletableFuture + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or errors using CompletableFuture * *

Multi-table mode: the table name is set on every record instead of on the request. * The SDK rejects a request that sets it at both levels, or on only some of the records. @@ -58,7 +60,7 @@ public static void main(String[] args) { List upsertColumns = new ArrayList<>(); upsertColumns.add(""); - // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // upsert is optional; when updateType is omitted the vault treats it the same as "UPDATE". // Set .updateType("REPLACE") to replace the matched row instead. UpsertOptions upsert = UpsertOptions.builder() .uniqueColumns(upsertColumns) @@ -94,16 +96,37 @@ public static void main(String[] args) { // Step 8: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Records that failed with a retryable status (5xx other than 529) come back + // unchanged and can be resubmitted as-is. + if (!response.getRecordsToRetry().isEmpty()) { + System.out.println("records to retry:\t" + response.getRecordsToRetry().size()); + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 9: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java index a3c1211b..26b743f7 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -9,6 +9,7 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.UpsertOptions; @@ -22,7 +23,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing a bulk insert request - * 4. Handling the insert response or any potential errors + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or any potential errors * *

Multi-table mode: the table name is set on every record instead of on the request. * The SDK rejects a request that sets it at both levels, or on only some of the records. @@ -57,7 +59,7 @@ public static void main(String[] args) { List upsertColumns = new ArrayList<>(); upsertColumns.add(""); - // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // upsert is optional; when updateType is omitted the vault treats it the same as "UPDATE". // Set .updateType("REPLACE") to replace the matched row instead. UpsertOptions upsert = UpsertOptions.builder() .uniqueColumns(upsertColumns) @@ -94,8 +96,36 @@ public static void main(String[] args) { // Step 8: Execute the bulk insert operation and print the response BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); System.out.println(response); + + // Step 9: Read the summary, then walk the per-record outcomes. A record succeeded + // when its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Step 10: Optionally retry the records that failed with a retryable status (5xx other + // than 529). Each returned record still carries its own tableName/upsert, so the retry + // request needs no request-level tableName. + List recordsToRetry = response.getRecordsToRetry(); + if (!recordsToRetry.isEmpty()) { + BulkInsertResponse retryResponse = skyflowClient.vault().bulkInsert( + BulkInsertRequest.builder() + .records(new ArrayList<>(recordsToRetry)) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 9: Handle any errors that occur during the process + // Step 11: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } diff --git a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java index 4dc4d67c..64371a6a 100644 --- a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java +++ b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java @@ -10,6 +10,7 @@ import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertOptions; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.UpsertOptions; @@ -66,7 +67,7 @@ public static void main(String[] args) { } // Step 5: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,13 +93,29 @@ public static void main(String[] args) { // Step 8: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request, options); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only — the same header this sample attaches to the outgoing + // request shows up here on any batch that failed. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] skyflowId=%s tokens=%s%n", + record.getIndex(), record.getSkyflowId(), record.getTokens()); + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 9: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); @@ -110,4 +127,4 @@ public static String getRequestId() { System.out.println("id=>" + id); return id; } -} +} \ No newline at end of file From e493d24999bcc827c13de8d1eddc34b1434a166c Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:30:00 +0530 Subject: [PATCH 03/15] SK-3061: fix inaccurate JSON response examples in flowvault README - Bulk Insert: tokens example showed a flat string per column ("card_number": "5484-..."), but the value is always a LIST of {token, tokenGroupName} entries - one per token group configured on that column, even when there's only one. This is the exact shape a dedicated regression test (testBulkInsert_successWithListOfMapsTokenShape) guards, and it's what the API's own generic Object typing is for. Added a populated hashedData example and a code snippet showing how to read a tokens entry, since there's no typed accessor for it yet. - Bulk Detokenize: metadata example showed {} on a successful record, but metadata normally carries skyflowId/tableName on success (per the wire type's own Javadoc and the key-rename Utils.java performs on it), and the real "nothing there" case is null, not {}. Field names/order for all four response record types (verified against InsertResponseRecord/BulkInsertResponseRecord, TokenizeResponseRecord/ TokenizeResponseToken, DetokenizeResponseRecord/BaseDetokenizeRecordResponse, DeleteTokensRecord) and the requestId null-on-success/populated-on-error-only behavior were already correct - no changes needed there. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/flowvault/README.md b/flowvault/README.md index 5873f74e..c48d7a8d 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -437,9 +437,17 @@ Sample response: "requestId": null, "tableName": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "tokens": { "card_number": "5484-7829-1702-9110", "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" }, + "tokens": { + "card_number": [ + { "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" } + ], + "cardholder_name": [ + { "token": "b2308e2a-c1f5-469b-97b7-1f193159399b", "tokenGroupName": "deterministic_string" }, + { "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "tokenGroupName": "vault_token_group" } + ] + }, "data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" }, - "hashedData": null, + "hashedData": { "card_number": "b6e6d...c3f9" }, "httpCode": 200, "error": null }, @@ -458,6 +466,17 @@ Sample response: } ``` +`tokens` is a per-column map, and its value is always a **list** of `{token, tokenGroupName}` entries — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. The API models this generically (`Object`, not a fixed type) to stay flexible, so the SDK does too — there is currently no typed accessor for it, you cast/iterate it yourself: + +```java +@SuppressWarnings("unchecked") +List> cardNumberTokens = + (List>) (Object) record.getTokens().get("card_number"); +for (Map entry : cardNumberTokens) { + System.out.println(entry.get("tokenGroupName") + " -> " + entry.get("token")); +} +``` + Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. > **Deprecation notice:** `getFields()` is deprecated in favor of `getTokens()` — it is kept only for backward compatibility and will be removed in a future release. Update call sites to `getTokens()`. @@ -598,7 +617,7 @@ Sample response: "requestId": null, "value": "4111111111111111", "tokenGroupName": "card_number_cg", - "metadata": {}, + "metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" }, "httpCode": 200, "token": "5479-4229-4622-1393", "error": null @@ -617,6 +636,8 @@ Sample response: } ``` +`metadata` typically carries the token's `skyflowId`/`tableName` on success; it's `null` (not `{}`) when the API returns nothing for it. + Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. # Bulk Delete Tokens From 34f3a24489c865be3a423aaaa713936bfbc7fe46 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:33:19 +0530 Subject: [PATCH 04/15] SK-3061: document schema vs. schemaless vault support per operation This was flagged in the Slack thread (Saketh's clarification point 1: "We have the SDK interface to schema/schemaless vault mapping already but we haven't added it in the readme, we will add it") and was still missing - grepping the README for "schema" turned up nothing. Added a table documenting which of the four bulk operations apply to which vault type, matching Devesh's original clarification in the thread: insert is structured/schema-only, tokenize and deleteTokens are schemaless-only (confirmed by git history - SK-2646 shipped them specifically as "Schemaless vault apis"), and detokenize works with both since it only needs the token itself, not a table. Verified this isn't enforced anywhere in Validations.java, so worded it as supported/intended usage rather than something the SDK validates or blocks. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flowvault/README.md b/flowvault/README.md index c48d7a8d..99f7b889 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -28,6 +28,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope - [Timeouts and retries](#timeouts-and-retries) - [Logging](#logging) - [VaultController — Bulk operations](#vaultcontroller--bulk-operations) + - [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults) - [Batching and concurrency](#batching-and-concurrency) - [Bulk Insert](#bulk-insert) - [Bulk Tokenize](#bulk-tokenize) @@ -299,6 +300,19 @@ The SDK logs through `java.util.logging` at `LogLevel.ERROR` by default. Levels | `bulkDeleteTokens(BulkDeleteTokensRequest)` | `BulkDeleteTokensRequest`, optional `BulkDeleteTokensOptions` | `BulkDeleteTokensResponse` | Delete many tokens in one call | | `bulkDeleteTokensAsync(BulkDeleteTokensRequest)` | same | `CompletableFuture` | Async variant of `bulkDeleteTokens` | +## Schema vs. schemaless vaults + +Which of these operations makes sense depends on whether the vault is **structured** (has a schema — tables and columns) or **schemaless** (stores standalone tokens with no table structure): + +| Operation | Supported on | +|---|---| +| `bulkInsert` / `bulkInsertAsync` | Structured (schema) vaults — inserts into a table's columns. | +| `bulkTokenize` / `bulkTokenizeAsync` | Schemaless vaults — tokenizes a raw value directly against named token groups, with no table involved. | +| `bulkDeleteTokens` / `bulkDeleteTokensAsync` | Schemaless vaults. | +| `bulkDetokenize` / `bulkDetokenizeAsync` | Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. | + +This reflects supported use cases, not something the SDK validates or blocks — nothing stops you from calling, say, `bulkTokenize` against a structured vault; it just isn't the intended usage and isn't a scenario the SDK is tested against. + Each method also accepts an optional options object (`BulkInsertOptions`, `BulkTokenizeOptions`, `BulkDetokenizeOptions`, `BulkDeleteTokensOptions`) — see [Custom Request Headers](#custom-request-headers). A single bulk call accepts at most **10,000** records or tokens; anything larger is rejected up front with a `SkyflowException`. Under that ceiling the SDK splits the payload into batches and sends them concurrently, which is why errors from one call can carry different `requestId` values. From 547b2c4d9aba037cc96dd06d92dbe67659d1dd74 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:35:59 +0530 Subject: [PATCH 05/15] SK-3061: cover InsertResponseRecord's deprecated constructor directly Codecov flagged 2 uncovered lines in InsertResponseRecord.java. The existing deprecated-constructor test only goes through BulkInsertResponseRecord's deprecated 8-arg constructor, which delegates straight to the new 9-arg constructor -> new 7-arg super constructor, never touching InsertResponseRecord's own deprecated 6-arg constructor. Nothing else in the codebase constructs InsertResponseRecord directly (it's only ever used via the Bulk subclass), so that constructor was genuinely untested. Added a test that instantiates it directly and asserts data defaults to null. Co-Authored-By: Claude Sonnet 5 --- .../vault/data/ResponseComponentTests.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 780711e1..4344ce28 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -67,6 +67,30 @@ public void testBulkInsertResponseRecord_deprecatedConstructorAndGetFieldsStillW Assert.assertEquals(hashedData, record.getHashedData()); } + @Test + @SuppressWarnings("deprecation") + public void testInsertResponseRecord_deprecatedConstructorDefaultsDataToNull() { + // BulkInsertResponseRecord's deprecated constructor delegates straight to the new + // 7-arg super constructor, so it never exercises InsertResponseRecord's own deprecated + // 6-arg constructor. Cover that one directly. + Map tokens = new HashMap<>(); + tokens.put("name", "tok-1"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + InsertResponseRecord record = new InsertResponseRecord( + "persons", "skyflow-id-1", tokens, hashedData, 200, null); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(tokens, record.getFields()); + Assert.assertNull(record.getData()); + Assert.assertEquals(hashedData, record.getHashedData()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); + } + @Test public void testBulkInsertResponseRecord_errorCase() { BulkInsertResponseRecord record = new BulkInsertResponseRecord( From c203addbe4922eb49eb010a4708fc0403aba1a8d Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:51:09 +0530 Subject: [PATCH 06/15] Revert unsubstantiated metadata content claim in flowvault README The previous commit asserted metadata "typically carries skyflowId/ tableName" with equal confidence for both keys. Re-checking: skyflowId is grounded in real code (Utils.java renames a skyflowID key to skyflowId when present, which only exists because that key is known to show up), but tableName is only mentioned in one line of Javadoc on the generated wire type - no transform logic touches it and no test in the suite constructs or asserts a tableName key anywhere in metadata. That's a docstring example, not a verified contract. Reverting the example and the claim rather than asserting a shape I can't actually back up. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flowvault/README.md b/flowvault/README.md index 99f7b889..ee507f6f 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -631,7 +631,7 @@ Sample response: "requestId": null, "value": "4111111111111111", "tokenGroupName": "card_number_cg", - "metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" }, + "metadata": {}, "httpCode": 200, "token": "5479-4229-4622-1393", "error": null @@ -650,8 +650,6 @@ Sample response: } ``` -`metadata` typically carries the token's `skyflowId`/`tableName` on success; it's `null` (not `{}`) when the API returns nothing for it. - Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. # Bulk Delete Tokens From 61c5e397c429d640418254c5c7fefd74c06e5730 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 17:59:03 +0530 Subject: [PATCH 07/15] SK-3061: repeat vault-type applicability inline on each bulk operation section The "Schema vs. schemaless vaults" table lives under "VaultController - Bulk operations", but a reader jumping straight to e.g. "# Bulk Tokenize" via the TOC or a search never sees it. Added a one-line "Vault type supported" note at the top of each of the four operation sections (Insert/Tokenize/Detokenize/Delete Tokens), linking back to the consolidated table for the full picture. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flowvault/README.md b/flowvault/README.md index ee507f6f..5b535899 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -358,6 +358,8 @@ The 10,000-item ceiling per bulk call is a separate, fixed limit and is not conf Insert many records — even across different tables — in a single call. Each record is a `BulkInsertRequestRecord` with its own `data` and, optionally, its own `tableName` and `upsert`. +> **Vault type supported:** structured (schema) vaults — inserts into a table's columns. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + **Note:** - `tableName` must be specified at exactly one level: either on the request (`BulkInsertRequest.builder().tableName(...)`) or on **every** record (`BulkInsertRequestRecord.builder().tableName(...)`) — not both, and not neither. @@ -501,6 +503,8 @@ Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequest Tokenize many values in one call. Each value can be tokenized against one or more named token groups. +> **Vault type supported:** schemaless vaults — tokenizes a raw value directly against named token groups, with no table involved. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk tokenize request ```java @@ -577,6 +581,8 @@ Tokenize reports at **two** levels: one entry per input value in `records`, and Detokenize many tokens in one call, optionally overriding the redaction applied per token group via `tokenGroupRedactions`. +> **Vault type supported:** both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk detokenize request ```java @@ -656,6 +662,8 @@ Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth re Delete many tokens in one call. +> **Vault type supported:** schemaless vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk delete tokens request ```java From 0c34f5cb939d92de6b7a3a67beb1b228c4ae0852 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 18:03:45 +0530 Subject: [PATCH 08/15] SK-3061: trim vault-type notes to match the terse delete-tokens style Insert/Tokenize/Detokenize repeated the explanation already in the consolidated table; shortened to one line each, consistent with the delete-tokens note. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flowvault/README.md b/flowvault/README.md index 5b535899..29edd17b 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -358,7 +358,7 @@ The 10,000-item ceiling per bulk call is a separate, fixed limit and is not conf Insert many records — even across different tables — in a single call. Each record is a `BulkInsertRequestRecord` with its own `data` and, optionally, its own `tableName` and `upsert`. -> **Vault type supported:** structured (schema) vaults — inserts into a table's columns. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). +> **Vault type supported:** structured (schema) vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). **Note:** @@ -503,7 +503,7 @@ Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequest Tokenize many values in one call. Each value can be tokenized against one or more named token groups. -> **Vault type supported:** schemaless vaults — tokenizes a raw value directly against named token groups, with no table involved. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). +> **Vault type supported:** schemaless vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). ### Construct a bulk tokenize request @@ -581,7 +581,7 @@ Tokenize reports at **two** levels: one entry per input value in `records`, and Detokenize many tokens in one call, optionally overriding the redaction applied per token group via `tokenGroupRedactions`. -> **Vault type supported:** both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). +> **Vault type supported:** both. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). ### Construct a bulk detokenize request From 146faf6200c58efacaae4f52bb22c36d78a979ab Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 18:19:06 +0530 Subject: [PATCH 09/15] SK-3061: allowlist "codehaus" for cspell CI spellcheck flagged flowvault/samples/README.md:38 - "codehaus" from the org.codehaus.mojo:exec-maven-plugin groupId in the sample run instructions. Legitimate Maven groupId, not a typo; added alongside the other domain-specific terms already in the word list (jfrog, sonatype, etc.). Co-Authored-By: Claude Sonnet 5 --- .cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell.json b/.cspell.json index 58d8ce26..cd82e044 100644 --- a/.cspell.json +++ b/.cspell.json @@ -51,6 +51,7 @@ "dotenv", "powermock", "mvn", + "codehaus", "sonatype", "jfrog", "prekarilabs", From 4487acc1105ff0f78e86aac07d33427908348a7f Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 18:29:33 +0530 Subject: [PATCH 10/15] SK-3061: fix Skyflow.getVaultConfig() crashing on an empty vault list Skyflow.getVaultConfig() did: Object[] array = this.builder.vaultConfigMap.values().toArray(); return (VaultConfig) array[0]; - an unguarded array[0] access. A client built without any addVaultConfig(...) call (build() never validates this) throws ArrayIndexOutOfBoundsException instead of failing predictably. Traced every call site of "getVaultConfig()" in the codebase first: all of them are on VaultController (which has its own, unrelated, already-safe getVaultConfig() returning its single stored config - no lookup involved), never on Skyflow directly. So this method had zero usages and zero test coverage anywhere in the suite. Fixed by mirroring the sibling method's established, already-correct convention: BaseSkyflow.getVaultConfig(String) is a plain vaultConfigMap.get(vaultId), returning null when absent - no exception, no signature change. Rewrote the no-arg overload the same way (.stream().findFirst().orElse(null)) rather than making it throw SkyflowException like vault()/vault(String) do, since that would diverge from its own sibling's contract and require adding a checked exception to the signature - a source-incompatible change under this module's japicmp gate for no real benefit. This fix needs no baseline update: same signature, implementation-only. Added 7 tests covering: single vault, first-of-several (consistent with vault()'s "first" semantics, and identity-matched against getVaultConfig(id)), the empty-client regression case itself, null after removing the only vault, falling back to the remaining vault after the first is removed, and the two existing gaps on the by-id overload (unknown id, empty client). Co-Authored-By: Claude Sonnet 5 --- .../src/main/java/com/skyflow/Skyflow.java | 3 +- .../test/java/com/skyflow/SkyflowTests.java | 57 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/flowvault/src/main/java/com/skyflow/Skyflow.java b/flowvault/src/main/java/com/skyflow/Skyflow.java index 5c2e682c..c2647824 100644 --- a/flowvault/src/main/java/com/skyflow/Skyflow.java +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -36,8 +36,7 @@ public static SkyflowClientBuilder builder() { } public VaultConfig getVaultConfig() { - Object[] array = this.builder.vaultConfigMap.values().toArray(); - return (VaultConfig) array[0]; + return this.builder.vaultConfigMap.values().stream().findFirst().orElse(null); } /** diff --git a/flowvault/src/test/java/com/skyflow/SkyflowTests.java b/flowvault/src/test/java/com/skyflow/SkyflowTests.java index 7986e68a..4052c4a9 100644 --- a/flowvault/src/test/java/com/skyflow/SkyflowTests.java +++ b/flowvault/src/test/java/com/skyflow/SkyflowTests.java @@ -509,11 +509,66 @@ public void testVaultById_removedVaultThrowsWhileOthersStillResolve() throws Sky } } - // ── getVaultConfig ──────────────────────────────────────────────────────── + // ── getVaultConfig() ────────────────────────────────────────────────────── + + @Test + public void testGetVaultConfig_returnsTheOnlyConfiguredVault() throws SkyflowException { + // addVaultConfigTemplate stores cloneVaultConfig(vaultConfig), not the same reference, + // so compare fields rather than identity against the config passed into addVaultConfig. + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertEquals("vault1", client.getVaultConfig().getVaultId()); + Assert.assertEquals("cluster1", client.getVaultConfig().getClusterId()); + } + + @Test + public void testGetVaultConfig_returnsFirstConfiguredVaultAmongSeveral() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + + Assert.assertEquals("vault1", client.getVaultConfig().getVaultId()); + // Consistent with vault(): the no-arg accessor always resolves to the first-registered vault. + Assert.assertSame(client.getVaultConfig("vault1"), client.getVaultConfig()); + } + + @Test + public void testGetVaultConfig_returnsNullWhenNoConfigExists() { + // Regression test: this used to be Object[] array = ...toArray(); return (VaultConfig) + // array[0], which threw ArrayIndexOutOfBoundsException on an empty map instead of + // failing gracefully like every other lookup in this class. + Assert.assertNull(Skyflow.builder().build().getVaultConfig()); + } + + @Test + public void testGetVaultConfig_returnsNullAfterTheOnlyVaultIsRemoved() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + client.removeVaultConfig("vault1"); + + Assert.assertNull(client.getVaultConfig()); + } + + @Test + public void testGetVaultConfig_fallsBackToTheRemainingVaultAfterTheFirstIsRemoved() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + client.removeVaultConfig("vault1"); + + Assert.assertEquals("vault2", client.getVaultConfig().getVaultId()); + } + + // ── getVaultConfig(vaultId) ─────────────────────────────────────────────── @Test public void testGetVaultConfig_returnsNullForUnknownVaultId() throws SkyflowException { Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); Assert.assertNull(client.getVaultConfig("vault-unknown")); } + + @Test + public void testGetVaultConfig_byIdReturnsNullWhenNoConfigExists() { + Assert.assertNull(Skyflow.builder().build().getVaultConfig("vault1")); + } } From 91eb4c3c285706b0b31d4fe22fb75f39cc194ce3 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 19:26:44 +0530 Subject: [PATCH 11/15] SK-3061: document detokenize metadata shape, now grounded in the proto Previously backed out a claim about metadata's typical content since only skyflowId was grounded (a rename in Utils.java) and tableName was just a Javadoc description with no code or test behind it. flowdb_dp_apis.proto's metadata field has its own literal example value, not just a free-text description: {"table": "table1", "skyflowID": "4524524534623"}. That's stronger evidence than the description text, and it reveals the description itself is misleading - it says "such as tableName or skyflowID" but the actual example key is "table", not "tableName". Utils.java only renames "skyflowID" to "skyflowId"; "table" passes through unrenamed. Documented the real shape and explained the rename explicitly. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flowvault/README.md b/flowvault/README.md index 29edd17b..d2abe82d 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -637,7 +637,7 @@ Sample response: "requestId": null, "value": "4111111111111111", "tokenGroupName": "card_number_cg", - "metadata": {}, + "metadata": { "table": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1" }, "httpCode": 200, "token": "5479-4229-4622-1393", "error": null @@ -656,6 +656,8 @@ Sample response: } ``` +`metadata` typically carries `table` and the token's `skyflowId` on success — the API's own field definition documents its value as `{"table": "table1", "skyflowID": "4524524534623"}` (note the wire key is `skyflowID`, uppercase-ID); the SDK renames that one key to `skyflowId` before returning it, but leaves `table` as-is. It's `null` (not `{}`) when the API returns nothing for it. + Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. # Bulk Delete Tokens From be49ebf3cdedcea21882d5e4390251c3270ed491 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 19:49:03 +0530 Subject: [PATCH 12/15] SK-3061: add typed Token accessor for insert response tokens Adds getTokenDetails() to InsertResponseRecord (inherited by BulkInsertResponseRecord) alongside the existing generic getTokens(): Map. It parses the same data into Map>, so callers get getToken()/getTokenGroupName() instead of casting Map entries by hand - the exact "known gap" flagged in the Slack thread and the earlier README note. Token is a straight reintroduction of flowvault's own pre-rework class (deleted in 685a82ff when insert was reworked around the current InsertResponseRecord/tokens map), not a new invention - same shape the user specified, with final fields and a toString() added to match TokenizeResponseToken's established convention in this package. getTokenDetails() is purely additive (new method, no signature changes to anything existing) and computed fresh from getTokens() on every call rather than stored separately, so the generic and typed views can never disagree. It normalizes every shape getTokens()'s value is known to take - a list of {token, tokenGroupName} entries, a single such entry not wrapped in a list, or a bare token value with no group info - into a consistent List, returning null only when getTokens() itself is null. Fixed a real landmine along the way: ResponseComponentTests.java had a dangling {@link Token} javadoc reference left over from the class's removal - harmless while there was no Token class to resolve to, but it would have silently started resolving to this new class instead (with the surrounding comment still saying it "was removed"). Updated the comment to describe what actually happened. Added 9 tests covering Token itself and every shape getTokenDetails() normalizes, including one against BulkInsertResponseRecord directly (not just the base class) to confirm the inherited method works for the type callers actually receive. Updated the README's Bulk Insert section to document getTokenDetails() in place of the manual cast-it-yourself snippet from the previous commit. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 15 +-- .../vault/data/InsertResponseRecord.java | 65 ++++++++++ .../java/com/skyflow/vault/data/Token.java | 37 ++++++ .../vault/data/ResponseComponentTests.java | 116 +++++++++++++++++- 4 files changed, 219 insertions(+), 14 deletions(-) create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/Token.java diff --git a/flowvault/README.md b/flowvault/README.md index d2abe82d..eae5fd32 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -482,18 +482,17 @@ Sample response: } ``` -`tokens` is a per-column map, and its value is always a **list** of `{token, tokenGroupName}` entries — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. The API models this generically (`Object`, not a fixed type) to stay flexible, so the SDK does too — there is currently no typed accessor for it, you cast/iterate it yourself: +`tokens` is a per-column map, and its value is always a **list** of `{token, tokenGroupName}` entries — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. The API models this generically (`Object`, not a fixed type) to stay flexible, so `getTokens()` does too. For a typed view of the same data, use `getTokenDetails()` instead — it parses `getTokens()` into `Map>`, with `Token.getToken()`/`Token.getTokenGroupName()`, no casting required: ```java -@SuppressWarnings("unchecked") -List> cardNumberTokens = - (List>) (Object) record.getTokens().get("card_number"); -for (Map entry : cardNumberTokens) { - System.out.println(entry.get("tokenGroupName") + " -> " + entry.get("token")); +for (Token token : record.getTokenDetails().get("card_number")) { + System.out.println(token.getTokenGroupName() + " -> " + token.getToken()); } ``` -Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. +`getTokenDetails()` is a computed view derived from `getTokens()` on every call, not a separately-stored field, so the two can never disagree. It returns `null` when `getTokens()` is `null` (e.g. a failed record), and it also accepts a column value that's a single `{token, tokenGroupName}` entry not wrapped in a list, or a bare token value with no group information — normalizing every shape into a `List` rather than throwing on an unexpected one. + +Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getTokenDetails()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. > **Deprecation notice:** `getFields()` is deprecated in favor of `getTokens()` — it is kept only for backward compatibility and will be removed in a future release. Update call sites to `getTokens()`. @@ -656,8 +655,6 @@ Sample response: } ``` -`metadata` typically carries `table` and the token's `skyflowId` on success — the API's own field definition documents its value as `{"table": "table1", "skyflowID": "4524524534623"}` (note the wire key is `skyflowID`, uppercase-ID); the SDK renames that one key to `skyflowId` before returning it, but leaves `table` as-is. It's `null` (not `{}`) when the API returns nothing for it. - Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. # Bulk Delete Tokens diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java index 02635a18..8b11965d 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -3,6 +3,9 @@ import com.skyflow.logs.InfoLogs; import com.skyflow.utils.logger.LogUtil; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public class InsertResponseRecord { @@ -56,6 +59,68 @@ public Map getFields() { return getTokens(); } + /** + * A typed view of {@link #getTokens()}: the same per-column token data, parsed into + * {@link Token} objects instead of raw {@code Object}s. The API models a column's tokens + * generically to stay flexible (see {@link #getTokens()}), so this parses every shape that + * generic value is known to take — a list of {@code {token, tokenGroupName}} entries (a + * column tokenized against more than one group), a single such entry, or a bare token value + * with no group information — into a consistently-typed {@code List} per column. + * + *

Returns {@code null} when {@link #getTokens()} is {@code null} (e.g. a failed record). + * A column whose raw value cannot be parsed into any of the above shapes is omitted, rather + * than throwing. + */ + public Map> getTokenDetails() { + if (tokens == null) { + return null; + } + Map> details = new LinkedHashMap<>(); + for (Map.Entry entry : tokens.entrySet()) { + List parsed = parseTokenEntries(entry.getValue()); + if (parsed != null) { + details.put(entry.getKey(), parsed); + } + } + return details; + } + + private static List parseTokenEntries(Object rawValue) { + if (rawValue == null) { + return null; + } + List parsed = new ArrayList<>(); + if (rawValue instanceof List) { + for (Object entry : (List) rawValue) { + Token token = toToken(entry); + if (token != null) { + parsed.add(token); + } + } + } else { + Token token = toToken(rawValue); + if (token != null) { + parsed.add(token); + } + } + return parsed; + } + + private static Token toToken(Object entry) { + if (entry instanceof Map) { + Map entryMap = (Map) entry; + Object token = entryMap.get("token"); + Object tokenGroupName = entryMap.get("tokenGroupName"); + return new Token(token != null ? token.toString() : null, + tokenGroupName != null ? tokenGroupName.toString() : null); + } + if (entry != null) { + // A column tokenized against a single, unnamed group can come back as a bare value. + return new Token(entry.toString(), null); + } + return null; + } + public Map getData() { return data; } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Token.java b/flowvault/src/main/java/com/skyflow/vault/data/Token.java new file mode 100644 index 00000000..b01c2a0f --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/Token.java @@ -0,0 +1,37 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +/** + * One token-group outcome for a single column value, as returned inside + * {@link InsertResponseRecord#getTokens()}. A column tokenized against more than one + * token group comes back as a list of these, one per group; see + * {@link InsertResponseRecord#getTokenDetails()} for the typed accessor that parses them. + */ +public class Token { + @Expose(serialize = true) + private final String token; + @Expose(serialize = true) + private final String tokenGroupName; + + public Token(String token, String tokenGroupName) { + this.token = token; + this.tokenGroupName = tokenGroupName; + } + + public String getToken() { + return token; + } + + public String getTokenGroupName() { + return tokenGroupName; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} \ No newline at end of file diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 4344ce28..2b3e028c 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -3,6 +3,7 @@ import org.junit.Assert; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -10,17 +11,18 @@ /** * Tests for the response/success/summary building-block classes that carry real - * constructor logic or toString() serialization: {@link Success}, {@link Summary}, - * {@link Token}, {@link TokenizeResponseToken}, {@link TokenizeResponseRecord}, - * {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, + * constructor logic or toString() serialization: {@link Token}, {@link TokenizeResponseToken}, + * {@link TokenizeResponseRecord}, {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, * {@link DeleteTokensRecord}, {@link BulkDeleteTokensResponseRecord}, * {@link DeleteTokensSummary}, {@link DetokenizeSummary}, * {@link ErrorRecord} and {@link DetokenizeResponseObject}. */ public class ResponseComponentTests { - // Tests for Success, Summary and Token were removed: the bulk insert response contract - // replaced those classes with BulkInsertResponseRecord / BulkSummary, covered below. + // Tests for Success and Summary were removed: the bulk insert response contract replaced + // those classes with BulkInsertResponseRecord / BulkSummary, covered below. Token was removed + // in the same rework, then reintroduced as the typed accessor for InsertResponseRecord's + // getTokens() map - see the InsertResponseRecord section below. // ── BulkInsertResponseRecord ───────────────────────────────────────────── @@ -46,6 +48,9 @@ public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); + // getTokenDetails() is inherited unchanged from InsertResponseRecord - confirm it works + // on the subclass callers actually receive, not just the base class. + Assert.assertEquals("tok-1", record.getTokenDetails().get("name").get(0).getToken()); } @Test @@ -105,6 +110,7 @@ public void testBulkInsertResponseRecord_errorCase() { Assert.assertNull(record.getFields()); Assert.assertNull(record.getData()); Assert.assertNull(record.getHashedData()); + Assert.assertNull(record.getTokenDetails()); } @Test @@ -119,6 +125,106 @@ public void testBulkInsertResponseRecord_toStringSerializesNulls() { Assert.assertTrue(json.contains("\"data\":null")); } + // ── Token / InsertResponseRecord.getTokenDetails() ──────────────────────── + + @Test + public void testToken_gettersReturnConstructorValues() { + Token token = new Token("tok-1", "group1"); + Assert.assertEquals("tok-1", token.getToken()); + Assert.assertEquals("group1", token.getTokenGroupName()); + } + + @Test + public void testToken_toStringSerializesFields() { + Token token = new Token("tok-1", "group1"); + String json = token.toString(); + Assert.assertTrue(json.contains("tok-1")); + Assert.assertTrue(json.contains("group1")); + } + + @Test + public void testGetTokenDetails_returnsNullWhenTokensIsNull() { + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", null, null, null, 200, null); + Assert.assertNull(record.getTokenDetails()); + } + + @Test + public void testGetTokenDetails_parsesAListOfTokenGroupEntriesPerColumn() { + // The real, tested API shape for a column tokenized against more than one group - + // see VaultControllerTests.testBulkInsert_successWithListOfMapsTokenShape. + Map entry1 = new HashMap<>(); + entry1.put("token", "tok-a"); + entry1.put("tokenGroupName", "tg1"); + Map entry2 = new HashMap<>(); + entry2.put("token", "tok-b"); + entry2.put("tokenGroupName", "tg2"); + + Map tokens = new HashMap<>(); + tokens.put("col1", Arrays.asList(entry1, entry2)); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + List col1 = record.getTokenDetails().get("col1"); + Assert.assertEquals(2, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); + Assert.assertEquals("tok-b", col1.get(1).getToken()); + Assert.assertEquals("tg2", col1.get(1).getTokenGroupName()); + } + + @Test + public void testGetTokenDetails_parsesASingleTokenGroupEntryNotWrappedInAList() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map tokens = new HashMap<>(); + tokens.put("col1", entry); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + List col1 = record.getTokenDetails().get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); + } + + @Test + public void testGetTokenDetails_parsesABareTokenValueWithNoGroupInfo() { + Map tokens = new HashMap<>(); + tokens.put("col1", "tok-abc"); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + List col1 = record.getTokenDetails().get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-abc", col1.get(0).getToken()); + Assert.assertNull(col1.get(0).getTokenGroupName()); + } + + @Test + public void testGetTokenDetails_handlesMultipleColumnsIndependently() { + Map groupedEntry = new HashMap<>(); + groupedEntry.put("token", "tok-a"); + groupedEntry.put("tokenGroupName", "tg1"); + + Map tokens = new HashMap<>(); + tokens.put("col1", Collections.singletonList(groupedEntry)); + tokens.put("col2", "tok-bare"); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + Map> details = record.getTokenDetails(); + Assert.assertEquals("tg1", details.get("col1").get(0).getTokenGroupName()); + Assert.assertEquals("tok-bare", details.get("col2").get(0).getToken()); + Assert.assertNull(details.get("col2").get(0).getTokenGroupName()); + } + // ── BulkSummary ────────────────────────────────────────────────────────── @Test From 234694b4af6bddc00f04289542c6132e67393ea7 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 20:02:32 +0530 Subject: [PATCH 13/15] SK-3061: use typed Token accessor in samples, cover its parsing branches Two changes landed together since both were already staged: 1. BulkInsertSync/Async, BulkMultiTableInsertSync/Async, and CustomHeaderExample previously printed record.getTokens() (the raw Map) directly. Updated all five to walk getTokenDetails() instead - a nested loop over Map>, printing token.getTokenGroupName()/ token.getToken() per column, matching the pattern BulkTokenizeSync.java already uses for its own typed per-token loop. 2. 3 more tests, closing every branch the previous commit's 9 tests left untouched in InsertResponseRecord's new parsing logic: - parseTokenEntries's own null check (a column present in the map with a null value, as opposed to the whole tokens map being null, which was already covered) - that column is now omitted from getTokenDetails() rather than appearing with a null/empty entry. - toToken's final `return null` and the corresponding "skip adding" branch in parseTokenEntries's list loop - a null element sitting inside a column's token-group list, which the loop must skip rather than NPE on. - The tokenGroupName-absent ternary branch in toToken's Map-entry parsing - every existing Map-entry test populated both "token" and "tokenGroupName" keys, so the "key missing" path was never exercised. Co-Authored-By: Claude Sonnet 5 --- .../com/example/vault/BulkInsertAsync.java | 14 ++++- .../com/example/vault/BulkInsertSync.java | 14 ++++- .../vault/BulkMultiTableInsertAsync.java | 14 ++++- .../vault/BulkMultiTableInsertSync.java | 14 ++++- .../example/vault/CustomHeaderExample.java | 13 ++++- .../vault/data/ResponseComponentTests.java | 53 +++++++++++++++++++ 6 files changed, 112 insertions(+), 10 deletions(-) diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java index daf25202..e61caa97 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java @@ -10,11 +10,13 @@ import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -105,8 +107,16 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { - System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", - record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokenDetails() is a typed view of getTokens(): Map> + // instead of Map, so no casting to read token/tokenGroupName. + for (Map.Entry> column : record.getTokenDetails().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } } else { System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java index 3a059e99..e1928bd8 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java @@ -11,11 +11,13 @@ import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. @@ -103,8 +105,16 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { - System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", - record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokenDetails() is a typed view of getTokens(): Map> + // instead of Map, so no casting to read token/tokenGroupName. + for (Map.Entry> column : record.getTokenDetails().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } } else { System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java index 8d9890f3..38ac3016 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -10,11 +10,13 @@ import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -107,8 +109,16 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { - System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", - record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokenDetails() is a typed view of getTokens(): Map> + // instead of Map, so no casting to read token/tokenGroupName. + for (Map.Entry> column : record.getTokenDetails().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } } else { System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java index 26b743f7..b4ce7a23 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -11,11 +11,13 @@ import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. @@ -105,8 +107,16 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { - System.out.printf("[%d] %s -> skyflowId=%s tokens=%s%n", - record.getIndex(), record.getTableName(), record.getSkyflowId(), record.getTokens()); + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokenDetails() is a typed view of getTokens(): Map> + // instead of Map, so no casting to read token/tokenGroupName. + for (Map.Entry> column : record.getTokenDetails().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } } else { System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); diff --git a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java index 64371a6a..294d5490 100644 --- a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java +++ b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java @@ -12,11 +12,13 @@ import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -105,8 +107,15 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { - System.out.printf("[%d] skyflowId=%s tokens=%s%n", - record.getIndex(), record.getSkyflowId(), record.getTokens()); + System.out.printf("[%d] skyflowId=%s%n", record.getIndex(), record.getSkyflowId()); + // getTokenDetails() is a typed view of getTokens(): Map> + // instead of Map, so no casting to read token/tokenGroupName. + for (Map.Entry> column : record.getTokenDetails().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } } else { System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 2b3e028c..28c4cda0 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -225,6 +225,59 @@ public void testGetTokenDetails_handlesMultipleColumnsIndependently() { Assert.assertNull(details.get("col2").get(0).getTokenGroupName()); } + @Test + public void testGetTokenDetails_omitsAColumnWithANullValue() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map tokens = new HashMap<>(); + tokens.put("col1", entry); + tokens.put("col2", null); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + Map> details = record.getTokenDetails(); + Assert.assertTrue(details.containsKey("col1")); + Assert.assertFalse(details.containsKey("col2")); + } + + @Test + public void testGetTokenDetails_mapEntryMissingTokenGroupNameKeyParsesAsNull() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + // no "tokenGroupName" key at all - distinct from the bare-value case, since here the + // raw entry is still a Map, just missing one of the two expected keys. + + Map tokens = new HashMap<>(); + tokens.put("col1", entry); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + List col1 = record.getTokenDetails().get("col1"); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertNull(col1.get(0).getTokenGroupName()); + } + + @Test + public void testGetTokenDetails_skipsNullEntriesWithinAList() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map tokens = new HashMap<>(); + tokens.put("col1", Arrays.asList(entry, null)); + + InsertResponseRecord record = new InsertResponseRecord( + "table1", "id-1", tokens, null, null, 200, null); + + List col1 = record.getTokenDetails().get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + } + // ── BulkSummary ────────────────────────────────────────────────────────── @Test From 8148504ed66f16d3199c7a688c5d8fc311bd65e6 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 20:25:57 +0530 Subject: [PATCH 14/15] SK-3061: make InsertResponseRecord.tokens typed (Map>), matching v3 Per explicit direction: retype tokens/getTokens() itself to Map> - matching the pre-rework ("v3") shape - instead of keeping it generic and exposing a separate getTokenDetails() accessor alongside it (the previous commit's approach). BREAKING CHANGE - flagged and confirmed before implementing: - getTokens()'s return type changes (Map -> Map>). - Both InsertResponseRecord constructors' `tokens` parameter type changes for the same reason. This isn't a choice - Map and Map> erase to the same raw `Map` type, so Java forbids two constructor overloads at the same arity that differ only in that generic parameter. There is no way to add this as a new overload alongside the old one; the parameter type has to change in place. Same for BulkInsertResponseRecord's two constructors. - getFields() (deprecated alias) now returns the typed map too, since it just delegates to getTokens(). This diverges from "keep contract exactly like API" (the API's own wire contract types a column's tokens as generic Object) - accepted as the tradeoff for matching v3's typed shape. Moved the parsing logic (a column's raw value can be a list of {token, tokenGroupName} entries, a single such entry, or a bare value) from InsertResponseRecord into a new static utility, Token.parseTokens(Map): Map>. Utils.formatBulkInsertResponse now calls it to convert the wire type's raw tokens map before constructing BulkInsertResponseRecord - parsing happens once at construction time instead of on every getTokens() call. Updated every test that constructed an InsertResponseRecord/ BulkInsertResponseRecord with a raw Map tokens value, or asserted on the old generic return type. The former getTokenDetails()-specific tests now test Token.parseTokens() directly. VaultControllerTests.testBulkInsert_successWithListOfMapsTokenShape simplifies nicely: it no longer needs to cast the parsed result, since getTokens() itself is the typed view now. Updated README and the 5 insert samples (BulkInsertSync/Async, BulkMultiTableInsertSync/Async, CustomHeaderExample) to use getTokens() directly instead of the now-removed getTokenDetails(). Needs a japicmp baseline regeneration (scripts/contract-snapshot- update.sh flowvault) before merge - this is a genuine, intentional break of the existing contract, unlike every other change in this branch so far. I could not run mvn/java in this sandbox to regenerate or verify it myself. Co-Authored-By: Claude Sonnet 5 --- flowvault/README.md | 8 +- .../com/example/vault/BulkInsertAsync.java | 6 +- .../com/example/vault/BulkInsertSync.java | 6 +- .../vault/BulkMultiTableInsertAsync.java | 6 +- .../vault/BulkMultiTableInsertSync.java | 6 +- .../example/vault/CustomHeaderExample.java | 6 +- .../main/java/com/skyflow/utils/Utils.java | 3 +- .../vault/data/BulkInsertResponseRecord.java | 5 +- .../vault/data/InsertResponseRecord.java | 80 ++---------- .../java/com/skyflow/vault/data/Token.java | 72 ++++++++++- .../java/com/skyflow/utils/UtilsTests.java | 6 +- .../controller/VaultControllerTests.java | 13 +- .../skyflow/vault/data/BulkResponseTests.java | 4 +- .../vault/data/ResponseComponentTests.java | 122 +++++++----------- 14 files changed, 165 insertions(+), 178 deletions(-) diff --git a/flowvault/README.md b/flowvault/README.md index eae5fd32..da25cb02 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -482,17 +482,17 @@ Sample response: } ``` -`tokens` is a per-column map, and its value is always a **list** of `{token, tokenGroupName}` entries — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. The API models this generically (`Object`, not a fixed type) to stay flexible, so `getTokens()` does too. For a typed view of the same data, use `getTokenDetails()` instead — it parses `getTokens()` into `Map>`, with `Token.getToken()`/`Token.getTokenGroupName()`, no casting required: +`getTokens()` returns `Map>` — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. On the wire the API models this generically (`Object`, not a fixed type) to stay flexible, but the SDK parses it into `Token` objects before handing it back, so callers get `Token.getToken()`/`Token.getTokenGroupName()` directly with no casting required: ```java -for (Token token : record.getTokenDetails().get("card_number")) { +for (Token token : record.getTokens().get("card_number")) { System.out.println(token.getTokenGroupName() + " -> " + token.getToken()); } ``` -`getTokenDetails()` is a computed view derived from `getTokens()` on every call, not a separately-stored field, so the two can never disagree. It returns `null` when `getTokens()` is `null` (e.g. a failed record), and it also accepts a column value that's a single `{token, tokenGroupName}` entry not wrapped in a list, or a bare token value with no group information — normalizing every shape into a `List` rather than throwing on an unexpected one. +The parser (`Token.parseTokens()`) normalizes every shape the raw wire value is known to take — a list of `{token, tokenGroupName}` entries, a single such entry not wrapped in a list, or a bare token value with no group information — into a consistent `List`, rather than throwing on an unexpected one. `getTokens()` returns `null` when the record has no tokens (e.g. a failed record). -Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getTokenDetails()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. +Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. > **Deprecation notice:** `getFields()` is deprecated in favor of `getTokens()` — it is kept only for backward compatibility and will be removed in a future release. Update call sites to `getTokens()`. diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java index e61caa97..791a74dd 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java @@ -109,9 +109,9 @@ public static void main(String[] args) { if (record.getError() == null) { System.out.printf("[%d] %s -> skyflowId=%s%n", record.getIndex(), record.getTableName(), record.getSkyflowId()); - // getTokenDetails() is a typed view of getTokens(): Map> - // instead of Map, so no casting to read token/tokenGroupName. - for (Map.Entry> column : record.getTokenDetails().entrySet()) { + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { for (Token token : column.getValue()) { System.out.printf(" %s[%s] -> %s%n", column.getKey(), token.getTokenGroupName(), token.getToken()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java index e1928bd8..8f146a82 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java @@ -107,9 +107,9 @@ public static void main(String[] args) { if (record.getError() == null) { System.out.printf("[%d] %s -> skyflowId=%s%n", record.getIndex(), record.getTableName(), record.getSkyflowId()); - // getTokenDetails() is a typed view of getTokens(): Map> - // instead of Map, so no casting to read token/tokenGroupName. - for (Map.Entry> column : record.getTokenDetails().entrySet()) { + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { for (Token token : column.getValue()) { System.out.printf(" %s[%s] -> %s%n", column.getKey(), token.getTokenGroupName(), token.getToken()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java index 38ac3016..3dbfccbe 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -111,9 +111,9 @@ public static void main(String[] args) { if (record.getError() == null) { System.out.printf("[%d] %s -> skyflowId=%s%n", record.getIndex(), record.getTableName(), record.getSkyflowId()); - // getTokenDetails() is a typed view of getTokens(): Map> - // instead of Map, so no casting to read token/tokenGroupName. - for (Map.Entry> column : record.getTokenDetails().entrySet()) { + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { for (Token token : column.getValue()) { System.out.printf(" %s[%s] -> %s%n", column.getKey(), token.getTokenGroupName(), token.getToken()); diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java index b4ce7a23..ddc835ac 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -109,9 +109,9 @@ public static void main(String[] args) { if (record.getError() == null) { System.out.printf("[%d] %s -> skyflowId=%s%n", record.getIndex(), record.getTableName(), record.getSkyflowId()); - // getTokenDetails() is a typed view of getTokens(): Map> - // instead of Map, so no casting to read token/tokenGroupName. - for (Map.Entry> column : record.getTokenDetails().entrySet()) { + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { for (Token token : column.getValue()) { System.out.printf(" %s[%s] -> %s%n", column.getKey(), token.getTokenGroupName(), token.getToken()); diff --git a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java index 294d5490..b947239c 100644 --- a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java +++ b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java @@ -108,9 +108,9 @@ public static void main(String[] args) { for (BulkInsertResponseRecord record : response.getRecords()) { if (record.getError() == null) { System.out.printf("[%d] skyflowId=%s%n", record.getIndex(), record.getSkyflowId()); - // getTokenDetails() is a typed view of getTokens(): Map> - // instead of Map, so no casting to read token/tokenGroupName. - for (Map.Entry> column : record.getTokenDetails().entrySet()) { + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { for (Token token : column.getValue()) { System.out.printf(" %s[%s] -> %s%n", column.getKey(), token.getTokenGroupName(), token.getToken()); diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index c607ace3..31d1b331 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -49,6 +49,7 @@ import com.skyflow.vault.data.ErrorRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.UpsertOptions; @@ -760,7 +761,7 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo indexNumber, current.getTableName().orElse(null), current.getSkyflowId().orElse(null), - current.getTokens().orElse(null), + Token.parseTokens(current.getTokens().orElse(null)), current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java index 84f105e9..1d85c7b6 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; +import java.util.List; import java.util.Map; // Bulk counterpart of InsertResponseRecord. Adds the caller-facing position of the record @@ -16,13 +17,13 @@ public class BulkInsertResponseRecord extends InsertResponseRecord { */ @Deprecated(since = "1.0.2", forRemoval = true) public BulkInsertResponseRecord(int index, String tableName, String skyflowId, - Map tokens, Map hashedData, + Map> tokens, Map hashedData, int httpCode, String error, String requestId) { this(index, tableName, skyflowId, tokens, null, hashedData, httpCode, error, requestId); } public BulkInsertResponseRecord(int index, String tableName, String skyflowId, - Map tokens, Map data, Map hashedData, + Map> tokens, Map data, Map hashedData, int httpCode, String error, String requestId) { super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); this.index = index; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java index 8b11965d..57c7bcf9 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -3,15 +3,13 @@ import com.skyflow.logs.InfoLogs; import com.skyflow.utils.logger.LogUtil; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class InsertResponseRecord { private final String tableName; private final String skyflowId; - private final Map tokens; + private final Map> tokens; private final Map data; private final Map hashedData; private final int httpCode; @@ -22,12 +20,12 @@ public class InsertResponseRecord { * which also lets you populate {@code data}. This overload always leaves {@code data} null. */ @Deprecated(since = "1.0.2", forRemoval = true) - public InsertResponseRecord(String tableName, String skyflowId, Map tokens, + public InsertResponseRecord(String tableName, String skyflowId, Map> tokens, Map hashedData, int httpCode, String error) { this(tableName, skyflowId, tokens, null, hashedData, httpCode, error); } - public InsertResponseRecord(String tableName, String skyflowId, Map tokens, + public InsertResponseRecord(String tableName, String skyflowId, Map> tokens, Map data, Map hashedData, int httpCode, String error) { this.tableName = tableName; this.skyflowId = skyflowId; @@ -46,7 +44,13 @@ public String getSkyflowId() { return skyflowId; } - public Map getTokens() { + /** + * Per-column token data. The API models a column's tokens generically (see + * {@link Token#parseTokens(Map)}), but the SDK parses that into {@link Token} objects here + * so callers get {@link Token#getToken()}/{@link Token#getTokenGroupName()} directly, with no + * casting required. + */ + public Map> getTokens() { return tokens; } @@ -54,73 +58,11 @@ public Map getTokens() { * @deprecated Response key 'fields' is deprecated. Use {@link #getTokens()} instead. */ @Deprecated(since = "1.0.2", forRemoval = true) - public Map getFields() { + public Map> getFields() { LogUtil.printWarningLog(InfoLogs.DEPRECATED_INSERT_FIELDS_GETTER.getLog()); return getTokens(); } - /** - * A typed view of {@link #getTokens()}: the same per-column token data, parsed into - * {@link Token} objects instead of raw {@code Object}s. The API models a column's tokens - * generically to stay flexible (see {@link #getTokens()}), so this parses every shape that - * generic value is known to take — a list of {@code {token, tokenGroupName}} entries (a - * column tokenized against more than one group), a single such entry, or a bare token value - * with no group information — into a consistently-typed {@code List} per column. - * - *

Returns {@code null} when {@link #getTokens()} is {@code null} (e.g. a failed record). - * A column whose raw value cannot be parsed into any of the above shapes is omitted, rather - * than throwing. - */ - public Map> getTokenDetails() { - if (tokens == null) { - return null; - } - Map> details = new LinkedHashMap<>(); - for (Map.Entry entry : tokens.entrySet()) { - List parsed = parseTokenEntries(entry.getValue()); - if (parsed != null) { - details.put(entry.getKey(), parsed); - } - } - return details; - } - - private static List parseTokenEntries(Object rawValue) { - if (rawValue == null) { - return null; - } - List parsed = new ArrayList<>(); - if (rawValue instanceof List) { - for (Object entry : (List) rawValue) { - Token token = toToken(entry); - if (token != null) { - parsed.add(token); - } - } - } else { - Token token = toToken(rawValue); - if (token != null) { - parsed.add(token); - } - } - return parsed; - } - - private static Token toToken(Object entry) { - if (entry instanceof Map) { - Map entryMap = (Map) entry; - Object token = entryMap.get("token"); - Object tokenGroupName = entryMap.get("tokenGroupName"); - return new Token(token != null ? token.toString() : null, - tokenGroupName != null ? tokenGroupName.toString() : null); - } - if (entry != null) { - // A column tokenized against a single, unnamed group can come back as a bare value. - return new Token(entry.toString(), null); - } - return null; - } - public Map getData() { return data; } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Token.java b/flowvault/src/main/java/com/skyflow/vault/data/Token.java index b01c2a0f..e84efb8f 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/Token.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/Token.java @@ -4,11 +4,15 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + /** * One token-group outcome for a single column value, as returned inside * {@link InsertResponseRecord#getTokens()}. A column tokenized against more than one - * token group comes back as a list of these, one per group; see - * {@link InsertResponseRecord#getTokenDetails()} for the typed accessor that parses them. + * token group comes back as a list of these, one per group. */ public class Token { @Expose(serialize = true) @@ -34,4 +38,66 @@ public String toString() { Gson gson = new GsonBuilder().serializeNulls().create(); return gson.toJson(this); } -} \ No newline at end of file + + /** + * Parses the API's raw, generically-typed per-column token data (as returned by the wire + * type, {@code Map}) into {@code Map>}. The API models + * a column's tokens generically to stay flexible, so this parses every shape that generic + * value is known to take — a list of {@code {token, tokenGroupName}} entries (a column + * tokenized against more than one group), a single such entry, or a bare token value with + * no group information — into a consistently-typed {@code List} per column. + * + *

Returns {@code null} when {@code rawTokens} is {@code null} (e.g. a failed record). A + * column whose raw value cannot be parsed into any of the above shapes is omitted, rather + * than throwing. A {@code null} element inside a column's list is skipped the same way. + */ + public static Map> parseTokens(Map rawTokens) { + if (rawTokens == null) { + return null; + } + Map> parsed = new LinkedHashMap<>(); + for (Map.Entry entry : rawTokens.entrySet()) { + List tokens = parseTokenEntries(entry.getValue()); + if (tokens != null) { + parsed.put(entry.getKey(), tokens); + } + } + return parsed; + } + + private static List parseTokenEntries(Object rawValue) { + if (rawValue == null) { + return null; + } + List parsed = new ArrayList<>(); + if (rawValue instanceof List) { + for (Object entry : (List) rawValue) { + Token token = toToken(entry); + if (token != null) { + parsed.add(token); + } + } + } else { + Token token = toToken(rawValue); + if (token != null) { + parsed.add(token); + } + } + return parsed; + } + + private static Token toToken(Object entry) { + if (entry instanceof Map) { + Map entryMap = (Map) entry; + Object token = entryMap.get("token"); + Object tokenGroupName = entryMap.get("tokenGroupName"); + return new Token(token != null ? token.toString() : null, + tokenGroupName != null ? tokenGroupName.toString() : null); + } + if (entry != null) { + // A column tokenized against a single, unnamed group can come back as a bare value. + return new Token(entry.toString(), null); + } + return null; + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index ae41587e..2ba3effd 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -1701,9 +1701,11 @@ public void testFormatBulkInsertResponse_success() { Assert.assertEquals(1, result.getRecords().size()); BulkInsertResponseRecord inserted = result.getRecords().get(0); Assert.assertEquals("sky-id-1", inserted.getSkyflowId()); - Assert.assertEquals(tokens, inserted.getTokens()); + // The wire type's raw tokens map is parsed into typed Token objects before reaching the + // caller - see ResponseComponentTests's Token.parseTokens() tests for the parsing logic. + Assert.assertEquals("tok-abc", inserted.getTokens().get("name").get(0).getToken()); // getFields() is deprecated but still delegates to getTokens() for backward compatibility. - Assert.assertEquals(tokens, inserted.getFields()); + Assert.assertEquals("tok-abc", inserted.getFields().get("name").get(0).getToken()); Assert.assertEquals(data, inserted.getData()); Assert.assertEquals(0, inserted.getIndex()); Assert.assertEquals(200, inserted.getHttpCode()); 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 63504d4b..7d668283 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -41,6 +41,7 @@ import com.skyflow.vault.data.DeleteTokensOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestInterceptor; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.TokenizeOptions; import com.skyflow.vault.data.TokenizeRequestRecord; @@ -707,13 +708,11 @@ public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { Assert.assertNotNull(inserted.getTokens()); // getFields() is deprecated but still delegates to getTokens() for backward compatibility. Assert.assertEquals(inserted.getTokens(), inserted.getFields()); - // The token map is surfaced verbatim as `tokens`, so a List token shape survives intact. - Object field1Tokens = inserted.getTokens().get("field1"); - Assert.assertTrue(field1Tokens instanceof List); - Assert.assertEquals(1, ((List) field1Tokens).size()); - Map field1Token = (Map) ((List) field1Tokens).get(0); - Assert.assertEquals("tok-xyz", field1Token.get("token")); - Assert.assertEquals("group1", field1Token.get("tokenGroupName")); + // The wire type's List token shape is parsed into typed Token objects - no casting. + List field1Tokens = inserted.getTokens().get("field1"); + Assert.assertEquals(1, field1Tokens.size()); + Assert.assertEquals("tok-xyz", field1Tokens.get(0).getToken()); + Assert.assertEquals("group1", field1Tokens.get(0).getTokenGroupName()); } // Tests for the unary query / get controller methods were removed: VaultController is bulk-only now. diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java index cd347df8..8b58b873 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -56,8 +56,8 @@ public void testBulkInsertResponse_twoArgConstructorComputesSummary() { @Test public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { - Map tokens = new HashMap<>(); - tokens.put("name", "token-name"); + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("token-name", "group1"))); Map data = new HashMap<>(); data.put("name", "john"); Map hashedData = new HashMap<>(); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 28c4cda0..b3dfa849 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -21,15 +21,15 @@ public class ResponseComponentTests { // Tests for Success and Summary were removed: the bulk insert response contract replaced // those classes with BulkInsertResponseRecord / BulkSummary, covered below. Token was removed - // in the same rework, then reintroduced as the typed accessor for InsertResponseRecord's - // getTokens() map - see the InsertResponseRecord section below. + // in the same rework, then reintroduced (with the same shape it had before) as the type + // InsertResponseRecord.getTokens() now returns - see the InsertResponseRecord section below. // ── BulkInsertResponseRecord ───────────────────────────────────────────── @Test public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { - Map tokens = new HashMap<>(); - tokens.put("name", "tok-1"); + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); Map data = new HashMap<>(); data.put("name", "john"); Map hashedData = new HashMap<>(); @@ -48,16 +48,16 @@ public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); - // getTokenDetails() is inherited unchanged from InsertResponseRecord - confirm it works - // on the subclass callers actually receive, not just the base class. - Assert.assertEquals("tok-1", record.getTokenDetails().get("name").get(0).getToken()); + // getTokens() is inherited unchanged from InsertResponseRecord - confirm it works on the + // subclass callers actually receive, not just the base class. + Assert.assertEquals("tok-1", record.getTokens().get("name").get(0).getToken()); } @Test @SuppressWarnings("deprecation") public void testBulkInsertResponseRecord_deprecatedConstructorAndGetFieldsStillWork() { - Map tokens = new HashMap<>(); - tokens.put("name", "tok-1"); + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-1"); @@ -78,8 +78,8 @@ public void testInsertResponseRecord_deprecatedConstructorDefaultsDataToNull() { // BulkInsertResponseRecord's deprecated constructor delegates straight to the new // 7-arg super constructor, so it never exercises InsertResponseRecord's own deprecated // 6-arg constructor. Cover that one directly. - Map tokens = new HashMap<>(); - tokens.put("name", "tok-1"); + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-1"); @@ -110,7 +110,6 @@ public void testBulkInsertResponseRecord_errorCase() { Assert.assertNull(record.getFields()); Assert.assertNull(record.getData()); Assert.assertNull(record.getHashedData()); - Assert.assertNull(record.getTokenDetails()); } @Test @@ -125,7 +124,7 @@ public void testBulkInsertResponseRecord_toStringSerializesNulls() { Assert.assertTrue(json.contains("\"data\":null")); } - // ── Token / InsertResponseRecord.getTokenDetails() ──────────────────────── + // ── Token / Token.parseTokens() ─────────────────────────────────────────── @Test public void testToken_gettersReturnConstructorValues() { @@ -143,14 +142,12 @@ public void testToken_toStringSerializesFields() { } @Test - public void testGetTokenDetails_returnsNullWhenTokensIsNull() { - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", null, null, null, 200, null); - Assert.assertNull(record.getTokenDetails()); + public void testParseTokens_returnsNullWhenRawTokensIsNull() { + Assert.assertNull(Token.parseTokens(null)); } @Test - public void testGetTokenDetails_parsesAListOfTokenGroupEntriesPerColumn() { + public void testParseTokens_parsesAListOfTokenGroupEntriesPerColumn() { // The real, tested API shape for a column tokenized against more than one group - // see VaultControllerTests.testBulkInsert_successWithListOfMapsTokenShape. Map entry1 = new HashMap<>(); @@ -160,13 +157,10 @@ public void testGetTokenDetails_parsesAListOfTokenGroupEntriesPerColumn() { entry2.put("token", "tok-b"); entry2.put("tokenGroupName", "tg2"); - Map tokens = new HashMap<>(); - tokens.put("col1", Arrays.asList(entry1, entry2)); - - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Arrays.asList(entry1, entry2)); - List col1 = record.getTokenDetails().get("col1"); + List col1 = Token.parseTokens(rawTokens).get("col1"); Assert.assertEquals(2, col1.size()); Assert.assertEquals("tok-a", col1.get(0).getToken()); Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); @@ -175,105 +169,87 @@ public void testGetTokenDetails_parsesAListOfTokenGroupEntriesPerColumn() { } @Test - public void testGetTokenDetails_parsesASingleTokenGroupEntryNotWrappedInAList() { + public void testParseTokens_parsesASingleTokenGroupEntryNotWrappedInAList() { Map entry = new HashMap<>(); entry.put("token", "tok-a"); entry.put("tokenGroupName", "tg1"); - Map tokens = new HashMap<>(); - tokens.put("col1", entry); - - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); - List col1 = record.getTokenDetails().get("col1"); + List col1 = Token.parseTokens(rawTokens).get("col1"); Assert.assertEquals(1, col1.size()); Assert.assertEquals("tok-a", col1.get(0).getToken()); Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); } @Test - public void testGetTokenDetails_parsesABareTokenValueWithNoGroupInfo() { - Map tokens = new HashMap<>(); - tokens.put("col1", "tok-abc"); - - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); + public void testParseTokens_parsesABareTokenValueWithNoGroupInfo() { + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", "tok-abc"); - List col1 = record.getTokenDetails().get("col1"); + List col1 = Token.parseTokens(rawTokens).get("col1"); Assert.assertEquals(1, col1.size()); Assert.assertEquals("tok-abc", col1.get(0).getToken()); Assert.assertNull(col1.get(0).getTokenGroupName()); } @Test - public void testGetTokenDetails_handlesMultipleColumnsIndependently() { + public void testParseTokens_handlesMultipleColumnsIndependently() { Map groupedEntry = new HashMap<>(); groupedEntry.put("token", "tok-a"); groupedEntry.put("tokenGroupName", "tg1"); - Map tokens = new HashMap<>(); - tokens.put("col1", Collections.singletonList(groupedEntry)); - tokens.put("col2", "tok-bare"); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Collections.singletonList(groupedEntry)); + rawTokens.put("col2", "tok-bare"); - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); - - Map> details = record.getTokenDetails(); - Assert.assertEquals("tg1", details.get("col1").get(0).getTokenGroupName()); - Assert.assertEquals("tok-bare", details.get("col2").get(0).getToken()); - Assert.assertNull(details.get("col2").get(0).getTokenGroupName()); + Map> parsed = Token.parseTokens(rawTokens); + Assert.assertEquals("tg1", parsed.get("col1").get(0).getTokenGroupName()); + Assert.assertEquals("tok-bare", parsed.get("col2").get(0).getToken()); + Assert.assertNull(parsed.get("col2").get(0).getTokenGroupName()); } @Test - public void testGetTokenDetails_omitsAColumnWithANullValue() { + public void testParseTokens_omitsAColumnWithANullValue() { Map entry = new HashMap<>(); entry.put("token", "tok-a"); entry.put("tokenGroupName", "tg1"); - Map tokens = new HashMap<>(); - tokens.put("col1", entry); - tokens.put("col2", null); - - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); + rawTokens.put("col2", null); - Map> details = record.getTokenDetails(); - Assert.assertTrue(details.containsKey("col1")); - Assert.assertFalse(details.containsKey("col2")); + Map> parsed = Token.parseTokens(rawTokens); + Assert.assertTrue(parsed.containsKey("col1")); + Assert.assertFalse(parsed.containsKey("col2")); } @Test - public void testGetTokenDetails_mapEntryMissingTokenGroupNameKeyParsesAsNull() { + public void testParseTokens_mapEntryMissingTokenGroupNameKeyParsesAsNull() { Map entry = new HashMap<>(); entry.put("token", "tok-a"); // no "tokenGroupName" key at all - distinct from the bare-value case, since here the // raw entry is still a Map, just missing one of the two expected keys. - Map tokens = new HashMap<>(); - tokens.put("col1", entry); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); - - List col1 = record.getTokenDetails().get("col1"); + List col1 = Token.parseTokens(rawTokens).get("col1"); Assert.assertEquals("tok-a", col1.get(0).getToken()); Assert.assertNull(col1.get(0).getTokenGroupName()); } @Test - public void testGetTokenDetails_skipsNullEntriesWithinAList() { + public void testParseTokens_skipsNullEntriesWithinAList() { Map entry = new HashMap<>(); entry.put("token", "tok-a"); entry.put("tokenGroupName", "tg1"); - Map tokens = new HashMap<>(); - tokens.put("col1", Arrays.asList(entry, null)); - - InsertResponseRecord record = new InsertResponseRecord( - "table1", "id-1", tokens, null, null, 200, null); + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Arrays.asList(entry, null)); - List col1 = record.getTokenDetails().get("col1"); + List col1 = Token.parseTokens(rawTokens).get("col1"); Assert.assertEquals(1, col1.size()); Assert.assertEquals("tok-a", col1.get(0).getToken()); } From f326ec20693b472d5f8e465f8fc2b096675b5595 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Thu, 13 Aug 2026 20:51:06 +0530 Subject: [PATCH 15/15] SK-3061: regenerate flowvault japicmp baseline for typed getTokens() Regenerated via scripts/contract-snapshot-update.sh flowvault, now that a JDK/Maven are available to run it. Verified mvn -pl common,flowvault -am verify passes clean (692 tests, japicmp included) against this new baseline. The 3 intentional incompatibilities this baseline now accepts, per the full japicmp diff (flowvault/target/japicmp/default-cli.diff): - InsertResponseRecord's deprecated 6-arg constructor: tokens param generics changed (Map -> Map>). - BulkInsertResponseRecord's deprecated 8-arg constructor: same. - getFields(): return type generics changed to match. getTokens() itself was never actually flagged - it's reported as a brand new method against this baseline (the pre-SK-3061 codebase only ever had getFields()), so making it typed was non-breaking on its own. The three real breaks above are avoidable (the deprecated constructors are a different arity than the primary ones, so no erasure conflict forces their parameter type to change; getFields() could reverse-map back to the old shape instead of delegating directly) - flagged and declined in favor of just regenerating the baseline, per explicit direction. Co-Authored-By: Claude Sonnet 5 --- .../skyflow-flowvault-java.baseline.jar | Bin 461421 -> 469240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/flowvault/api-report/skyflow-flowvault-java.baseline.jar b/flowvault/api-report/skyflow-flowvault-java.baseline.jar index d98e0eb7b5b1f4d11b1cf304e2b6f4bcd08c1f6a..021d9cdb1a6519ada54953ae0526b221c6a746b0 100644 GIT binary patch literal 469240 zcmbTeWq2H0mIP>_1tp7_!D42X#mvmi%w);J5;HS1Gs_k;Gc#Ju7OwodXS-j|ytgx} z@6(T}%6lv8W@Owran4CONicAf4^U809}M#0v_JgYAM^+C57NR){50Y+B6P3AA3lJ6 zkdyof_4^3We;y|HmxEE>kNAHFOY_T!iwG+!(MpS4OOK97Nz%|x!Aa6kj*m{%DbUX{ zZSFbHN=Q?SOHVnM)+<~{Q%jC1UN`_0pav?y}a#iwp1hUVdD#iSuE zm=E^0_dx$RBQ037JGuAolYjsJ*BLEWLjURKe0u-X(a4h4&cW8s*ulx%*pW(RXrAhy zr@=DIG|9C4kNH0BRAT==f22Rnr~eu4jjsg60jH9KyiIuJE|7q;+ zv-$mf|25V{-`UFP|1<;o|2Tt@zLWm{X&U;!o<_*n%Gk+R$=1@?#!=DP+FIYioz~Dw z-_bEbL0%S_AK|sJqp_YxB2a<xXag_IGC}3gf*&X&GrL=V>Xn_XkzBd>>YOY@jiXV5_ZmqfLwPE|Je3xi41H zgG6(yFt6^732woFR(VTQ29SNA60F13nCYEZty`Bzmbx8|vJZ<3XFir^yl9=1xn+^U z2rm6Sg!mbZ@eR|fQT~8(<7P_MeLxKEp26Y|lFG$owh)Z^i2^!Eh*=Da!h)js1K0&v zmi&phTs>_x-P|i1Ekn4CoS6%Fr4&)w;@j;z#7~r#N zlEx-$X0mL12j=%fPfe64Lh^t0+8+LQ2I+5N6k=2z8teE z^wqSBjXr+H4S$HIbDnjZMSq$;sflMitR(E?)V2cV55*STg8?mXR0|ygJLgo@@_JXl zBV8YbaC5W_>J-IGlI1ZSr2Bf2lKEpBUCUR8rWu_!`xLeqSBA8&Kra?qgpH?ya8V(n zq|ZT{WHdmR9%wlW_!QP0cA&}&LWJ$k5!;aUldbksk-b<<8Kev{r5RDQ`f?@pzxs6# zl++|C4y;Q-Ovv~zD0FF-Y#uYa@u04LVFEcCAmSqLZW;wwZ3Q z6_&bf=G0Hkq5agD2jW6By0-C~gZ$&s7A4~hchG{bScqwf0Y@e19?)y_L=?~0TlkO}D%3HwJ5mB@B{Hct*b=p8BQ%(xFOq<)BtxF7r60X;q5q1F z8h#9&$*6>_p5MYM=q`B%)GDaadx$qzOTUNATVgiQl%xT6OU!;QMwA5_@bDZ4yQR5; zmWI2>O!%`#6^uXi`8`sa_eX(UaaYsku041*iyF?KQSrXsiQZ07FTK$nUml0z;xaO0 z`x{IVdQwRk>Xu9J^tGiBlW<<;X~evOi|O z5?^#US?^nXyZng2d2F8A8n}>fTrq4Os=zUsSKXz2^)_wipLOtuL2K3wVNtR)Fe(Av2K>fdvW#gNjWdPfX;Q~KsubjY zetg+{rEX-{71nFh5_%y-oB{*)iQc74S*sfAWb{>Kj~6MTuwi;o<(Wn5aRi2hVqwW3>l`|t=nC7$$rsOg1G>$p?k5A~o(IVd^{4o5E z7Sz8*3*BF%<)1_&ZsTO^U}$XTWcyEY@rtam5a34;J~zA4VV33;H!h=C?H{KE5AY$t zsK#eARMU1v-Y6IMrV-}-5e2hE796;V!qj88zuxxt^b3N%*E$3;$9AV*>F0StRF=uW zP?@{2F;iR=NdoW3go*^$h!rH&fHu_gMYm73Car2A$^s*}nB9W54(jI=CJ&OLFB~~9 zNQ$r6Tyz2BEnC@lM5_}Da!oNUWvksIhEVeA+5prXE{;9SBE6qXPyYR}72z1I+Cade?eA;-*;Ugdf z>`3e_noy&%9xh*xB!<=gBWaKQ01 zDd!FybJ|DlXct*Gz$Ic6SXZ>|s7EojnnI~M@MQ&xvW^4UeUJI93}%kJXfroNdDVE( z;-TOG5w-NSCjiiFqU< z5UxbWoT)c*6>rECdm3>}ZxV5x;(XD=&L(Na`b_-R6SV*NKDM*eg$mh-YbhmjqzvNG z)J?mRx?Z=G?%Ac89m2xP(779CEr66+x(eUTTjI%c7tqG`;~S1`>mH!!TT9rNSoEoEwJml(Sm@~1${}2f&9uXsS?;=3};_u+d z{QnKd|B>Gle)GE=0#DqU6|4DtWyAcV1r3d{E9{uSi;8?T0SX$d2HNvxP}@@3I;*sW zgChR-Qa>W{-=N%HwYsRmIC9WG8>K%OP0{asd$@x8p8{QE$!dN0%u_n=+T-LdJYCaqEO{jC+xbXfP z`niW$xAW$&WPe#R{17HYAtG-~obX7aV(17@N|i_6ocW_qxLQP3I#QWIXv@HRM*Ski zf)azmwXC2S8tpV zCpsi`Quig9rB9Y$yQFzd3K`i(bu#cbP)yO-VIIg{PuES^@fU0C0LBXqE+~WOo6axKr_a=`C!pT(5O4I?m>PTuoo7;E z`o>#e@&eCt7Erp@3#Y=tGb}pE?Dnfl-UgMEdX@GBri?jtEK;Kx?4Im5YpKfKWA?o> z7RcXH`~NcN`6tc)gR>G8wBLgs6rPzim!$>>4HU%hav_a#)pVT+7X%gpwNt&U5xg=h zHJ<=&tz2PSy8J|hUA_35QH<*;6-bs0$z!80gRAel(ewKo`xE3Q1s*tg*X5>)yhxzF zeG>kK+@%{%C*DbDj!CiAu-Y*VWYN;`Q?ma#VevLhDd1GU7T?4(=v?`VgL|`OdMnrx zj~89SY)bM@D7^zQ%OM7nz}k=~gj%w)0ERf_C^E<$h@Lc{krm^aD9M6k?t_H``JHNj zVZogeW4`E+HSe0VjWgl0xo%Eh%|d3x;udrD)JO;eHYU8U?CUpzX*_)LVdh+;LFKHX z>E5{fGCnbj3vs7kI>GM0WQ%*EzHj15OQuq+4cRTovbVG=^7eHTsCQ^Y-JsBrrFFhg{ELsyLX4~CW?0UY`!*3 zK|X@a0kMb6=~~ewVo2|Y0Wz`0`<(R~IWW@lGpp~&SqA$%g83U!AmD6eDQ@HV?nWpW zJKDWVNaKHQcB;Qzk(E(iby@-Wie|$KIYgR2m>~o}7W61+Q2Z$L{K=a;NoVMt7?Dj$ z_11)yR?$~oh5NL%G^o>eF3ETXcCvOR@XpiR^UaY2N;2<8+g-2ET)(%qyKX%HnrP7l zvFa8=0GXvIR-N(@0Vei5Lq%b(j`rUHaObCK1$Ru=X4hJpK(YiJMu^~-n134=i>m|&fa1tIW2vs%%@iqG$ zQ0K_d7Ep&uvtp|B$c!R3jo&WU8Imbty>nX zJ#aD0!RQBMRy3+Lpj4Px(<)s~9d_pm)uH;r5d=jrEjUf0!qJr~i2NUa9UQ`&WqJ?= z({l=&EFg@C5ye~}H_HeEeo_!eR*fOZ=qv-13g9SNL#fANhE`4CEGl_QH?e^3aqHF% z*1VKg$oW`hmJoqe2{5OSsBA(Q?Wrsr}q#Hp+=YKqO3-(V5DlW~XC(K5KGU>cRJXjR`3?Hin}~bPn$R z2{h)jQM79d1ruoM=+`JEmuMV^D_vWI z=&=hp!=*gy)mCaXzv?PT?c64U-0|Dyp*({qnyYLWL5$JHEpcj?^^|C`F%f5r2nUt! zt6t=-F<0HqZi2utk&hr54R`sz7#Jk9bqIhZ3Mq}j(^q4IgS|&RjJ|?{^5#G-Bv=qC zbZNEeoESC}$W*n*x60~vS!pz5=Tqnt1ujwWSUNQ6DPlYgk8h04Yy|QaxUmts$f6g7 z%Hn-Fmokg@9}SCVi;H}5f(-p2PbYp%nxJSSR(F3%=uc81PBj+sk}eUkub_60H*Id{ zqSLS6TWv=voQ0bqWz;GG{fK(u zij8PaW1cGwO@(g&0}Y#-Eq-eY#$gK%a5Qq5H2rm~T6IVAV=)bUW93MrO4v8_omUrbqlpx7T>t}vRi`6!nC4p}&c)fx?Y*!V0 zQihjzpKYeri8C8S3mSAM#mv6QPH8xeVWe`AO?KNw{$TL!1M#3XWS_$2iMsG*K~|II zne?O`qUH!Y#F&?H(2@$yhA;M@CW;ITapY3jlCbJRin5(;Qr%r4{uLS%tmvcCw8OwH z;|7oZ@-v3_p$vxPx`RWcmdq|avpWr=qc0VBIHBg*uSr18;?ZrK>As=l&pdHKk{O#r zuBWk53zKoDENUlgg@=bLoGOZ$}G*I=3ps@Z?XiMevfp+yHw z10y*>q*!A0)dqP6PGYozkEa;esmj%aAW*6a`f5vdg~&J0&ulMe`uqAtWE;6@8& zU5+^FC*?m=zdYhtVb=51ZZ3}-vc;@&MQZlj|78C}%9Xd%?8%74Z@Uzs^zAZs*fLH; z0Hx&M5>!p{ix^49AKLQ#d!zX7OgKIXIH>YN#C;vA)Sn5) zQ&N-j=cvuRPj^V|`z3DDJU8~i3p}nHKhhjNwhPz7Uo!9}7PkdRO8m`<^av z7TV6wgS9;gEOw)F*{HIw-*wuyL^h$s>B3XD5qkujr0%J`DhiZpEzL#8HIPC@84Rau z6V1l%isN$9B9hV!pm#Jb-fLItC#EqtRP_~oaeLk8bIL;s!c}a|F$k?rAH{-m*GjCE zLGB!IapTp+7T;6Bn)ZTFbsE)6j5Zf1Tn}<6$7fK0qi~cy>%rMt31t5!0}pis*F+4Z zC_RE$<=2sTc$!R;#xx_{WWC&GG7-BW&v^Q+n!Echut{E84-`MoZDgW0?SlPsE2d3a zu5;GiKh6<)8JlersH-vE_zPqAO7!{%$zfU~u?KeJk-OiTHpY2kC`{MG+a>gI^e-Pd zSx$h;L;XHM7IoD_C|FLfE=y!DgW*li>t>+ePeL(LQ~C*8uMa=qsO?DHw~4QqM5u5a zMH=8wE9}{KE%m)4oh5SmPd=cV4<Yp>wQ(W zIH*>+Sw;~<^LDq7^pi&y%!DdMdyFEE5i27NO^F+4TTpAV?39S`&1VO;q2UC9q_Qcu0#_M+L$#BUQHb(Ao~(F>>tEX&w0{rPf5^Rm zg7hD1FGE3E5>y_AN3{quqkMFDxEw*NSU66fswYr@K#T?^XHStV701+oH~dw(BEZl4 z3m;`G6AX>i$4hiE~I6D zLd%*-kIdnoKMpZjjeH(N2O7Jv2((P5evGMl0xr#DSkzPyh_GjXls=if$!5bqSWNCP z@~4$>z|jSTQU!FkBXs`)eg1w=BnH=3ggmbSEYSVsA9=PK8aWiBG;#)0M}tFrLB>Di zNv~GU+@z9_?bPoOP}iqPq6E|T_G1=%#kKXZ0I@zJvy|H%MUY>NnMTqTtI$q+O}Sc> zUB`&=!67cr56nC9*Eb9f61QM~RNr3WT9{YxJByQ#_AO4#pdEp~NX{)VBJ7QEetwmR zDvRzG86+}9)*MANP0O^~Lx6LLM0=|I~1 zsE|2!;ZUfha>SDH!jSdby1A;Gf((z$1THhK;=N@Dn^ZoMN|0mI(H7kij3H@j0a6~l8HY2#TMUk&r|eoSiJZ?fs9XB zR1otknqX3l$cAeG7lLo-DGK$WU|&L~w_GM|7shMWi%8=%R0EiMlW2FEN+K!(XWS0i z9<%F9Fa&R@(TDq7*6k4zHJ*vMu0*PCxhsP3_^_n*OPnm`neB{gx+u_%8vwEF;&Bbz zo$88d_T4f*#&`Lbp)a56p6A(d$Qk>5`bN?XE$>o&ijqIZZZL71a&&{DDXMeT_?Fz! zDaqRIhC-`}8 z4G5KlIsBG)Ky>{rAQ=7{5Wn}Je|eYx9?6ZVsXL-9puV12(L`Vkfs#;?lJB<{%ibb<@(_5bt~$F%k~Vr0F-6$VK5Qt z#UjT}drWLZrOPz!7~@wwmySlgV#*|5xo>03rNX3yoP;iMDT z?kkdoa1&x@6DxuP8H7xh+VpLstHE(&DWIvAg*a>(d8sl5WasKGwKK!+6$&;jkuVhX z-S!pX2`ndu{mrKsN9+fb&gK1BAYKHvOGjtV7u8#lusD^v4ea1G6PW zS>_SVDzmd+^_!A%AZ5*p=PpU+VdE!)2;~gi3oYFuspPab=sy~Z(#t?&U&I58oWDX~ zL#WW{2-PUOr{ z+fYjbYd|NUz))w({>ygEB3G@sqg3rpcBJ6prVLPghyl{7Zms&HU2>FWB-Z>FM*T4P z!e$vKr+t8lV%isFuYp*3Z)Lp|L31}^+kI|b-}qM+T)eeD>$lotF#17C;hM%EE*eD^oP1rj zYLep0_}S3TPj-%rfo<=w-)S{}37BxG1;svoQK$`HRH2xjwwDQ|o=B-QUSxhTTW@r9 z7@lVUgoYGo&0Lrr?~DxQWh1udbj`*1j~*7F$wwaGs6^0623p#Gf}VBStIK4ux>Mr_ z^G$1rpy)71L@{*XJmPv!i-x%b{<=ZHIc#K9?@_$rtK>`0=M+ANrtc5@wP|)%t%DECB(x)G5KtUDu_gvC6QvXY~+PRU0qX0U0REL{j>2$ zBq~O~(5;w58@)6`D;B#+GoJhSaenKmZ*A?;oKat?&4d^nE`x~Q=IiU*-~zm-1!8dB zD_WFd$fM}$k}~@wu9g$2n)##ZcqklR|8B(RtZL9xs=3-~_ILmqK8IzZ+oqlG!9!r2 zFhSn2)$R0Ls`sJGl}%_AytrR#LAhlj)&+A`HeuozR28?e0KtR~=J~-d+|vOJGPlxw z=X7L3Ej0@e*};2`056?rX#Cq_}33tEb2qOwey51>MDzArkt?LkU}OF9|2{& z1BOjCE@=EL_kKlDP?-dN8bsSeDfl6~6uJtC;@Un!jg;|Wl_;0<5XsLiVZ!%bab~~9 zH}jC0g6(nXQP+`eV4H-HX582q0tX;mge`Lwi;%dZg zCDO91u*dS)L2-exOZ2!AnhXih%yqpK&{e(}x>b(hT6C6_7v(&2j{UeO)zfIVi)qFk znV9371rGl+LhS01yD>LvfZD$U(0Hv@>wge+KtHT_V?3_2<^F)Bafy430=}~c#g)35qLtvs)>;zfi`|^R$z zd5Y|40_VM>`ROgnQbXW|aP9qKxAe{)DL}HpsJPu;ip+W0Yez^t_^muK)EOtfjV!zx?v<>YK65Y;j==K+F_%eN-bBu z_s5uKulh0siVnJOJBxCX#|Q+v?w~#kejbz{nTNZ^E6iBQU1`{4_N5BX*NU^HD#iB; z#W$J>Szp2itui4F7dmjN@Q_cqk=^5mj>bxUaG}zRr<3p+Xgro6h#q!2Z60;Ry7C?1 z;m-|ym}_mL7t5F>nU2Fq@0BwpCgiLey+PH@HKkQtOsl`40d@0?+;--lr5xJU-2@Un zkXgLZy?y*GGmdPuW#7x}J}|yla{YDu!2Z9L8GjV6{3At5!tl?8gG`rAH$BRA-WRSf zxw`bH)z^%LULa5;pO6&>;Ej&JBm{|4>!Elf!0>;FO2e+Wf3#ZSUAnq``U-Lp=^c)? zCB2kkw`X5)%FkRoud_QX$x!LsQvW$^yBG7^(uLR1xa`>-B~MzcsWwJHvJNe*iiS$| z&}+F>ft!jM8Sf3(t%U`;r$m0awY!{e(bfuY$M?eH9J^c32B3;elVO;|>x-}jkI31j zc}VnWNd&3=o(cva7WYT$7<8N;ukrnt!@bL}zg`=*|LxlRLxTN#{y0I&@{e4Mpsrk^ zU%^#1L9UOrdwK9VQZ5DyO9bhc)WfD6+y!6bq?2)?hqUMH5P{qfz90C;?MxNLlUNVc zO!w;@_vuWoXJ=7ay5GT?13#KO5he<(3V@0Of3(v zTiUo4Ov=zKQMz7@Lx(?6#v zUYb~X&}oE}vbbYo6fH}^WSz&@jmKURtAD6V@v-gu9yfPP+p742xO6{V)!}%BB`286BpA3HMI`947UkFKM(K=C%6ad_ zZ)h0XIYrBZIe1(Ze@C@@JIyOTF}KhR#K44>l3!--xEhR_u}Rq9kNP3;i%!goh!2RV z!8B=@D2(I}9^2!5B{B9DMGicCj$~Q|x(>MBjk90k(zRHQT0`OBe?m*Ga6+1BHiu=k z>@1UegINMk${$B2ZMPWB%8x{&Aj0>3rDB`X*m8K4?WTiM;Ek|0Xm)IAnvm9uyI&$L zoq=<*xeME3zW#!-*ZHGsM7U?+{wv*YN*Dy!ZHRrh6pa5iMJ@W*u>V&P(tlO5{kM1i z)A6#Mp-13tl5R8dO<<64U&@~3UnW!l-{6GfpF&Vz?&YMsoRXHlO#Do254pp{XC_8K z68!-3Oy0Y6x*n@SnHy@5MR&jI=skXU&d28s@`G4yiV$oGbXRJUKz1_(`($})yi1er zLGOHK5!odQ#)8j~Xi=Z{t(SG_DOor-^@=;9Wx2qEyIc%wJsozaFr+G0*I9}WDv7vwBF`ZkK%Kj3&b}wS=@O8FlU|BQIxSGfX6c^pWJE}z!~we z$)w;(;-)@rPa6N!aw&@+cGfK<(CfVLLTlv45(dB3U8p4YwNd!P>%e{4jMYfE zn3JiXqr$F&#u+)Ru_tA>ygk?ZDo?0tH%euSS-EZY33L-nYXI|2TdkKI+PmQJ1I%rL zEjvzbin5(x3p!$bzCt{teh|Un3vo=k zo-!C$>=L1F7~_QuWg{EsItZF0QFbT?UM!T!4!e|s=DPqPG;i776~b3jl4Pk5{pYT8 zX8h9PzCi%nW6r~GAiu#InGSU+_72{{zxBgt{u;dhbI6>b_zzR|Y`rytaet9Cl=Bi| zS-OPO8=Fu*oV0K%AV!$aWvu~Cg3jEQ;h~IqD#mOZ{z4Q}b_H6ki2r+fQ~EXc!QQ&< z2H!V`ix2l@gM?Mt*}1_;f?f-p^?->h+?_tNq?Op5`mM*xRii=6$7(-DN|;j0{cOha zC4&v9`$!$n?FxG{;Y{ZK#QJdDngOqS+>QM$-wVNUxlb~qttrt$)3<;bIAdM@2z?Hq zLx3H(4SUW-L=g#|a3$M5KkD`1z_wo~BpRj#A-vWIG4LSfAP|5n&GrU585O2Ux!f!U zwgl2d!cuz^L^RwPFandPtktCq+%G5Tj@ec>D7P?ivG#hAG9v(6aD4h=f=rz~Y?E)V#FXk#q%+o0`b9-d0e@C4=58NH z-Xnq)lbfkO&?a*7hAR~Z*jHZou*K?U6wUk^|w)tHvGIfT5N?bY` z+!XJOn`5XZ@pzk>>@q`aswdwcmY$yDN;&;I6tn*S)^Pm0gYzeIwnAw|g7|&YLp3DD z6pd3}kZk|Feo>3z-$}viCJgJe_Co1}DjWVu$3`Axej0bZ2c1urJ_ zXS(E_*Huv5mf(4-lM;)TifvF&j@Q;bt=0=D!rxvOPixmh0}9Ogz^@nK+Cx{BTNwm( zjc8ub7X0*1xgu{Gt>mjD&$JsGyusqiiIgh7E(KKU)gTNpfWhKQ=!X!Pf+fpuX8L4UXL$Brwc>z_Bpc!_=&r{ibGm{_NVPpLh5=6c1uY#>%!o}mozx@1{S|5xo0&sj!0BK+;n%JSFb^q+a9KUZjUsA?*rh@!lDxau2W2JVp- z2gAe~qc0T)BcebEYildfIxwLJt=VH@m<*1VhqjQBG9>Zo?GYvngVDLuEBj$7mT;RZ zuU{p<#gL6mr=O*0)53pmFJp!29qS+z2H*I>$+& zrL@R%!RZ0Oqx-mc^heF zdO9`d5ItUfO-X_O5iM38=m6=?op)5?J9<-ZFvge;TcuR*upX}lwyzMJ$?Kb|AcD$@ ztj1-T|I1hjB@a@Q#44c%t^tsi*rUIg%~4vwUf3sxYNO%DdR7q!zWC24GJ#$%lYoz^pQ$>#AwMF8zX;RGme5wXFnM1OL&@qEQ|XxO3T@=36zy-dkss zM;!2R%zLiJcl z;xx^UTOtx+zt?$;JyOx2`R4?KV!*-2?lqhwdq`7gL~GttcBX-`_~E@I1uQlkmI@7A z{+sRefNMrtI}Jg1B!C1_KO2`?T6Cg6mO{@YGjWSKqtXWGaY7Mgf&;czd#29It#m0z4rA<4vMrSs zP;d&rX^AXYzaBYb+HOk2m`p3625D932;x(;0))Gp!%2d>L+1j-q6{+;{rl(0$0sC# zsjuN%W;4gpCYC6#!W+dTEW7;jXnX@&jAFv_<8*we@*=Qc8q0F&;liK#RF$%Neo9r2 ze~L6MsN@c6R&&>Zu)@kUJlIzd7hnp0JF=g{a=PxI)+a^v5+lSnJEMV2hmOEle5y4Y z`a0*}RmY^B5t7h6KLO!d{F(*JBR=-*Dxw?UWm7xZF@y1`at3A1{8K^7S!gs7T2U-P zq6}r~xaw+3TN=!UPaa8;M*?Qlk?pH#k?uaL5tM@ci1q@_mh;Kk1S_2J$eARf6R^%b z>}`KC!Q`jgoz7O@dm%(AnSGB?B`~QjSa9pI^vGJKsjK;dGV-?LU*efr= z*Sg_;(nC}z@d=f%En8+nR0mvW)>VE)H42Scj%g#Dh!9?)B4 z7gJfQ8E`wEBoDm2*<|pB;p^$6prPu0xP(7CC{e=RUA+h)Uvyj`{AElMm?WDd72CP4 zab4J^p~e$Q(XJ_7OuyJr*T7*Ix6soe4^HL)f8`xww(YeSPD{k` zK7=yM)unaU)nI zWbjC2Zj*!Fw6|h(5i}RBNRqy?Q9u_*+_HD<;r7lCg36E`@@hA#wwR?i3|2}$1XVcy_Ds|P(KpHP7{JR)9~Fr1d5eJPJv zD5o=!P6+KQ#of2tKUxpG(^R%h-zE0IdjsRYtP#Ec=tlfsrJes&omvq$BGDs<0DLny zobLCiYM8go2~+7JXhtm13lqp`mZNCqwvNJ(MjkEL);ucB-UNG)+a_@#Za^qdH@bQ| z7-e&e9U3_J$>jrP^;3+Xgnvj6hB9Z?-UI0R=48kbC87(SXzLV67Y;lXU-%vk?|bvr z&%l zFGxNhO_=`T%U7vOD=Bv@ql&bjT1YeDucO(jnrmHCx9G&#Z2`RixSdpb)|??e>814+ znq1!4gSv&jkz%J&CGDP~%lL6R$i&30W@EIoo^c2(P^iB$U7^{1onNFMVfSz@Pi*w9 z>3U~*wz|)j>y;!Y81N>smEhBz#}512SYBE#sJpg=>!Xu<>)!b@rYuM>48nWZ>%5yN zktj5jIx!_?KN-6=R7}M3h|O$)`O9MpgfM#oakCjHm3#=d3I`f{&JT+W1}wJbt}_#{ zWLd92f+r|IBD2T4J^}k%#rik;B*p*mb0;06=8mxi`v6E?U6H zmE1YNbE4j0d21)t0tQ}Undox>7vBBP@koI7_fCf;#ibGb4=vP?G=MGxyGJ9QEQWDZ zQxkgGW_0=e?UqB^`zRsl%Hl<0v?~l_jHzcfeFT;!w>;RaV)=*MK!$K80CT`DC5s@h zh{*J9y7c;NH8gBoGk>)pMH-zy7|b9mh69()+5sbs`O68B_3f|+N$7$K#=5q43-}WJ zjC{*f*)l+cnpM}?s|JyT5}6uhk-h9nSr*5d%;4?NZ|qqi{V(JwP9U zQA#WP+cNja<Aqae0vAmZt!nWRq^lo!dB{R#>Auu=Ds`1hdeM%w+-mTde54O@{Za5Y98Zy`D#fM>;l^WF+*U# zxse$Tp-g6NMDCXjlD^CwtGfC+JkcKORgiiXQSw5X0=fd40{RM?f~mux|8o*jlNs>#HLJ*L(@=vG9ZmTlm$A#U)iy_x01Dh za+R#QJLIHsulVDP=n`xYC3=vPdmX_rZYXxuig5hH7$8O+7;}G*#t#2hd;d*c^uO10 z{kw^qp`@ue^9AL#Q5u>CM1Dm33#9=zt-O#N3^~+#;W0qZ^oFM15;_2UhDPT#x{8yS zK98yM$e&MwCoSH=Tn`7omV@Kq>&CCUlcnd!ms>6$XzI@h{7Qkjgg{83RM#5gaN4=< z%9*C&K>1=fd1^N^TFV%Ot}|PUqRu|V0u>|ks9NObeGG@ARbb1zfIf;3N8M7Fq44az zUPNr~?KnvhSC%Es`a---iKHNhEcjD@___g32LUkUM2b(gG=`;N7fDE(PYr~P<7Cv!YDm3JdDyLDm80c zhSMvVYyjL*~yt-UrUC>ym+4rNUpOc9Jevlnc^K$Z+a zM5XyLyJmvso)Bs&#>_|ijz7(3(@M^91Q#6o5a-b*G>t9C_OBy% zU=-CV1-UTF;|%bC5e2R*HYv!$?Ty#u(%6Q{uN&e+yO{Bt7vp=>L7XiO{T<420Ym<- zu!R|HC1_r%8KG)TVn=poS!Ai;Y*kQo)xTy}@5Gg)aIt@U2@B-{9Q+iT@kwY_gA-v$ z`DAU5Y|LFMt-n%}@89u}ThieJCNpCaJZThBH(axs=jS)(l^#<+!kTTBcF=EnLpn~g zwk}dC{U0LSh;btXR0GbCigj=1gw7E?M0EATL7hCi!A^?^Tf4z1F&QxGb&4ioYf(K0 zZnt9O8{zL#c!`?%%bqDfyhtkWcZ`nsiEO6vVLMTS6B3osFKH;qj+4xa6Ce0vFgg?; zd6m4}&{KJ9!&JPS(8axO$gtYtqvRH8IgL(>b=%S=v-`=#5D!pGnZ;0ngN|bhjEMB} z_4yVW3M@1wrD*0uI^@m5cPK>PCc~0LgG0qpn_ireq!K`8KN9)+4;IB#*oeExqJ6Z}mPdwhdoyCM`Xz@D;2g@6tGQk{*2 zz_(^q>G9L0>8wNZR`-u4*B|(w$JeYhHL=Xqny(j`OveG&PZLqPAMo_XgrSW;qT(yh zLb>)B(M_K7c0|U)clizyAxi}!cG1r>vT!|#e6Bg;9#UdXpwJiZ6DiDDWrG&|na43V zvHK)t!Nd(|>rveE-TV3ii7J3i(9J#n=q0A&63CA2Db3Gy+$Pz-gR#To6op z#3RP8ic5AX<=X15KVVpeF(F_tCUkGc%9dyb-2lnHkVQfOF+#Z1N{fVQzhXKdqVEww zXH40pztSVDR+Vi2vs2pv)gF>;K+bF{69(Y5$Yy*U4Ff3mI7rieAl_eMoltcLehTb> z@Z$?NOtBFbupFXt2{lpM39GoiWQ^%hzoUGa(u1HQL%E!9d0|A2Mlr?LsF z-Mi|GpA|wvjs-=G?~R%{4GtTjJ%jlCN+Hes&puA3ndRx}ewJH7*YT6g>vYHUmYY8w z1W~T9EDB=|hY6;>i-EWF8iU%&;kEa8UXF~{7Bjbt*u>f)2R>Elh8;#vc{iHb8@Yl4 zB1p@dScQppEJ#il-!pE%`=FziP%;2G9Aq-{t$xYIWJEa*dJfRE-)&Y|gC^cH!D#jg z19xl-bo1(?cF-tj$5iRM%h*>l`?Adfwp)WU16lFZOA`-IKAzW^ zx1d*{x8i#<<0U~?ne#3T8p%dNX~-F={+wv;CTm1spF}}!qHDm5m$xN-{<>-_N1}0L z#anb7rXhVTSMbS!U-BTdSf8L{g2s*)e_wQMJuWQn0tyKq-Y zM$9<2&}?P~$@{)yUnFxERH^7Z`(0iQ@b>b+XvhVpBx+(4(x?}WRw~BE`|)tW&Y)0x zL8_20ZX>9bEwyE6?4>QqJPFsB#~aavah=Fc zaO6^)(Q)ok?(5Oj=Lh#&+#j5YT59ARDG_0^(1i<}f0MjNPvD@D~I^w^3 z%l{zG|0?5%l>7w3j}h?m9;>Ps=STPdL)kY*2f8NRcG$7qNxEY@>Daby+jhscZQJVD zwr$(KIeq5bZ_eDAp6{;JZ?Ba%|K2=L)vjH;c6GTV_kjOIfQ;GzG~c^Vp2KPmXiWA4 zVv02?rI5>OQ<0a6*gu8I3{d%jnT43*Q+a4#Ho~8(pPwp}8stHvc%L2$&nH)Lp3u&zgqzBgh}$WWXsMy7D7IeE#?@|M?2B z{O1(%=QsJSrN&1u#$uWaj(xi)p)p?=C|7Ezm5Wk7rWjcU**BA4C@vcpfOHF-c@S4j zt)<7I$qm`vm>}}u69)v%u@Z^NHW=Q^#-e(Lt-;=ji|5xPrWLu6#h zoP#);Of{ELUSS@o@KGoTPC@v}UmmTL=x>WjF zRd?0#h%;1vN-HM;U;BNvE|SDL9PZ*<8FGE@AXHjTC-PS<#{%{qoknD}n*p0`Ka~{p zsf;cYDZR3${q{9X(2eSc=uH2Ws}Ir5L8^?eijBarX@w(|VV$p63I z%zx+)B}vks4R$GBfDc~gJRR@asn6ZqpRHw` z%YKgJXOF8U`#7t$!pZmug@S8ShEU7HPp92Jc8ZTFHa>%O-L}?n=6M>lN?AxEs1ypr zH4%~8K`~@5%V1e4u=nEEhytwJU!T`v3_c#Svc1`UJW1V`oZSxG2CfMf;}(;R;)nT8 zCR{k#f$~oAt+uLROe1u@=VS%3Ry zC$vo92&9jMJ!{ts3t|%qD^kJ-;JNrv^{C`IH{bF|#DX+;RF2kT;x3?e2%|%b*n@b5 zITT*wnR#7{s+&AV?Sg4rtT60w=a=%%9TkPS?jtbi{q|V=2H(nC{V}BdS7!g)Pk+a^ zav$Z;A68NGgvWD)RrR;h8C4cCr-Tz!(K4MszUBHyV_a-K%jN$XJ{ z<$1+v{#tCl;?`|*LBnF}Q2twK{UL2BaQy>PB62@ObA5kgdHNNd1#nwsXz|61O<~+j zr1wxJC_C==h;dg!-uet<>lWP^cg2OYY6xgDh;maU{^Ujh@VdRC#Vd4#=Pr~t0A9Iq zMkGCSn-HW5T%WqgTf*)jjn?Y*E1e0Mj}I+0%iq7}u@SwvBBy$1IQlgfT?e^9e6d7c zJH^t+V%jdkc{+O3MrFmsO3`=Rr#tbTTWT1U+@$=ge=>)vhzCg@ulo}Wd4B`PVH)Ll z2WJLK^78C%&-N&_-o$OM6Ik|@!tl*cwJN39)7EA;k2zT09Z~CCX$S_Gcb~okeR$_p z@Rj&Sm4OsSuO;Xh^jgMx^Je4_*K}b(X=t@{OWS^;YR{yy7L&=50V$2*C3BJ;SSxvH9D-)h9aMQxnMe+8(>64| zC09-%T=)3BE}Z6E@B-TASvHCAQF?;gdScQ1{R7z2N86r}x6o@}=P=UEpeQ&q4{$c0 z8P0JCZD%qDeo|G6t~4gvyxNV_Eh!2U39~XzS6fQucg9e-(tff;JS0z~_>}){-?S&4 z$xfCM+X<7E?zcBR*#1XWrJ)p06Z?o~;lGvG{HApOw^{YCv9n+NKfA8!R8Mi8$?Q}1 zMz*{$@>gPHRC(a610Hy_wR**$-^KAGf<5BO|BPou#+_jxx+sokY-7xHjMtC1cK}*n zh6(*6C!)l3(J-<;$av(6S0`O&*tl{q^7bi&wn9wNbz@Og_Q7jcP3C&R37FWj7v3(3 zsyD?M5eg{<(dOByxNszG*H4uwN~d00h%6(O>+oH3T&L?K-|=>(yIe5Qv^n#~##4QF zqNbyLFPp1(l_!ja?q%r7!}X3OWR(=VAM62|0=PrSj(@pJXMl8`6xy$dLKysZAyDh%3HlPvfcb8>4Nhcr#gVwZCXk7Xwa*S61(Va(adb!%K;Ij$D9H@ z#4D)N7qbE-bVCaLl7w_nY|XBr25K5Q=vfR=nVQjP^<>un_L1fsIP725w= zX#Cgw^cPC{OY`^daARp1IXS=J7O+N;pAZvw$|Fs_eZUzG=4clHrU-rg_hK2%GSZ^{ zYlqJhf)rfg6JaA73pdpYd*l+dvJyR0!ZZVNEINiVbAxfHZUYO=4pdxTaA!Nh5CpdZ zBZ{oym|4yAi!@up*@+V`%x7(izI{r9LkoPC1}F7|Fj6>nZ_eC>l68CAQo^Q-{uFRm z52O*hSQ$sB%&&#DCamF#lzvfj(86 zq;PPVnwoTPvG#h_y54%;#_0KGyUqBi#F{isE zmW@ww$(?|Iwbk?*sTfb3Z)T{;02BoY-j}+kCs&4TqZ_bsQwRD{b8Pc*Q+HLQU7wN0}^`ISbbqG+$0Po>&q zyAb>@(D-0uQb!DHR!&gmR`Kvc34^C_n}85taeWPS~#n55wYK*TuqKd!8sVM^m4uTf9+|r)$hGy?J zr3%zL`t{SEX-d+ppO{~R!ereKF9eGBaL4bX%{>wpWKwIK#%kAuI@rO<31p3o+*76L zrSnK5)1mYzn8j48_rdfOQHw^hLW$?R>L=Es>n&g8`*y-z+t++L-8KLi0e=EO2e*?^ zkS$Nt=67Plf~_=~@5eteNF=R}q6rPn=c7Yus!YS?KH*7nvKti2PQfW86I{M7jz`(0 zGP<0;LV=@jt&;5WK>zG(vCU*^oS_eRsVrmVgTURca`acC$czaz1x=k#>dN%NO8i}t3d-tPz|g}_BUwMyR!*qTQfPxIRFvok3(r0j>x>IG?9oFfFH z{e7dhmNX=k3z~??8(@oS1+yI&zo6wu@Ewn@3al_Ml>}XV^Hx?v$krT~t=G0S|Dwp? zC*mV5IPp$JbcT=E<@YU4)eKB5iZ5@(Fdb9u3l!*}{pP z#&R-)fIbS*DttA4B^yETDCz-f%a^=b*p*%-p*PS28x0A`=CCQ^Z*7zZ@kY$LUx<1Q zxwX>^9*g0xNih^Rz``P;^*CEWG^lN$$*0&p9YTx_S%s#@973~13{P^Y$WBPs9G;nA z_RIdDvnk8Sta55WXwf=1?q8xyuRAWpr_U_YqNq0e#4yTRsN2ZBf!1~Mi5-PHbqTF0 zXu?aMS7~VCgl#{6UxQcg6Gb=27$M(;qX@G>R7B0-z5sbArdgL-|9YTl#fD|degztM zI$&)x{P-4g7prt&#b`*UdK^*Gm(go{)*#dalI)-<)zYFsHU43JLochMDR@e&k(PW? zwreP=ErN((9ev|kn*qVa+YSBbR+&%Cq8cF#X~0u1kg5mrwve-oRZkrZrLCCo3)l_! zE-L~ST4`t&V3C#Lxq8EJ4xksTN1rE1tte3k)cJZrpV*=U`v#2&ZBzowcjX+&4s>(l zERIWpF;8zW*>~gjZxqIN6%W_vwOn#f?;tGt#7}UE?xD6ix!LFgeS-D|zF~hcz&#*W z6})X|E4(sr5;zJfn!f~9QvYzyfORYRtU(oMgv~b&M~hqAodd2R4Xmt59PaWn%n^*1 zm?h}*pmA6X!j(jcp=X9*4jsfv3ztEv^eeN_E~>PKgYnFqa=3Bqe(w#o0m;sSm2xqc zmUIfWW4>1TDp?4I_xCsAPWx6%@~#r}AjMUIuj)?Y;P>us8>c^Dh3}Z#IzTLM;uxC) z&#ea0KbSdd^meA7bT7dGYTc!OwplZ_+gp}DfZ-F|?g093j!nwuo}Z=05RQu6{!2%^Hr-wGZc@m z7AVIMG`|XK>6N{K3iCL{$sSFqe7izo&^#c16%iZfeZZ!4g9b;otFR?C_%SqiDt+DjIZB8Q6g@PlX3)bO1OZ67tjAu~+z!v!RMWK**v)axo z-70lKnFUtWSTC94u-cmp1Eo@Ez&;o3SzR>MJy=Xsgj~2i0ld?XDb>b+$D#ieyi0A_ zrf2vf6YnQ`jjpXh+o^-N>-r6OQ$)<-M$RsxhMLl&KycP$ApSC|1w~fUk=6i}h*UV^ zBVPVC{0yVTfH+4(F2AU()R&AAyqL^WmhcM)v*~+~7A0|q{2wgZr*1h25O{Me+U(-j zMpxkEJRHs%mvO*nA&#JeTZz-3iQV@7yiMVS2vM!l)|cRZd}$nv+93%alte4DB0mZP zm}z(#46G1%)SoL7zzE@0E5%X7P{^KoQSCKR9T13{$slf>HvVxnsKiNT_#_>mvRhDU zWvY`XE7vy|H~8U|p{OOc78)>k944#w`jz!-P2SjP;%j6M!n4pehr*as^UZds7-tD`^8pVlL;Chf8T_VKM$wOCr@wukIF^;x0TCpX6k<(LO~l#dn*}z zJ#F1z4!i$wN0VFppaDOM>|ARdv1DHvFnwVhE|g%b9#wc)^O<7YBcL+eTd#m1I$+s; z0ga(M*)m`Vz#!O-95_cp3o>~UncRUZr?Vv2u@!x`U#2Y^%yV|Rk#mtWl|RUNhwlUp zoE{#_jy>rkoK6Jxeh99t+{5}cU_D$0%`DBJ3aan+Aa*x9_(rKGbGJo)cmT^k!FC?k{U=blU zZ8XpVpGU9X&mGRRT}IJXsv!^A9K|GCavzUqJhZViI^*}+hL(q6gj$IOPqkRCCkNk0 zr%id|q9%91l050lX#=}+UMI+1xivq#bJ`GM1)!T`4H)ZMBEKxBi}lB4(OlZai&iO? z42tXgHcv;{O?H*6XpYu4Rxv>bUU!&Fm&>7SV?>uoS6yWNNB>7>I7+=x%+{4OMEG|3 zk`3OhnGFiW-ei}P*~p2fER_VIbP6^hXW4fw;N{q;o(oK!$ae+TG^7;C{dNycrREl^ z4_uj~bV7+MxsRN7B9bU>S&u^Ur}bw?0EU?2&~(>A!sPK-=$+(strxD#2QiuwD|Hfd z#0D+{i1ga}Y*#~EybE22x;HWu|EIVp}U8Tc}>tTLnzt2IAf*RE}Ws4X-X=)2( zWp~AdaZIBuA<083W7u0!#hbdlr8zi`naV+Hgrr8oJT&*Pf0SG5_Old(ABS-6x4h4P zDYyRC&HQII%7=hXqU#R<9e`QZ7Bn(4Jb^V%;t#^+uF4LhZ0;~4AvNp+8U>RtuAL7G z&9svrK`<+HdT0tk_#k`w@k2jHcbHmnelPjSbzuUISB)Px44l~4X5OQ)xpiN4+N1c+ z&(cL+-6`8er)la^NG5|@ z%{?j&&sQmi!U^w3Lh$FPG=VC|?)-ePxJd-?vM5vN44*GJ_%P<`2nNai1mmv(;dbTx z{dxq`wJlv{ko{mUUWpfHPzjZEI(Q7;OOF&HjcC_MzQbix3rS0Hr>yGEX281pc;Sj? z{F-o6MDYY^yt0kucJieN;eFL@zAKel+7ED~bB~f?&1uBAql!#UIzkuGl0FSStt`)8 z<*^OxTou}G@#`vuCY|;jn4bEck~lt(k=a@RAtORnqcBSRJ>5!2Ku6H6IJaFeeoTH8 zjYd$S+CloIm7~@?;jL`Cau!x$vpigh;%~p^KFAtHyFEWjN7vtWV*U$1@>jb1mufof zpSv*R6cnX=cK8xpLgakjkhPb(V#hG6yi9*~Va)#4g%SAym4%hmS~;j+Hlm!WSDdOO zOvKMAKiGv|x=l9z5LsrHpb*%GWSD?GNU zYE5jxe3X^|Mf)}(f;g9sl+e=;8ekZ6B)6$8y<%rVsDwCz-&lm zSJg|!J(=bJ5^7M8NDVtuCwykn#I;z7=EX_Q%&hBL}-3z$Dz;f zv-&mP}z_U>dMtTYR2 zaZcdG!toD9!}RJ$8merAXFZbw2EJ|EW}XD%DW6=Kl8&_@=u+{Yl0z0&rPkU2*6zH- zg@@{~*r|Cs7NFuiNP2(^p+3USVjU$vnaRb1*5}ck3%4iN5nhrbSQ#xPoh}Tg7wjYf zDv-uQda_A2H_=03$C-@+G|Uw@1h;$(p;_ETxJr4Gf*WG*N0i)3YXj3*a8EESX7|); zQFYiDIJcwy+G@+wRpvQ3lbnOBQDrv#RvV%9tTS28GS)sPg@D2Iq3sOo(s zFS?n)=EW%FBC5rfAGcbX-Gkl1qdPi$@MHxYPm%4naNq3^t@TA=T&e}jl130neq=iw zW)SlbFM1l-;SLZ;NG1FQ!=?RJphH667d)zq=ziZOl~&UYTy68QE8u4dQ7w_03-l zj_7vwT}(u+Ok|~%jUndatkulFr?8AF@5+6pN{n=#XA4=7fgeq?LTkE8D!|Z82X-rD z0yZTB)2rPKVTyNSj`QsKZWVE%Roo6tfgR(VJ_E(cM&^<*{T){3y7L&>!*&5-S#2A5 zPw=!pmcJqF0h0n;OG3!3i)qOXVufDN^tHjt?R3p9_30#~l3f zOM_rt_s>2I6kN}f9&ewYB|qI)rSL*s(j%Zm3(>^FJkmI_owba;b!Huy`p*CzioQnT zXjtA<4}41z7~{c4;ccz(AA3012`gA*?RVQUHtb+SmSQe>MS8b2#f@u!`-X2XDQgPlc53T*0-Cq+N(r zlJlpmE;rI*6`JtbB!qQz0u4TPiI)~}eIAvDnE&Tr8+@cJN;vLaQil#Xp$KnOg-1{B z-iJgeWN#HpW)EnP)UsM?Fs9w>(Bj|x4R_K=(vOBBCrwC!4^<;#i8euP6sSnFcM9Cf za|mI`d{1F%rbae5>Q^{MuQLE*(v!$8dl(qMP5(K}C#O5H9*hG7^3KY58 z(Xzzcp)a7?=rl@vN4=r=#(k5rqdbk9e30KndS&4Uj4e-iP>KfVdWvN1`ica3*GD3P z>L$YTzmXnxEfjcu60B3a7^dgdD^Jl-$>MxrUFwyM==Hn0JB3!UzaowP zkQC|{*3M6kfFrA%e*pBzc$s|Zj|lnuxn1)A5+OfUu>Ad~{=@5Inv&c+;fmBRz5&H7 zq>|%8StnZnDqk8eSD+pGR0*ZN#eg+rgUB=TD}7bupEKm=W+gM#Xel>itFC9vboXPc zub0=yj-O1GBJjKmVMe*WKB4>HKL9YApUsRKvbtuIqCd@3^he*LWk2!|_yhFV^+4lm zQIpHB`Y*00vk zh7vP?grT9yN5XTBHxW@-kbTqFQUfq=g|=AC&e!$U7D>xJYqnQSyv;vOj2L9QplBid z$ut#68pA;CXY8NKRJ!CLZl=^#&Y-DcGk?3A)7@YpyW)r`6CZy}J+ceA>p2h~yHo1L zJR+M=oW3DLEtbxkeFE87h-at;FA7yui9Z!}7So|Tl;C=vEq4H6Ed=c1Zwe8CA+{PZB}WZU)c+pqIJ#B36#?@L469dBTZl`;|^gbdVC@ebYRQ zc-#wtG&O=#4{Aok5)7(c2Lji2rYvMBg? zN{PyuBi0SEfn|qQzho@84d?X;IHrF9Q#b#o8e3mi0jJ94HzGlHO{kkGa|uBzVb|5Z zv<);zAc@0Yvh7w3(C4m?yD|RTKu-Lh@5Ucg-rw$p`(OJA;yozMN%Fjr_<_w(^}pUg z>t+jxiRXYv{>Kn*_gGoZagx?@zv6n9X1o879c%{J;P`Yx%!xe7S#BK>ji?tB_ZlZ( z1IrDB))TU3V9}z(42S~AZP0hHkT$4G?c~?LU}Lw#%&?vZMgX4PHtwzENLw|s9^F4~ zBz8`UTx$v1&1N#{X;JXX*h$esO{19k@zP9W8%CmkM)pA>uO8L^=7KJVUYws)5&IQv z{A=-qs|aC)GJaUomKHW7eYwIo@mS@Wk)(z}i8D!U$!FS|1~e20fvkA<$td2YuYm#D zLSc1sIM6Li>1IPzTIU~o*i486aRs+Uu?Jvrg{x`(XLM%~q4MgXdatEDHKFd9tm)*I ziidq*3hbUDyh&aK(ixxIQ)kFmjp|fJVXB^a&R?@A$_n{K*+Mb~hosmb!*2BkOc+pp z*qQeXMPHSL*0m#1S0kpc%ja+_nu*%_T;;(tLZe$ukRsv>M3c87{}IIj(?V`$HMN6& zOtt?IVT&X*99l93Ud@Y{WZq5p{@BCjb*y(?ZwXJ5tw}hJPBh6!X286&8VUwDBsQtc z0DQIuPHXWBJbg9=K7{JVQ2fb&xW*f^wHCKnZ=bw@wLGFROfm3D{&W{%j;=vi#tzw3 z+VI@%hHAi#%`&dO^KqeSUN@wGDCD@Rw?QXlIHq^Fsu!9*XUlx3e#@i(WLS2jj&Y@; z&^X+ni$B zKStSPmsTCr^N@O}_(W#1%% zXlj6HsM>=_*1yiXiDgF092x5N(t~#7HSOr4+qiiy@l=443>(^~Ttbij#4Osgj4S|t zUYUpTzJK37KcZ;mbXQ%^X;y=B>F4!l-qiFLTgx{~j=XVG164yOCALpK^II4-HRk})^YV+d(uLq58V zf_JWI4&x>zddl5USxqYt&?8I4FqGR62Qct=ha(J%;Ocm4UG`TkUMct~1**rSbYpMT$wm5qa;tnkH$Zu5zIQmyk@@VPJa>jnEa|Dh`^s7^tqvMnRGbHoet5ssg9UTI8ZNW z>F;9_kf24kU`dX0P<(zqO=tIfeSZG>>1@*(3E+kwP7aPo>6=1riSYeyNq6w8`;}6K z;!i!{`y0JJ8|eOFbc<0!QbQ#30(>})?-bLj@WkW#ng{(Z)!C}MdTX{~B|m3GW$s5r zY(y(s14dKpGS)+~zkZCcFfT&_b{CMcex?e|CYoSmY-i+RP?w_)#PI@<YGb$i=Y6> zEP|%u2Ym2)!y7LtTO<$>(0k~I2C2bh)nf`jh1!r^3ErRsu}wskVoi~*8m6qFksCXL%y4*J2WG0Z;zrxnqu}USxny(m4-l)g%k9KApe354njFT zngK!_jBg=mCD^I%MoGPT7?WkSXuD_?y`z)h)Kw(lzh=8yHK4(QeVqhzrvK?4nXQA@ zJKcu3)~d}eTHuTEQwHO3{MjrPAm#hr`mW=c@|<1ku31D@JbZ$}0es`Tmj#LT zOw2j!5xv|fH~LU=e3g0yHkyQ&VbcBrCPc>4^zD$@o1P!Ui9^P=Elbf) zvN`b_ne2N0z&$LoiwmC|`;3i{9o?GNG7w7Yb&YAe3_T;u81m6C$)MjX=hL#QEUs{* zUnOP%13IC~8a$FC!PF`35aAiR=0jhmdI7zt0%11bFHJtT3%%4)rk}wWGm@R=+)()v z09T2vz#LgctuJS_ z57sR-<Rk+^E9(d`8vwL7|W&f|U)Xp3MPfP+=Tr@lyOz z#Bs=!UDiXO+R`me%#L-@sUQaPieo_XZDDt(i0TSty?)fruyWhCnG^}i-RyMNgw`JW_R`YZ@U7b4_6UEoU(G<0REcCed9e{!yM(*MM{VgNw+ z5`TBqt{E0cs$m*bW3Gyq_~2ZtX;@>bB+7p>HTD=SpMEBW?~+T;m6EaS)rqq?0lUo` zjG&Q3?Df4Hx4zMUS3$%g(?x7et65I6|ACQpKMu)bo6qo7P()?;`^gWf9!36GcF4kS z7wSB-HE-a~u70z>aO)2e(5?S2=~DbXZv7Wd{vS4%|J5>f}!){@A=( zlVMgS!wm-qU_Y4`V#keC6$p<2JqQXUD)gc)=Gai~H>K0SKQDM;d=v0d{(aaR^s%ls zmp=o<3VGV)PrH6pJNF|ahx{BC0-1XJ2lYLG$Y!*?r zWzg@Xb8I+KU!~n1;j;nf+egmO&6NlscqndrXGLz`>P zJxbkcL?Th?27$+=r0s#FpPoj4x?)}O%1=y|m)1aXyooNks?>mw{<>$w!7)XY{Z$do z=62ERlvbw(0|c)m@xHRTqtPrC6eIoswSflBt7pep*3T%+M7@X0PD?A zQLxNQWro~zU?9u3B2RQa%X+1zCs3MfK8*&-%{3+_v!tAeL=wgN2oY%c-Gx2WE^M2+ z{!JHTz2v70*cSok>dmViRw->rCg^q6k6?pTGeH|Lt%z?MV6-SvHI@ev{_*F&POdh*4Ylcz?A$)1;UW}zQVQoxTmW7BX7JcN5C(6V;TwojLfCYnY| z6&gmRBlt|^zvBp~bn>1)AvR_R9dD>r4*90C_bgZ+@BvXoA+uy!rovD-2oy;A?3pG) zPK3+|04`T4EJHKM6Rs7CVcrW_!nBdKzJTlAIp;4F(hdp`WpDrG9APvF zjl2ATw&%fr7jXVRHm3a5_xe*S{67o4|MC8Bck8c)$MH(obgmz$k!TZ$5N4n(Fb}aj zfeIH7#5W=Nq6mp1mG>uqo#V8&64N%rM3rHy zRs&EFf0v(adLvp2LbQT)3fZA$iMVC-{O1T`B#DASIJ5A#bSq_Yx-twPqac%4{Gjf+ zm5p}qXqquNP1^TNyjl86FMjRQn`8NFG{%PI`i}OtqK;5iFpM#4sy+f2S2M%JuW9Dz zuNk4cfxso*bw#S~Wv9&kvzF!X9P{6w;?EvoxL*p*bZH8x^xdy&`^A&BpLedq94!N% zJ$5}|Xnbypxc9$v#B*CLw@vR#Letdefk)X-Qu|zuR5#XWAC(UuvQbFRfA?X9IvR}A zR!S5Qfd0O97l7QHpd3ggj?JEzNnSfOKwC|63zsy$Bcuf7-Lpp?e^3DhWCcE~m(;k7 zM9@(&AfiWSa!RIWpRvOCDZ%H|888koc#4`l7Y@5Mh|m5pP~$`m=iEf9i{a{k0K(Mm zhaOzadw7!hEu@}9;Fd@3T(553UO-TAMBvVoP|jzjFCeG_UkEC_ZNMyi!}4dB0LCPF z3wUccn&7j)M#u4lDa&nl(Rj%S0_IsEHUKIS1Xl16v~XRC++58Y0V=NuU@;$swCUV2 z{y_&;U&ykmd>n0>-%hIkuej@fwCnzoy8pSsn=xonuSU3Zsdmmp_Lw)c_6uKz%#)NT z4jfW3up4R2sGQ*ZM-eH%q^Zv>jr0QWDxK#zUxyN`9Q+#eD{dWgbHE?(hh(3CaF!F#lZZ2oI%=i7`Hh(0(y`7w-uIpa@3I2b z%wc^|?ud$B)enol+L&vStwZi}2n=Bj{Lq5vkJ4tEH>m^D{IaTzymaj--8SI!v7ux8 zV?&3H<}-IoAVe{!00mnoTGr)+o^Nz>91b~D6dks7eC12xoQv;DFlVH%iK4R&!_x7v z7VnL^Kwonk-by-{oLPaEge6&d{o{t3x%XuIV(-=k0RpM&)w&0`<(KYK-%2~j7kB4T zdrqCGaniGvJI$Sz2ODsO{%gI;tzha{HZ;85Z3ayqf64THv~|Gu>VeBqN$RWEG8k7~ z&n9P36Ux+XCjLhvAoQ7sGApJ?!(f0A)R)k*B?f?T8gTq})I=TTE>W?9Y6l%4yivs1 zi1(pg#LCzKrFnoNu_%5685?%tPe_!~3vAsF)-vxD?L%*hK?Z5hKw<)rYDeV3p1(T% z6l+Jd?H`rl=5Lkr|4XskKS#WOK^lLLcJjv(AMOTk`3-&!{Cs>Cq-&L^2i{wCfhfTI zkPv9lFRbQ@%1WxM7V>Vu>m5WFRD`75h`KMEuwT3TsV`{_TZnIXmU^OZMkEZ|u16WW zPZ~78asZ%ps=;A9AwS4Hz<(1nxQeLlI$8pvdlz#h4bCsq_oJx2%x-dFY%ksqMSc4D zmRpE=f++8{&EGw8gn~nDvIFHBx&bYC1~Q3%ZbYb74xkC6j#Z!3z=E$)!ZNKFAt zr#HdrQ6mxXphwe2Yekx!;9XL$;$-^5&` zS@g?=Sq>Ffx#9j;YQ=Y}xdt+HTc(u*Pt4A1HzQQ`l6`n@$j9us$pR>4N5iQZ4ET>$c#^_FCxI3!(PJ8^`<8V`KR%3Lhz9ezrD>hRC1Eo}=m{6SX;@}cO~dL`-|+A3l1sd{;|og*c~_i?OUSwm6x zbMIv_k32VKGx4BxBiASTsQP@Kx3U+6CXkFSv%ahCxJG1$SVFRUrov|RbbiA+EcznMb8fVaSoU&lL zoNheqaaj!CKxuX=mr2wIu^hgb%m39AQV5qIvHrN_zQ4sczu~?4ZEP%U{#mHyE+cOF zF*odzwy6e5xq_m9ow%1RUA*;Bw14k23JCTNo?ABwL%eHRw9yV@W%kaE! z=@g2M|)LwAp^eU4|f-?O;+_t z$v~))s7EYY5W7FaWAT&YmW1mAB~FP4#Qvg#{d^k|QU581^_wHVR(uxFx{+_`xSeREG-Vz%Qo*?)<*b-Y@WIIWM2QPXo zB?))C;|@)m;)xxA8zm-%zYrFniJg+-LQbl}7QLD|3hR4haYyyIECNtId2MLqi;-6A zprw@8F5Fx(;dskTpf@9v_Zj8QkGKHOj@Cvg=lVnbFEVpLsf?;MX9L?@H`^UsH`Tpj zcx6~AVVDG$X@jvR)xnk@WDh{F@~t9nqsl9V%Zz}XSTty?CtfBgEUCnO4;D#UeiX@| zB>Pqh<0@sKpOk*-5imZmod;Q*B|#UuML5q3)Hbsd(}?;RBUV1#HER{;HV7ldA}*t* zP3vg-2h&$pMcwFJt#Zk(%Ahi&iD&)$BSPjmn>LMMNK9>?GI zdHxHY|BLth=cN#T;Q4>^{_h!1!SoMDE%s$c(^$Z`1TYS#SOsD33wb{5E+{$>11{-) zs(kYvE+LcTA1?r&5pG`SX_ucLaxl;Ht5A}u&5aG0RxQq$KU({)(Jyb;+o(U#OZzr_ zI75H@_{IZu4D!lFe*V={<`(Z#s#;E#TqGZ#Qu*jDSs@h%>0CjSi7N*nBW;snbBCu+y|{n<*f$nqgqFj7A(|E>O}YFKq)6mPj_K zam!Y{G4Yax1MWP^Ao=_W9GoB#?+--kpSC;XveLcwMKcOz769Po$h5ca%4FAh*Z{2u zIRqs=Sx_!hq8DLURm!x;>SPTR71W?;gej~fG&j1DU#pj)RMQ{p9Y(5Oj2@}UH(<_a zfv???v{??Dv|-!G3!2726~!_-EgifimC*@wY;t>_39 zH@!eGdZKk!U`V7vUA&?jbRA3CbtC!Ht_jC8W2@f9OeCsk>ah{*&i_RUB30z!r}yhPFkF^!qLu)qyRE2R|@!FE&F zEUiu^iUH{|vj&gmy$6yv?FIg;Pm-wBRC*@K$YoN!s6iV^yx$aB+=H`1HpZSL5EIw1 zFs=F)MTz4$dRvDlvPhFP1Xd<;!m_r{4)k=5TqbRK3oy8^A=n%zemZgV

&s~Kxl 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%0AeH!-c@Rh>yOzQGv)~d^ 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%0A1>qC+!}JVdBCSnxdi9sFM5mqi~V!oB%v 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