diff --git a/.cspell.json b/.cspell.json index a0ec0be6..abe743cb 100644 --- a/.cspell.json +++ b/.cspell.json @@ -73,9 +73,26 @@ "pkcs", "prioritise", "Prioritise", + "prioritised", "Timeto", "Wdex", - "jacoco" + "jacoco", + "serialise", + "serialised", + "serialises", + "serialising", + "normalise", + "Normalise", + "normalised", + "normalises", + "Normalises", + "normalising", + "behaviour", + "Behaviour", + "behaviours", + "sanitisation", + "recognised", + "unrecognised" ], "languageSettings": [ { @@ -98,7 +115,9 @@ "src/main/java/com/skyflow/generated/**", "**/*.ts", "**/processed-*", - "samples/src/main/java/com/example/credentials.json" + "samples/src/main/java/com/example/credentials.json", + "RUNNING_SAMPLES.md", + "docs/superpowers/**" ], "ignoreRegExpList": [ "/\\b[A-Z][A-Z0-9_]{2,}\\b/g", @@ -106,6 +125,7 @@ "/(eyJ[A-Za-z0-9+/=_-]+\\.)+[A-Za-z0-9+/=_-]+/g", "/[A-Za-z0-9_.~-]*%[0-9A-Fa-f]{2}[A-Za-z0-9_.~%-]*/g", "/\\b[A-Za-z0-9_]{7,}\\b(?=])/g", - "/\"[A-Za-z0-9+/=]{15,}\"/g" + "/\"[A-Za-z0-9+/=]{15,}\"/g", + "/-D[A-Za-z][A-Za-z0-9.]*/g" ] } diff --git a/.gitignore b/.gitignore index d5a178d4..0efc6bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .idea -target \ No newline at end of file +target + +RUNNING_SAMPLES.md + +docs/superpowers/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 10606856..9cf57026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,103 +1,5 @@ # Changelog -All notable changes to this project will be documented in this file. -## [1.15.0] - 2024-08-01 -### Added -- insert data using bulk operation `insertBulk` +All notable changes to this project will be documented as part of the release notes. -## [1.14.0] - 2024-02-01 -### Fixed -- handling of detokenize response to avoid breaking changes. - -## [1.13.0] - 2024-01-10 -### Added -- Continue on error support for batch Insert. - -## [1.12.1] - 2023-11-09 -### Fixed -- Static Bearer token being used for multiple Skyflow Client instances. - -## [1.12.0] - 2023-10-25 -### Added -- `tokens` support in Get Method - -## [1.11.0] - 2023-09-01 -### Added -- `query` vault API - -## [1.10.0] - 2023-08-09 -- Added `delete` vault API support. -## [1.9.0] - 2023-06-08 -### Added -- `redaction` key for detokenize method for column group support. - -## [1.8.2] - 2023-03-20 -### Fixed -- removed grace period logic for bearer token generation. - -## [1.8.1] - 2023-03-01 -### Fixed -- java cached token bug - -## [1.8.0] - 2023-01-10 -### Added -- `update` vault API -- `get` vault API - -## [1.7.1] - 2022-11-29 -### Changed -- `setContext` to `setCtx` method. -- `setTimetoLive` accepts seconds in `Integer` instead of `Double`. - -## [1.7.0] - 2022-11-22 -### Added -- `upsert` support for insert method. - -## [1.6.0] - 2022-10-11 - -### Added -- Added Support for Context Aware Authorization. -- Added Support to generate scoped skyflow bearer tokens. -## [1.5.0] - 2022-04-12 - -### Added -- support for application/x-www-form-urlencoded and multipart/form-data content-type's in connections. - -## [1.4.1] - 2022-03-29 - -### Fixed -- Request headers not getting overridden due to case sensitivity - -## [1.4.0] - 2022-03-15 - -### Changed - -- deprecated `isValid` in favour of `isExpired` - -## [1.3.0] - 2022-02-24 - -### Added - -- `requestId` in error logs and error responses for API Errors -- `isValid` method for validating Service Account bearer token - -## [1.2.0] - 2022-01-11 - -### Added -- Logging functionality -- `Configuration.setLogLevel` function for setting the package-level LogLevel -- `generateBearerTokenFromCreds` function which takes credentials as string - -### Changed -- Renamed and deprecated `GenerateToken` in favor of `generateBearerToken` -- `vaultID` and `vaultURL` are optional in `SkyflowConfiguration` constructor - -## [1.1.0] - 2021-11-10 -### Added -- `insert` vault API -- `detokenize` vault API -- `getById` vault API -- `invokeConnection` -## [1.0.1] - 2021-10-20 -### Added -- Service Account Token generation \ No newline at end of file +See [GitHub](https://github.com/skyflowapi/skyflow-java/releases) or [Maven](https://mvnrepository.com/artifact/com.skyflow/skyflow-java) for more details on each released version. diff --git a/README.md b/README.md index 3f8a9adb..e58b6fc7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Skyflow Java +> **This is the current, recommended version of the Skyflow SDK.** V2.1.0 brings flexible auth, multi-vault support, builder patterns, native data types, and rich error diagnostics. +> +> Migrating from v1? See the **[Migration Guide](add link)** for step-by-step instructions. V1 is in maintenance mode and will reach End of Life on October 31, 2026. + The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) @@ -15,12 +19,7 @@ The Skyflow Java SDK is designed to help with integrating Skyflow into a Java ba - [Configuration](#configuration) - [Gradle users](#gradle-users) - [Maven users](#maven-users) -- [Migration from v1 to v2](#migration-from-v1-to-v2) - - [Authentication options](#authentication-options) - - [Initializing the client](#initializing-the-client) - - [Request & response structure](#request--response-structure) - - [Request options](#request-options) - - [Error structure](#error-structure) +- [Migration from v1 to v2](docs/migrate_to_v2.md) - [Quickstart](#quickstart) - [Authenticate](#authenticate) - [Initialize the client](#initialize-the-client) @@ -94,265 +93,7 @@ Add this dependency to your project's `pom.xml` file: # Migrate from v1 to v2 -Below are the steps to migrate the java sdk from v1 to v2. - -### Authentication options - -In V2, we have introduced multiple authentication options. You can now provide credentials in the following ways: - -- Passing credentials in ENV. (`SKYFLOW_CREDENTIALS`) _(Recommended)_ -- API Key -- Path to your credentials JSON file -- Stringified JSON of your credentials -- Bearer token - -These options allow you to choose the authentication method that best suits your use case. - -**V1 (Old)** - -```java -static class DemoTokenProvider implements TokenProvider { - @Override - public String getBearerToken() throws Exception { - ResponseToken res = null; - try { - String filePath = ""; - res = Token.generateBearerToken(filePath); - } catch (SkyflowException e) { - e.printStackTrace(); - } - return res.getAccessToken(); - } -} -``` - -**V2 (New): Passing one of the following:** - -```java -// Option 1: API Key (Recommended) -Credentials skyflowCredentials = new Credentials(); -skyflowCredentials.setApiKey(""); // Replace with your actual API key - -// Option 2: Environment Variables (Recommended) -// Set SKYFLOW_CREDENTIALS in your environment - -// Option 3: Credentials File -skyflowCredentials.setPath(""); // Replace with the path to credentials file - -// Option 4: Stringified JSON -skyflowCredentials.setCredentialsString(""); // Replace with the credentials string - -// Option 5: Bearer Token -skyflowCredentials.setToken(""); // Replace with your actual authentication token. -``` - -Notes: - -- Use only ONE authentication method. -- API Key or environment variables are recommended for production use. -- Secure storage of credentials is essential. -- For overriding behavior and priority order of credentials, please refer to [Initialize the client](#initialize-the-client) section in [Quickstart](#quickstart). - ---- - -### Initializing the client - -In V2, we have introduced a builder design pattern for client initialization and added support for multi-vault. This allows you to configure multiple vaults during client initialization. In V2, the log level is tied to each individual client instance. During client initialization, you can pass the following parameters: - -- `vaultId` and `clusterId`: These values are derived from the vault ID & vault URL. -- `env`: Specify the environment (e.g., SANDBOX or PROD). -- `credentials`: The necessary authentication credentials. - -**V1 (Old)** - -```java -// DemoTokenProvider class is an implementation of the TokenProvider interface -DemoTokenProvider demoTokenProvider = new DemoTokenProvider(); -SkyflowConfiguration skyflowConfig = new SkyflowConfiguration("","", demoTokenProvider); -Skyflow skyflowClient = Skyflow.init(skyflowConfig); -``` - -**V2 (New)** - -```java -Credentials credentials = new Credentials(); -credentials.setPath(""); // Replace with the path to the credentials file - -// Configure the first vault (Blitz) -VaultConfig config = new VaultConfig(); -config.setVaultId(""); // Replace with the ID of the first vault -config.setClusterId(""); // Replace with the cluster ID of the first vault -config.setEnv(Env.DEV); // Set the environment (e.g., DEV, STAGE, PROD) -config.setCredentials(credentials); // Associate the credentials with the vault - -// Set up credentials for the Skyflow client -Credentials skyflowCredentials = new Credentials(); -skyflowCredentials.setPath(""); // Replace with the path to another credentials file - -// Create a Skyflow client and add vault configurations -Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.DEBUG) // Enable debugging for detailed logs - .addVaultConfig(config) // Add the first vault configuration - .addSkyflowCredentials(skyflowCredentials) // Add general Skyflow credentials - .build(); -``` - -**Key Changes:** - -- `vaultUrl` replaced with `clusterId`. -- Added environment specification (`env`). -- Instance-specific log levels. - ---- - -### Request & response structure - -In V2, we have removed the use of JSON objects from a third-party package. Instead, we have transitioned to accepting native ArrayList and HashMap data structures and adopted the builder pattern for request creation. This request needs: - -- `table`: The name of the table. -- `values`: An array list of objects containing the data to be inserted. - -The response will be of type `InsertResponse` class, which contains `insertedFields` and `errors`. - -**V1 (Old):** Request building - -```java -JSONObject recordsJson = new JSONObject(); -JSONArray recordsArrayJson = new JSONArray(); - -JSONObject recordJson = new JSONObject(); -recordJson.put("table", "cards"); - -JSONObject fieldsJson = new JSONObject(); -fields.put("cardNumber", "41111111111"); -fields.put("cvv", "123"); - -recordJson.put("fields", fieldsJson); -recordsArrayJson.add(record); -recordsJson.put("records", recordsArrayJson); -try { - JSONObject insertResponse = skyflowClient.insert(records); - System.out.println(insertResponse); -} catch (SkyflowException exception) { - System.out.println(exception); -} -``` - -**V2 (New):** Request building - -```java -ArrayList> values = new ArrayList<>(); -HashMap value = new HashMap<>(); -value.put("", ""); // Replace with column name and value -value.put("", ""); // Replace with another column name and value -values.add(values); - -ArrayList> tokens = new ArrayList<>(); -HashMap token = new HashMap<>(); -token.put("", ""); // Replace with the token for COLUMN_NAME_2 -tokens.add(token); - -InsertRequest insertRequest = InsertRequest.builder() - .table("") // Replace with the table name - .continueOnError(true) // Continue inserting even if some records fail - .tokenMode(TokenMode.ENABLE) // Enable BYOT for token validation - .values(values) // Data to insert - .tokens(tokens) // Provide tokens for BYOT columns - .returnTokens(true) // Return tokens along with the response - .build(); -``` - -**V1 (Old):** Response structure - -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - } - ] -} -``` - -**V2 (New):** Response structure - -```json -{ - "insertedFields": [ - { - "card_number": "5484-7829-1702-9110", - "request_index": "0", - "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" - } - ], - "errors": [] -} -``` - ---- - -### Request options - -In V2, with the introduction of the builder design pattern has made handling optional fields in Java more efficient and straightforward. - -**V1 (Old)** - -```java -InsertOptions insertOptions = new InsertOptions(true); -``` - -**V2 (New)** - -```java -InsertRequest upsertRequest = InsertRequest.builder() - .table("") // Replace with the table name - .continueOnError(false) // Stop inserting if any record fails - .tokenMode(TokenMode.DISABLE) // Disable BYOT - .values(values) // Data to insert - .returnTokens(false) // Do not return tokens - .upsert("") // Replace with the column name used for upsert logic - .build(); -``` - ---- - -### Error structure - -In V2, we have enriched the error details to provide better debugging capabilities. -The error response now includes: - -- `httpStatus`: The HTTP status code. -- `grpcCode`: The gRPC code associated with the error. -- `details` & `message`: A detailed description of the error. -- `requestId`: A unique request identifier for easier debugging. - -**V1 (Old):** Error structure - -```json -{ - "code": "", - "description": "" -} -``` - -**V2 (New):** Error structure - -```js -{ - "httpStatus": "", - "grpcCode": , - "httpCode": , - "message": "", - "requestId": "", - "details": ["
"] -} -``` +Upgrading from v1? See the dedicated migration guide: **[docs/migrate_to_v2.md](docs/migrate_to_v2.md)** # Quickstart diff --git a/docs/migrate_to_v2.md b/docs/migrate_to_v2.md new file mode 100644 index 00000000..55facd45 --- /dev/null +++ b/docs/migrate_to_v2.md @@ -0,0 +1,252 @@ +# Skyflow Java SDK — V1 to V2 Migration Guide + +This guide covers the steps to migrate the Skyflow Java SDK from v1 to v2. + +--- + +## Authentication options + +In V2, multiple authentication options are available. You can now provide credentials in the following ways: + +- Environment variable (`SKYFLOW_CREDENTIALS`) _(Recommended)_ +- API Key +- Path to credentials JSON file +- Stringified JSON of credentials +- Bearer token + +**V1 (Old)** + +```java +static class DemoTokenProvider implements TokenProvider { + @Override + public String getBearerToken() throws Exception { + ResponseToken res = null; + try { + String filePath = ""; + res = Token.generateBearerToken(filePath); + } catch (SkyflowException e) { + e.printStackTrace(); + } + return res.getAccessToken(); + } +} +``` + +**V2 (New): Choose one of the following:** + +```java +// Option 1: API Key (Recommended) +Credentials skyflowCredentials = new Credentials(); +skyflowCredentials.setApiKey(""); + +// Option 2: Environment Variable (Recommended) +// Set SKYFLOW_CREDENTIALS in your environment + +// Option 3: Credentials File +skyflowCredentials.setPath(""); + +// Option 4: Stringified JSON +skyflowCredentials.setCredentialsString(""); + +// Option 5: Bearer Token +skyflowCredentials.setToken(""); +``` + +> **Notes:** +> - Use only ONE authentication method per credentials object. +> - API Key or environment variable are recommended for production. +> - For priority order see [Quickstart — Initialize the client](../README.md#initialize-the-client). + +--- + +## Initializing the client + +V2 introduces a builder pattern for client initialization with multi-vault support. + +**Key changes:** +- `vaultUrl` replaced with `clusterId` (derived from vault URL) +- Added `env` specification (e.g. `Env.PROD`, `Env.SANDBOX`) +- Log level is now per-client-instance + +**V1 (Old)** + +```java +DemoTokenProvider demoTokenProvider = new DemoTokenProvider(); +SkyflowConfiguration skyflowConfig = new SkyflowConfiguration( + "", "", demoTokenProvider +); +Skyflow skyflowClient = Skyflow.init(skyflowConfig); +``` + +**V2 (New)** + +```java +Credentials credentials = new Credentials(); +credentials.setPath(""); + +VaultConfig config = new VaultConfig(); +config.setVaultId(""); +config.setClusterId(""); +config.setEnv(Env.PROD); +config.setCredentials(credentials); + +Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.DEBUG) + .addVaultConfig(config) + .build(); +``` + +--- + +## Request and response structure + +V2 removes third-party JSON objects in favour of native `ArrayList` and `HashMap` with a builder pattern for requests. + +**V1 (Old) — Request** + +```java +JSONObject recordsJson = new JSONObject(); +JSONArray recordsArrayJson = new JSONArray(); +JSONObject recordJson = new JSONObject(); +recordJson.put("table", "cards"); +JSONObject fieldsJson = new JSONObject(); +fieldsJson.put("cardNumber", "41111111111"); +fieldsJson.put("cvv", "123"); +recordJson.put("fields", fieldsJson); +recordsArrayJson.add(recordJson); +recordsJson.put("records", recordsArrayJson); +try { + JSONObject insertResponse = skyflowClient.insert(records); +} catch (SkyflowException e) { + System.out.println(e); +} +``` + +**V2 (New) — Request** + +```java +HashMap value = new HashMap<>(); +value.put("", ""); +value.put("", ""); +ArrayList> values = new ArrayList<>(); +values.add(value); + +InsertRequest insertRequest = InsertRequest.builder() + .table("") + .values(values) + .returnTokens(true) + .build(); + +InsertResponse response = skyflowClient.vault().insert(insertRequest); +``` + +**V1 (Old) — Response** + +```json +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" + } + } + ] +} +``` + +**V2 (New) — Response** + +```json +{ + "insertedFields": [ + { + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "card_number": "5484-7829-1702-9110", + "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" + } + ], + "errors": null +} +``` + +--- + +## Request options + +V2 builder pattern replaces V1 options objects. + +**V1 (Old)** + +```java +InsertOptions insertOptions = new InsertOptions(true); +``` + +**V2 (New)** + +```java +InsertRequest request = InsertRequest.builder() + .table("") + .values(values) + .continueOnError(false) + .tokenMode(TokenMode.DISABLE) + .returnTokens(false) + .upsert("") + .build(); +``` + +--- + +## Error structure + +V2 provides richer error details for easier debugging. + +**V1 (Old)** + +```json +{ + "code": "", + "description": "" +} +``` + +**V2 (New)** + +```json +{ + "httpStatus": "", + "grpcCode": "", + "httpCode": "", + "message": "", + "requestId": "", + "details": ["
"] +} +``` + +--- + +## Credential field names (v2.1+) + +The credentials JSON file field names are updated to follow Java camelCase conventions. Both old and new forms are permanently accepted. + +| Old form (still accepted) | New form (preferred) | +|---|---| +| `clientID` | `clientId` | +| `keyID` | `keyId` | +| `tokenURI` | `tokenUri` | + +--- + +## Response field names (v2.1+) + +Response maps now return `skyflowId` (camelCase). The legacy `skyflow_id` key is still present for backward compatibility but is deprecated. + +| Deprecated (still returned) | Preferred | +|---|---| +| `skyflow_id` | `skyflowId` | + +--- + +For the full list of changes see [CHANGELOG.md](../CHANGELOG.md). diff --git a/src/main/java/com/skyflow/VaultClient.java b/src/main/java/com/skyflow/VaultClient.java index e3352f19..76de35e8 100644 --- a/src/main/java/com/skyflow/VaultClient.java +++ b/src/main/java/com/skyflow/VaultClient.java @@ -122,7 +122,7 @@ protected V1DetokenizePayload getDetokenizePayload(DetokenizeRequest request) { return V1DetokenizePayload.builder() .continueOnError(request.getContinueOnError()) - .downloadUrl(request.getDownloadURL()) + .downloadUrl(request.getDownloadUrl()) .detokenizationParameters(recordRequests) .build(); } diff --git a/src/main/java/com/skyflow/errors/ErrorMessage.java b/src/main/java/com/skyflow/errors/ErrorMessage.java index fc222522..a99dd1cc 100644 --- a/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -58,13 +58,10 @@ public enum ErrorMessage { TableKeyError("%s0 Validation error. 'table' key is missing from the payload. Specify a 'table' key."), EmptyTable("%s0 Validation error. 'table' can't be empty. Specify a table."), ValuesKeyError("%s0 Validation error. 'values' key is missing from the payload. Specify a 'values' key."), - EmptyValues("%s0 Validation error. 'values' can't be empty. Specify values."), EmptyKeyInValues("%s0 Validation error. Invalid key in values. Specify a valid key."), - EmptyValueInValues("%s0 Validation error. Invalid value in values. Specify a valid value."), TokensKeyError("%s0 Validation error. 'tokens' key is missing from the payload. Specify a 'tokens' key."), EmptyTokens("%s0 Validation error. The 'tokens' field is empty. Specify tokens for one or more fields."), EmptyKeyInTokens("%s0 Validation error. Invalid key tokens. Specify a valid key."), - EmptyValueInTokens("%s0 Validation error. Invalid value in tokens. Specify a valid value."), EmptyUpsert("%s0 Validation error. 'upsert' key can't be empty. Specify an upsert column."), HomogenousNotSupportedWithUpsert("%s0 Validation error. 'homogenous' is not supported with 'upsert'. Specify either 'homogenous' or 'upsert'."), TokensPassedForTokenModeDisable("%s0 Validation error. 'tokenMode' wasn't specified. Set 'tokenMode' to 'ENABLE' to insert tokens."), diff --git a/src/main/java/com/skyflow/logs/ErrorLogs.java b/src/main/java/com/skyflow/logs/ErrorLogs.java index e6e8b304..47866efc 100644 --- a/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -50,15 +50,12 @@ public enum ErrorLogs { TABLE_IS_REQUIRED("Invalid %s1 request. Table is required."), EMPTY_TABLE_NAME("Invalid %s1 request. Table name can not be empty."), VALUES_IS_REQUIRED("Invalid %s1 request. Values are required."), - EMPTY_VALUES("Invalid %s1 request. Values can not be empty."), - EMPTY_OR_NULL_VALUE_IN_VALUES("Invalid %s1 request. Value can not be null or empty in values for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_VALUES("Invalid %s1 request. Key can not be null or empty in values"), EMPTY_UPSERT("Invalid %s1 request. Upsert can not be empty."), HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT("Invalid %s1 request. Homogenous is not supported when upsert is passed."), TOKENS_NOT_ALLOWED_WITH_TOKEN_MODE_DISABLE("Invalid %s1 request. Tokens are not allowed when tokenMode is DISABLE."), TOKENS_REQUIRED_WITH_TOKEN_MODE("Invalid %s1 request. Tokens are required when tokenMode is %s2."), EMPTY_TOKENS("Invalid %s1 request. Tokens can not be empty."), - EMPTY_OR_NULL_VALUE_IN_TOKENS("Invalid %s1 request. Value can not be null or empty in tokens for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_TOKENS("Invalid %s1 request. Key can not be null or empty in tokens."), INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT("Invalid %s1 request. For tokenMode as ENABLE_STRICT, tokens should be passed for all fields."), MISMATCH_OF_FIELDS_AND_TOKENS("Invalid %s1 request. Keys for values and tokens are not matching."), diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java index f71fc416..4391f1b1 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/src/main/java/com/skyflow/logs/InfoLogs.java @@ -14,7 +14,7 @@ public enum InfoLogs { // Bearer token generation EMPTY_BEARER_TOKEN("Bearer token is empty."), - BEARER_TOKEN_EXPIRED("Bearer token is expired."), + BEARER_TOKEN_EXPIRED("Bearer token is invalid or expired."), GET_BEARER_TOKEN_TRIGGERED("getBearerToken method triggered."), GET_BEARER_TOKEN_SUCCESS("Bearer token generated."), GET_SIGNED_DATA_TOKENS_TRIGGERED("getSignedDataTokens method triggered."), @@ -95,7 +95,11 @@ public enum InfoLogs { GET_DETECT_RUN_TRIGGERED("Get detect run method triggered."), VALIDATE_GET_DETECT_RUN_REQUEST("Validating get detect run request."), REIDENTIFY_TEXT_SUCCESS("Text data re-identified."), - ; + + // Deprecation warnings — v2 backward compat + DEPRECATED_SKYFLOW_ID_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), + DEPRECATED_DOWNLOAD_URL("[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead."); + private final String log; diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java index 9e3a6d63..4190f1de 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +++ b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java @@ -99,30 +99,40 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage()); } - JsonElement clientID = credentials.get("clientID"); - if (clientID == null) { + // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); } - JsonElement keyID = credentials.get("keyID"); - if (keyID == null) { + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); } - JsonElement tokenURI = credentials.get("tokenURI"); - if (tokenURI == null) { + JsonElement tokenUri = credentials.get("tokenUri"); + if (tokenUri == null) { + tokenUri = credentials.get("tokenURI"); + } + if (tokenUri == null) { LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); } PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); String signedUserJWT = getSignedToken( - clientID.getAsString(), keyID.getAsString(), tokenURI.getAsString(), pvtKey, context + clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context ); - String basePath = Utils.getBaseURL(tokenURI.getAsString()); + String basePath = Utils.getBaseURL(tokenUri.getAsString()); API_CLIENT_BUILDER.url(basePath); ApiClient apiClient = API_CLIENT_BUILDER.token("token").build(); AuthenticationClient authenticationApi = apiClient.authentication(); @@ -145,15 +155,15 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( } private static String getSignedToken( - String clientID, String keyID, String tokenURI, PrivateKey pvtKey, Object context + String clientId, String keyId, String tokenUri, PrivateKey pvtKey, Object context ) { final Date createdDate = new Date(); final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000)); io.jsonwebtoken.JwtBuilder builder = Jwts.builder() - .claim("iss", clientID) - .claim("key", keyID) - .claim("aud", tokenURI) - .claim("sub", clientID) + .claim("iss", clientId) + .claim("key", keyId) + .claim("aud", tokenUri) + .claim("sub", clientId) .expiration(expirationDate); if (context != null) { diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java index 0ce14007..b2c28ea1 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +++ b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java @@ -100,20 +100,27 @@ private static List generateSignedTokensFromCredentials throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage()); } - JsonElement clientID = credentials.get("clientID"); - if (clientID == null) { + // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); } - JsonElement keyID = credentials.get("keyID"); - if (keyID == null) { + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); } PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); signedDataTokens = getSignedToken( - clientID.getAsString(), keyID.getAsString(), pvtKey, dataTokens, timeToLive, context); + clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context); } catch (RuntimeException e) { LogUtil.printErrorLog(ErrorLogs.SIGNED_DATA_TOKENS_REJECTED.getLog()); throw new SkyflowException(e); @@ -122,7 +129,7 @@ private static List generateSignedTokensFromCredentials } private static List getSignedToken( - String clientID, String keyID, PrivateKey pvtKey, + String clientId, String keyId, PrivateKey pvtKey, ArrayList dataTokens, Integer timeToLive, Object context ) { final Date createdDate = new Date(); @@ -139,8 +146,8 @@ private static List getSignedToken( io.jsonwebtoken.JwtBuilder builder = Jwts.builder() .claim("iss", "sdk") .claim("iat", (createdDate.getTime() / 1000)) - .claim("key", keyID) - .claim("sub", clientID) + .claim("key", keyId) + .claim("sub", clientId) .claim("tok", dataToken) .expiration(expirationDate); diff --git a/src/main/java/com/skyflow/utils/Constants.java b/src/main/java/com/skyflow/utils/Constants.java index 6732ea60..1162b3f0 100644 --- a/src/main/java/com/skyflow/utils/Constants.java +++ b/src/main/java/com/skyflow/utils/Constants.java @@ -34,6 +34,16 @@ public final class Constants { public static final String PROCESSED_FILE_NAME_PREFIX = "processed-"; public static final String ERROR_FROM_CLIENT_HEADER_KEY = "error-from-client"; public static final String DEIDENTIFIED_FILE_PREFIX = "deidentified"; + public static final String HTTPS_PROTOCOL = "https"; + public static final String CURLY_PLACEHOLDER = "{%s}"; + public static final String EMPTY_STRING = ""; + public static final String QUOTE = "\""; + + public static final class HttpUtilityExtra { + public static final String RAW_BODY_KEY = "__raw_body__"; + public static final String SDK_GENERATED_PREFIX = "SDK-Generated-"; + private HttpUtilityExtra() {} + } static { String sdkVersion; diff --git a/src/main/java/com/skyflow/utils/HttpUtility.java b/src/main/java/com/skyflow/utils/HttpUtility.java index b8e9283b..e5793129 100644 --- a/src/main/java/com/skyflow/utils/HttpUtility.java +++ b/src/main/java/com/skyflow/utils/HttpUtility.java @@ -7,11 +7,13 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; public final class HttpUtility { @@ -32,8 +34,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< try { connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(method); - connection.setRequestProperty("content-type", "application/json"); connection.setRequestProperty("Accept", "*/*"); + boolean hasContentType = headers != null && headers.containsKey("content-type"); + if (!hasContentType && params != null && !params.isEmpty()) { + connection.setRequestProperty("content-type", "application/json"); + } if (headers != null && !headers.isEmpty()) { for (Map.Entry entry : headers.entrySet()) @@ -52,9 +57,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< byte[] input = null; String requestContentType = connection.getRequestProperty("content-type"); - if (requestContentType.contains("application/x-www-form-urlencoded")) { + if (params.has(Constants.HttpUtilityExtra.RAW_BODY_KEY) && params.size() == 1) { + input = params.get(Constants.HttpUtilityExtra.RAW_BODY_KEY).getAsString().getBytes(StandardCharsets.UTF_8); + } else if (requestContentType != null && requestContentType.contains("application/x-www-form-urlencoded")) { input = formatJsonToFormEncodedString(params).getBytes(StandardCharsets.UTF_8); - } else if (requestContentType.contains("multipart/form-data")) { + } else if (requestContentType != null && requestContentType.contains("multipart/form-data")) { input = formatJsonToMultiPartFormDataString(params, boundary).getBytes(StandardCharsets.UTF_8); } else { input = params.toString().getBytes(StandardCharsets.UTF_8); @@ -67,7 +74,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< int httpCode = connection.getResponseCode(); String requestID = connection.getHeaderField("x-request-id"); - HttpUtility.requestID = requestID.split(",")[0]; + if (requestID != null) { + HttpUtility.requestID = requestID.split(",")[0]; + } else { + HttpUtility.requestID = Constants.HttpUtilityExtra.SDK_GENERATED_PREFIX + UUID.randomUUID(); + } Map> responseHeaders = connection.getHeaderFields(); Reader streamReader; if (httpCode > 299) { @@ -159,7 +170,13 @@ public static String appendRequestId(String message, String requestId) { } private static String makeFormEncodeKeyValuePair(String key, String value) { - return key + "=" + value + "&"; + try { + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()); + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + return encodedKey + "=" + encodedValue + "&"; + } catch (Exception e) { + return key + "=" + value + "&"; + } } } diff --git a/src/main/java/com/skyflow/utils/Utils.java b/src/main/java/com/skyflow/utils/Utils.java index b33b08c1..0c20bd6c 100644 --- a/src/main/java/com/skyflow/utils/Utils.java +++ b/src/main/java/com/skyflow/utils/Utils.java @@ -17,6 +17,8 @@ import java.io.File; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; @@ -119,7 +121,12 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne for (Map.Entry entry : invokeConnectionRequest.getPathParams().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - filledURL = new StringBuilder(filledURL.toString().replace(String.format("{%s}", key), value)); + try { + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), encodedValue)); + } catch (Exception e) { + filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value)); + } } } @@ -128,7 +135,13 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne for (Map.Entry entry : invokeConnectionRequest.getQueryParams().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - filledURL.append(key).append("=").append(value).append("&"); + try { + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name()); + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + filledURL.append(encodedKey).append("=").append(encodedValue).append("&"); + } catch (Exception e) { + filledURL.append(key).append("=").append(value).append("&"); + } } filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1)); } diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java index e1f18795..0a4f847c 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/src/main/java/com/skyflow/utils/validations/Validations.java @@ -10,6 +10,7 @@ import java.util.regex.Pattern; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; @@ -146,12 +147,27 @@ public static void validateInvokeConnectionRequest(InvokeConnectionRequest invok } if (requestBody != null) { - Gson gson = new Gson(); - JsonObject bodyObject = gson.toJsonTree(requestBody).getAsJsonObject(); - if (bodyObject.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + if (requestBody.getClass().equals(Object.class)) { + return; + } + if (requestBody instanceof String) { + String bodyStr = (String) requestBody; + if (bodyStr.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + } + } else { + Gson gson = new Gson(); + JsonElement bodyElement = gson.toJsonTree(requestBody); + if (bodyElement.isJsonObject()) { + JsonObject bodyObject = bodyElement.getAsJsonObject(); + if (bodyObject.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + } + } } } } @@ -296,11 +312,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky ErrorLogs.VALUES_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.ValuesKeyError.getMessage()); - } else if (values.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValues.getMessage()); } else if (upsert != null) { if (upsert.trim().isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( @@ -324,15 +335,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage()); - } else { - Object value = valuesMap.get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), - InterfaceName.INSERT.getName(), key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); - } } } } @@ -574,15 +576,6 @@ public static void validateUpdateRequest(UpdateRequest updateRequest) throws Sky ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.UPDATE.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage()); - } else { - Object value = data.get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), InterfaceName.UPDATE.getName(), key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - ErrorMessage.EmptyValueInValues.getMessage()); - } } } @@ -875,15 +868,6 @@ private static void validateTokensMapWithTokenStrict( ErrorLogs.MISMATCH_OF_FIELDS_AND_TOKENS.getLog(), interfaceName )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MismatchOfFieldsAndTokens.getMessage()); - } else { - Object value = tokensMap.get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_TOKENS.getLog(), - interfaceName, key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokens.getMessage()); - } } } } diff --git a/src/main/java/com/skyflow/vault/controller/ConnectionController.java b/src/main/java/com/skyflow/vault/controller/ConnectionController.java index 4a9334d4..07f12dc8 100644 --- a/src/main/java/com/skyflow/vault/controller/ConnectionController.java +++ b/src/main/java/com/skyflow/vault/controller/ConnectionController.java @@ -55,16 +55,37 @@ public InvokeConnectionResponse invoke(InvokeConnectionRequest invokeConnectionR Object requestBodyObject = invokeConnectionRequest.getRequestBody(); if (requestBodyObject != null) { - try { - requestBody = convertObjectToJson(requestBodyObject); - } catch (Exception e) { - LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + if (requestBodyObject instanceof String) { + String contentType = headers.getOrDefault("content-type", ""); + if (!contentType.isEmpty() && !contentType.toLowerCase().contains("application/json")) { + requestBody = new JsonObject(); + requestBody.addProperty(Constants.HttpUtilityExtra.RAW_BODY_KEY, (String) requestBodyObject); + } else { + try { + requestBody = convertObjectToJson(requestBodyObject); + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + } + } + } else { + try { + requestBody = convertObjectToJson(requestBodyObject); + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + } } } String response = HttpUtility.sendRequest(requestMethod.name(), new URL(filledURL), requestBody, headers); - JsonObject data = JsonParser.parseString(response).getAsJsonObject(); + JsonObject data; + try { + data = JsonParser.parseString(response).getAsJsonObject(); + } catch (Exception e) { + data = new JsonObject(); + data.addProperty("response", response); + } HashMap metadata = new HashMap<>(); metadata.put("requestId", HttpUtility.getRequestID()); connectionResponse = new InvokeConnectionResponse(data, metadata, null); diff --git a/src/main/java/com/skyflow/vault/controller/DetectController.java b/src/main/java/com/skyflow/vault/controller/DetectController.java index d2dd891d..873c83e1 100644 --- a/src/main/java/com/skyflow/vault/controller/DetectController.java +++ b/src/main/java/com/skyflow/vault/controller/DetectController.java @@ -135,7 +135,6 @@ public DeidentifyFileResponse deidentifyFile(DeidentifyFileRequest request) thro if (DeidentifyFileStatus.SUCCESS.value().equalsIgnoreCase(response.getStatus())) { String base64File = response.getFileBase64(); - response.getEntities().get(0).getFile(); if (base64File != null) { byte[] decodedBytes = Base64.getDecoder().decode(base64File); String outputDir = request.getOutputDirectory(); @@ -297,7 +296,9 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D .map(Object::toString) .orElse(DetectRunsResponseStatus.UNKNOWN.toString()); - String fileExtension = firstOutput.getProcessedFileExtension().get().toString(); + String fileExtension = processedFileExtension + .map(Object::toString) + .orElse(DetectRunsResponseStatus.UNKNOWN.toString()); Float sizeInKb = response.getSize().orElse(null); Float durationInSeconds = response.getDuration().orElse(null); DeidentifyFileResponse deidentifyFileResponse = new DeidentifyFileResponse( @@ -320,7 +321,7 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D } private static synchronized DeidentifiedFileOutput getFirstOutput(DetectRunsResponse response) { - List outputs = response.getOutput().get(); + List outputs = response.getOutput().orElse(null); return outputs != null && !outputs.isEmpty() ? outputs.get(0) : null; } diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java index acac9608..35f3a798 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -129,6 +129,12 @@ private static synchronized HashMap getFormattedGetRecord(V1Fiel } else if (tokensOpt.isPresent()) { getRecord.putAll(tokensOpt.get()); } + + if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); + } + return getRecord; } @@ -148,6 +154,12 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi if (fieldsOpt.isPresent()) { queryRecord.putAll(fieldsOpt.get()); } + + if (queryRecord.containsKey("skyflow_id")) { + queryRecord.put("skyflowId", queryRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); + } + return queryRecord; } @@ -279,7 +291,7 @@ public GetResponse get(GetRequest getRequest) throws SkyflowException { .tokenization(getRequest.getReturnTokens()) .offset(getRequest.getOffset()) .limit(getRequest.getLimit()) - .downloadUrl(getRequest.getDownloadURL()) + .downloadUrl(getRequest.getDownloadUrl()) .columnName(getRequest.getColumnName()) .columnValues(getRequest.getColumnValues()) .fields(getRequest.getFields()) diff --git a/src/main/java/com/skyflow/vault/data/GetRequest.java b/src/main/java/com/skyflow/vault/data/GetRequest.java index 04626e35..0fccf7b8 100644 --- a/src/main/java/com/skyflow/vault/data/GetRequest.java +++ b/src/main/java/com/skyflow/vault/data/GetRequest.java @@ -1,7 +1,9 @@ package com.skyflow.vault.data; import com.skyflow.enums.RedactionType; +import com.skyflow.logs.InfoLogs; import com.skyflow.utils.Constants; +import com.skyflow.utils.logger.LogUtil; import java.util.ArrayList; @@ -44,8 +46,17 @@ public String getLimit() { return this.builder.limit; } + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { - return this.builder.downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; } public String getColumnName() { @@ -68,14 +79,14 @@ public static final class GetRequestBuilder { private ArrayList fields; private String offset; private String limit; - private Boolean downloadURL; + private Boolean downloadUrl; private String columnName; private ArrayList columnValues; private String orderBy; private GetRequestBuilder() { - this.downloadURL = true; this.orderBy = Constants.ORDER_ASCENDING; + this.downloadUrl = true; } public GetRequestBuilder table(String table) { @@ -113,8 +124,17 @@ public GetRequestBuilder limit(String limit) { return this; } + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public GetRequestBuilder downloadURL(Boolean downloadURL) { - this.downloadURL = downloadURL == null || downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return downloadUrl(downloadURL); + } + + public GetRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl == null || downloadUrl; return this; } diff --git a/src/main/java/com/skyflow/vault/data/GetResponse.java b/src/main/java/com/skyflow/vault/data/GetResponse.java index 34a01303..365fe38e 100644 --- a/src/main/java/com/skyflow/vault/data/GetResponse.java +++ b/src/main/java/com/skyflow/vault/data/GetResponse.java @@ -14,6 +14,14 @@ public GetResponse(ArrayList> data, ArrayListDeprecation notice: The {@code skyflow_id} key in each record map is + * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility.

+ */ public ArrayList> getData() { return data; } diff --git a/src/main/java/com/skyflow/vault/data/QueryResponse.java b/src/main/java/com/skyflow/vault/data/QueryResponse.java index 7a1bca51..afb32c60 100644 --- a/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ b/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -1,32 +1,52 @@ package com.skyflow.vault.data; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import java.util.ArrayList; import java.util.HashMap; public class QueryResponse { private final ArrayList> fields; - private ArrayList> tokenizedData; + private final ArrayList> errors; public QueryResponse(ArrayList> fields) { this.fields = fields; + this.errors = null; } + /** + * Returns the list of record maps from the Query response. Each map contains all + * field name/value pairs for the record. + * + *

Deprecation notice: The {@code skyflow_id} key in each record map is + * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility.

+ */ public ArrayList> getFields() { return fields; } + /** + * Always returns null. The Query API does not support partial-error responses. + */ + public ArrayList> getErrors() { + return errors; + } + @Override public String toString() { Gson gson = new GsonBuilder().serializeNulls().create(); JsonObject responseObject = gson.toJsonTree(this).getAsJsonObject(); JsonArray fieldsArray = responseObject.get("fields").getAsJsonArray(); + // tokenizedData is intentionally injected per-record — Query API cannot return tokens; + // this ensures the field is always present in serialised output for cross-SDK consistency for (JsonElement fieldElement : fieldsArray) { fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); } - responseObject.add("errors", null); - responseObject.remove("tokenizedData"); return responseObject.toString(); } } diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 186a18d2..481c0c16 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -1,5 +1,8 @@ package com.skyflow.vault.tokens; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.logger.LogUtil; + import java.util.ArrayList; public class DetokenizeRequest { @@ -21,18 +24,27 @@ public Boolean getContinueOnError() { return this.builder.continueOnError; } + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { - return this.builder.downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; } public static final class DetokenizeRequestBuilder { private ArrayList detokenizeData; private Boolean continueOnError; - private Boolean downloadURL; + private Boolean downloadUrl; private DetokenizeRequestBuilder() { this.continueOnError = false; - this.downloadURL = false; + this.downloadUrl = false; } public DetokenizeRequestBuilder detokenizeData(ArrayList detokenizeData) { @@ -45,8 +57,17 @@ public DetokenizeRequestBuilder continueOnError(Boolean continueOnError) { return this; } + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { - this.downloadURL = downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return downloadUrl(downloadURL); + } + + public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl; return this; } diff --git a/src/test/java/com/skyflow/VaultClientTests.java b/src/test/java/com/skyflow/VaultClientTests.java index 4c9be65f..f3e9f738 100644 --- a/src/test/java/com/skyflow/VaultClientTests.java +++ b/src/test/java/com/skyflow/VaultClientTests.java @@ -168,6 +168,26 @@ public void testGetDetokenizePayload() { } } + @Test + public void testGetDetokenizePayloadWithNewDownloadUrl() { + try { + DetokenizeData detokenizeDataRecord1 = new DetokenizeData(token); + detokenizeData.clear(); + detokenizeData.add(detokenizeDataRecord1); + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .downloadUrl(true) // new form + .continueOnError(false) + .build(); + V1DetokenizePayload payload = vaultClient.getDetokenizePayload(detokenizeRequest); + Assert.assertTrue("new downloadUrl() should be reflected in payload", payload.getDownloadUrl().get()); + Assert.assertTrue("new getDownloadUrl() should return true", detokenizeRequest.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", detokenizeRequest.getDownloadURL()); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testGetBulkInsertRequestBody() { try { diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java index ecd38e84..4eb90458 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java @@ -3,6 +3,7 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import org.junit.Assert; import org.junit.BeforeClass; @@ -249,4 +250,22 @@ public void testInvalidTokenURIInCredentialsForCredentials() throws SkyflowExcep Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); } } + + @Test + public void testBearerTokenWithNewFormCredentialKeys() { + try { + // Fake key — fails at RSA parsing, not at field lookup, confirming new-form keys were accepted + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}"; + BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build(); + bearerToken.getBearerToken(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + // InvalidKeySpec confirms all credential fields were resolved — failure is at RSA parsing, not field lookup + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidKeySpec.getMessage(), Constants.SDK_PREFIX), + e.getMessage()); + } + } } diff --git a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java index 5e2cbe60..93d69b0e 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java @@ -221,6 +221,26 @@ public void testInvalidKeySpecInCredentials() { } } + @Test + public void testSignedDataTokensWithNewFormCredentialKeys() { + try { + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\"}"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("test-token"); + SignedDataTokens signedDataTokens = SignedDataTokens.builder() + .setCredentials(credentialsString) + .setDataTokens(dataTokens) + .build(); + signedDataTokens.getSignedDataTokens(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + // Failure at RSA key parsing (not field lookup) confirms new-form keys clientId/keyId were found + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.InvalidKeySpec.getMessage(), e.getMessage()); + } + } + @Test public void testSignedDataTokenResponse() { try { diff --git a/src/test/java/com/skyflow/utils/HttpUtilityTests.java b/src/test/java/com/skyflow/utils/HttpUtilityTests.java index f7214690..2861ed9c 100644 --- a/src/test/java/com/skyflow/utils/HttpUtilityTests.java +++ b/src/test/java/com/skyflow/utils/HttpUtilityTests.java @@ -124,4 +124,69 @@ public void testSendRequestError() { fail(INVALID_EXCEPTION_THROWN); } } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithRawBody() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/xml"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/xml"); + JsonObject params = new JsonObject(); + params.addProperty("__raw_body__", "test"); + String response = httpUtility.sendRequest("POST", url, params, headers); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithoutContentTypeHeader() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/json"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value"); + String response = httpUtility.sendRequest("POST", url, params, null); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithNullRequestId() { + try { + given(mockConnection.getHeaderField(anyString())).willReturn(null); + given(mockConnection.getRequestProperty("content-type")).willReturn("application/json"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value"); + String response = httpUtility.sendRequest("GET", url, params, headers); + Assert.assertEquals(expected, response); + Assert.assertNotNull(HttpUtility.getRequestID()); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestFormURLEncodedWithSpecialCharacters() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/x-www-form-urlencoded"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/x-www-form-urlencoded"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value with spaces"); + params.addProperty("special", "test@email.com"); + String response = httpUtility.sendRequest("POST", url, params, headers); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } } diff --git a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java index da8494e1..2b8379bc 100644 --- a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java +++ b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java @@ -5,8 +5,12 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.types.DeidentifiedFileOutput; +import com.skyflow.generated.rest.types.DetectRunsResponse; +import com.skyflow.generated.rest.types.DetectRunsResponseStatus; import com.skyflow.vault.detect.AudioBleep; import com.skyflow.vault.detect.DeidentifyFileRequest; +import com.skyflow.vault.detect.DeidentifyFileResponse; import com.skyflow.vault.detect.FileInput; import com.skyflow.vault.detect.GetDetectRunRequest; import org.junit.Assert; @@ -14,8 +18,10 @@ import org.junit.Test; import java.io.File; +import java.lang.reflect.Method; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; public class DetectControllerFileTests { private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -406,4 +412,54 @@ public void testOutputDirectoryNotWritable() throws Exception { dir.delete(); } } + + // C1 regression: the dead statement response.getEntities().get(0) has been removed. + // The short-form constructor produces null entities; the guarded block at the call site + // handles that safely. + @Test + public void testDeidentifyFileResponseNullEntitiesDoesNotThrow() { + DeidentifyFileResponse response = new DeidentifyFileResponse("run-id", "IN_PROGRESS"); + Assert.assertNull("Entities must be null from short-form constructor", response.getEntities()); + } + + // C2: getFirstOutput must return null (not throw NoSuchElementException) when output is absent. + @Test + public void testGetFirstOutputReturnsNullWhenOutputAbsent() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder().build(); + Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class); + method.setAccessible(true); + DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response); + Assert.assertNull("getFirstOutput must return null when output Optional is absent", result); + } + + // C2: getFirstOutput must return null when output list is present but empty. + @Test + public void testGetFirstOutputReturnsNullWhenOutputListEmpty() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder() + .output(Collections.emptyList()) + .build(); + Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class); + method.setAccessible(true); + DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response); + Assert.assertNull("getFirstOutput must return null when output list is empty", result); + } + + // C3: parseDeidentifyFileResponse must not throw when processedFileExtension is absent; + // it should fall back to the UNKNOWN sentinel value. + @Test + public void testParseDeidentifyFileResponseFallsBackWhenExtensionAbsent() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .output(Collections.singletonList(DeidentifiedFileOutput.builder().build())) + .build(); + + Method method = DetectController.class.getDeclaredMethod( + "parseDeidentifyFileResponse", DetectRunsResponse.class, String.class, String.class); + method.setAccessible(true); + DeidentifyFileResponse result = (DeidentifyFileResponse) method.invoke(null, response, "run-id", "SUCCESS"); + + Assert.assertNotNull("Response must not be null", result); + Assert.assertEquals("Extension must fall back to UNKNOWN when absent", + DetectRunsResponseStatus.UNKNOWN.toString(), result.getExtension()); + } } \ No newline at end of file diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 4115bc2c..5dc02f37 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -10,6 +10,7 @@ import com.skyflow.errors.HttpStatus; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.types.V1FieldRecords; import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import com.skyflow.vault.data.*; @@ -19,6 +20,10 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + public class VaultControllerTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -185,4 +190,105 @@ public void testInvalidRequestInFileUploadMethod() { } } + @Test + public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "abc-123"); + fields.put("name", "John"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertEquals("skyflowId should be present (new form)", "abc-123", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "abc-123", result.get("skyflow_id")); + Assert.assertEquals("other fields should be preserved", "John", result.get("name")); + } + + @Test + public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "xyz-456"); + fields.put("email", "test@example.com"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertEquals("skyflowId should be present (new form)", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "xyz-456", result.get("skyflow_id")); + Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); + } + + @Test + public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception { + Map tokens = new HashMap<>(); + tokens.put("skyflow_id", "tok-789"); + tokens.put("card_number", "tok-card-abc"); + V1FieldRecords record = V1FieldRecords.builder().tokens(tokens).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertEquals("skyflowId should be present (new form)", "tok-789", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "tok-789", result.get("skyflow_id")); + Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); + } + + @Test + public void testGetRequestDownloadUrlNewForm() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadUrl(true) + .build(); + Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl()); + } + + @Test + public void testGetRequestDownloadURLDeprecatedFormStillWorks() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadURL(true) + .build(); + Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL()); + Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl()); + } + + @Test + public void testGetRequestDownloadUrlDefaultIsTrue() { + GetRequest request = GetRequest.builder() + .table("test_table") + .build(); + Assert.assertTrue("downloadUrl should be true by default (preserved from original)", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadUrlNewForm() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadUrl(true) + .build(); + Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadURLDeprecatedFormStillWorks() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadURL(true) + .build(); + Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL()); + Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadUrlDefaultIsFalse() { + DetokenizeRequest request = DetokenizeRequest.builder().build(); + Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl()); + } + } diff --git a/src/test/java/com/skyflow/vault/data/GetTests.java b/src/test/java/com/skyflow/vault/data/GetTests.java index 74306d4b..e62c605f 100644 --- a/src/test/java/com/skyflow/vault/data/GetTests.java +++ b/src/test/java/com/skyflow/vault/data/GetTests.java @@ -97,6 +97,24 @@ public void testValidGetByIdInputInGetRequestValidations() { } } + @Test + public void testValidGetByIdInputWithNewDownloadUrl() { + try { + ids.add(skyflowID); + fields.add(field); + GetRequest request = GetRequest.builder() + .ids(ids) + .table(table) + .downloadUrl(false) // new form + .build(); + Validations.validateGetRequest(request); + Assert.assertFalse("new getDownloadUrl() should return false", request.getDownloadUrl()); + Assert.assertFalse("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testValidGetByColumnValuesInputInGetRequestValidations() { try { @@ -129,6 +147,24 @@ public void testValidGetByColumnValuesInputInGetRequestValidations() { } } + @Test + public void testValidGetByColumnValuesInputWithNewDownloadUrl() { + try { + columnValues.add(columnValue); + GetRequest request = GetRequest.builder() + .table(table) + .columnName(columnName) + .columnValues(columnValues) + .downloadUrl(true) // new form + .build(); + Validations.validateGetRequest(request); + Assert.assertTrue("new getDownloadUrl() should return true", request.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testNoTableInGetRequestValidations() { ids.add(skyflowID); diff --git a/src/test/java/com/skyflow/vault/data/InsertTests.java b/src/test/java/com/skyflow/vault/data/InsertTests.java index 00399f00..a9178f4a 100644 --- a/src/test/java/com/skyflow/vault/data/InsertTests.java +++ b/src/test/java/com/skyflow/vault/data/InsertTests.java @@ -172,40 +172,45 @@ public void testNoValuesInInsertRequestValidations() { } @Test - public void testEmptyValuesInInsertRequestValidations() { + public void testEmptyValuesArrayPassesInsertRequestValidations() { + // SDK no longer blocks empty values array — BE is authoritative InsertRequest request = InsertRequest.builder().table(table).values(values).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail("Empty values array should pass SDK validation: " + e.getMessage()); } } @Test - public void testEmptyKeyInValuesInInsertRequestValidations() { - valueMap.put("", "test_value_3"); + public void testNullFieldValuePassesInsertRequestValidations() { + // SDK no longer blocks null field values — BE is authoritative per API spec + valueMap.put("test_column_1", null); values.add(valueMap); InsertRequest request = InsertRequest.builder().table(table).values(values).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyKeyInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail("Null field value should pass SDK validation: " + e.getMessage()); } } @Test - public void testEmptyValueInValuesInInsertRequestValidations() { - valueMap.put("test_column_3", ""); + public void testEmptyStringFieldValuePassesInsertRequestValidations() { + // SDK no longer blocks empty string field values — BE is authoritative per API spec + valueMap.put("test_column_1", ""); + values.add(valueMap); + InsertRequest request = InsertRequest.builder().table(table).values(values).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail("Empty string field value should pass SDK validation: " + e.getMessage()); + } + } + + @Test + public void testEmptyKeyInValuesInInsertRequestValidations() { + valueMap.put("", "test_value_3"); values.add(valueMap); InsertRequest request = InsertRequest.builder().table(table).values(values).build(); try { @@ -214,7 +219,7 @@ public void testEmptyValueInValuesInInsertRequestValidations() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), + Utils.parameterizedString(ErrorMessage.EmptyKeyInValues.getMessage(), Constants.SDK_PREFIX), e.getMessage() ); } @@ -388,23 +393,6 @@ public void testEmptyKeyInTokensInInsertRequestValidations() { } } - @Test - public void testEmptyValueInTokensInInsertRequestValidations() { - tokenMap.put("test_column_2", ""); - values.add(valueMap); - tokens.add(tokenMap); - InsertRequest request = InsertRequest.builder() - .table(table).values(values).tokens(tokens).tokenMode(TokenMode.ENABLE_STRICT) - .build(); - try { - Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); - } - } - @Test public void testInsertResponse() { try { diff --git a/src/test/java/com/skyflow/vault/data/QueryResponseTest.java b/src/test/java/com/skyflow/vault/data/QueryResponseTest.java new file mode 100644 index 00000000..428b1151 --- /dev/null +++ b/src/test/java/com/skyflow/vault/data/QueryResponseTest.java @@ -0,0 +1,29 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +public class QueryResponseTest { + + @Test + public void testGetErrorsReturnsNull() { + ArrayList> fields = new ArrayList<>(); + HashMap record = new HashMap<>(); + record.put("skyflowId", "abc-123"); + fields.add(record); + + QueryResponse response = new QueryResponse(fields); + + Assert.assertNull("getErrors() should return null when no errors", response.getErrors()); + } + + @Test + public void testGetErrorsIsPresentInToString() { + QueryResponse response = new QueryResponse(new ArrayList<>()); + String json = response.toString(); + Assert.assertTrue("toString() should include errors:null", json.contains("\"errors\":null")); + } +} diff --git a/src/test/java/com/skyflow/vault/data/UpdateTests.java b/src/test/java/com/skyflow/vault/data/UpdateTests.java index be702d4e..55e5b811 100644 --- a/src/test/java/com/skyflow/vault/data/UpdateTests.java +++ b/src/test/java/com/skyflow/vault/data/UpdateTests.java @@ -247,44 +247,6 @@ public void testEmptyKeyInValuesInUpdateRequestValidations() { } } - @Test - public void testNullValueInValuesInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - dataMap.put("test_column_3", null); - UpdateRequest request = UpdateRequest.builder().table(table).data(dataMap).build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - - @Test - public void testEmptyValueInValuesInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - dataMap.put("test_column_3", ""); - UpdateRequest request = UpdateRequest.builder().table(table).data(dataMap).build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - @Test public void testTokensWithTokenModeDisableInUpdateRequestValidations() { dataMap.put("skyflow_id", skyflowID); @@ -419,25 +381,6 @@ public void testNullKeyInTokensInUpdateRequestValidations() { } } - @Test - public void testNullValueInTokensInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - tokenMap.put("test_column_1", "test_token_1"); - tokenMap.put("test_column_2", null); - UpdateRequest request = UpdateRequest.builder() - .table(table).data(dataMap).tokens(tokenMap).tokenMode(TokenMode.ENABLE_STRICT) - .build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); - } - } - @Test public void testUpdateResponse() { try { diff --git a/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java b/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java index d417aeb7..43aad475 100644 --- a/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java +++ b/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java @@ -52,6 +52,22 @@ public void testValidInputInDetokenizeRequestValidations() { } } + @Test + public void testValidInputWithNewDownloadUrlInDetokenizeRequestValidations() { + try { + detokenizeData.add(maskedRedactionRecord); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .downloadUrl(true) // new form + .build(); + Validations.validateDetokenizeRequest(request); + Assert.assertTrue("new getDownloadUrl() should return true", request.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testNoTokensInDetokenizeRequestValidations() { DetokenizeRequest request = DetokenizeRequest.builder().build();