diff --git a/CHANGELOG.md b/CHANGELOG.md index 8534bd5..72ef906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [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 diff --git a/README.md b/README.md index 7405925..1a76fcb 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This Java SDK is designed to help developers easily implement Skyflow into their Add this dependency to your project's build file: ``` -implementation 'com.skyflow:skyflow-java:1.4.1' +implementation 'com.skyflow:skyflow-java:1.5.0' ``` #### Maven users @@ -47,7 +47,7 @@ Add this dependency to your project's POM: com.skyflow skyflow-java - 1.4.1 + 1.5.0 ``` --- diff --git a/pom.xml b/pom.xml index 646a28e..0b57200 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.skyflow skyflow-java - 1.4.1 + 1.5.0 jar ${project.groupId}:${project.artifactId} diff --git a/src/main/java/com/skyflow/common/utils/Helpers.java b/src/main/java/com/skyflow/common/utils/Helpers.java index 982fd10..7cdc1a6 100644 --- a/src/main/java/com/skyflow/common/utils/Helpers.java +++ b/src/main/java/com/skyflow/common/utils/Helpers.java @@ -5,19 +5,19 @@ import com.skyflow.entities.InsertRecordInput; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.DebugLogs; + import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public final class Helpers { + private static final String LINE_FEED = "\r\n"; public static JSONObject constructInsertRequest(InsertInput recordsInput, InsertOptions options) throws SkyflowException { JSONObject finalRequest = new JSONObject(); List requestBodyContent = new ArrayList(); @@ -161,4 +161,56 @@ public static String appendRequestIdToErrorObj(int status, String error, String return error; } + public static String formatJsonToFormEncodedString(JSONObject requestBody){ + LogUtil.printDebugLog(DebugLogs.FormatRequestBodyFormUrlFormEncoded.getLog()); + StringBuilder formEncodeString = new StringBuilder(); + HashMap jsonMap = convertJsonToMap(requestBody,""); + + for (Map.Entry currentEntry : jsonMap.entrySet()) + formEncodeString.append(makeFormEncodeKeyValuePair(currentEntry.getKey(),currentEntry.getValue())); + + return formEncodeString.substring(0,formEncodeString.length()-1); + } + + public static String formatJsonToMultiPartFormDataString(JSONObject requestBody,String boundary){ + LogUtil.printDebugLog(DebugLogs.FormatRequestBodyFormData.getLog()); + StringBuilder formEncodeString = new StringBuilder(); + HashMap jsonMap = convertJsonToMap(requestBody,""); + + for (Map.Entry currentEntry : jsonMap.entrySet()) + formEncodeString.append(makeFormDataKeyValuePair(currentEntry.getKey(),currentEntry.getValue(),boundary)); + + formEncodeString.append(LINE_FEED); + formEncodeString.append("--").append(boundary).append("--").append(LINE_FEED); + + return formEncodeString.toString(); + } + + private static HashMap convertJsonToMap(JSONObject json,String rootKey){ + HashMap currentMap = new HashMap<>(); + for (Object key : json.keySet()) { + Object currentValue = json.get(key); + String currentKey = rootKey.length() != 0 ? rootKey + '[' + key.toString() + ']' : rootKey + key.toString(); + if(currentValue instanceof JSONObject){ + currentMap.putAll(convertJsonToMap((JSONObject) currentValue, currentKey)); + }else { + currentMap.put(currentKey,currentValue.toString()); + } + } + return currentMap; + } + + private static String makeFormEncodeKeyValuePair(String key, String value){ + return key+"="+value+"&"; + } + + private static String makeFormDataKeyValuePair(String key,String value,String boundary){ + StringBuilder formDataTextField = new StringBuilder(); + formDataTextField.append("--").append(boundary).append(LINE_FEED); + formDataTextField.append("Content-Disposition: form-data; name=\"").append(key).append("\"").append(LINE_FEED); + formDataTextField.append(LINE_FEED); + formDataTextField.append(value).append(LINE_FEED); + + return formDataTextField.toString(); + } } diff --git a/src/main/java/com/skyflow/common/utils/HttpUtility.java b/src/main/java/com/skyflow/common/utils/HttpUtility.java index a30a0b0..ba019a4 100644 --- a/src/main/java/com/skyflow/common/utils/HttpUtility.java +++ b/src/main/java/com/skyflow/common/utils/HttpUtility.java @@ -10,28 +10,48 @@ import java.nio.charset.StandardCharsets; import java.util.Map; +import static com.skyflow.common.utils.Helpers.formatJsonToFormEncodedString; +import static com.skyflow.common.utils.Helpers.formatJsonToMultiPartFormDataString; + public final class HttpUtility { public static String sendRequest(String method, String requestUrl, JSONObject params, Map headers) throws IOException, SkyflowException { HttpURLConnection connection = null; BufferedReader in = null; StringBuffer response = null; + String boundary = String.valueOf(System.currentTimeMillis()); try { URL url = new URL(requestUrl); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(method); connection.setRequestProperty("content-type", "application/json"); + connection.setRequestProperty("Accept", "*/*"); if (headers != null && headers.size() > 0) { - for (Map.Entry entry : headers.entrySet()) { + for (Map.Entry entry : headers.entrySet()) connection.setRequestProperty(entry.getKey(), entry.getValue()); + + // append dynamic boundary if content-type is multipart/form-data + if (headers.containsKey("content-type")) { + if (headers.get("content-type") == "multipart/form-data") { + connection.setRequestProperty("content-type", "multipart/form-data; boundary=" + boundary); + } } } - if (params != null && params.size() > 0) { connection.setDoOutput(true); try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { - byte[] input = params.toString().getBytes(StandardCharsets.UTF_8); + byte[] input = null; + String requestContentType = connection.getRequestProperty("content-type"); + + if (requestContentType.contains("application/x-www-form-urlencoded")) { + input = formatJsonToFormEncodedString(params).getBytes(StandardCharsets.UTF_8); + } else if (requestContentType.contains("multipart/form-data")) { + input = formatJsonToMultiPartFormDataString(params, boundary).getBytes(StandardCharsets.UTF_8); + }else { + input = params.toString().getBytes(StandardCharsets.UTF_8); + } + wr.write(input, 0, input.length); wr.flush(); } diff --git a/src/main/java/com/skyflow/logs/DebugLogs.java b/src/main/java/com/skyflow/logs/DebugLogs.java new file mode 100644 index 0000000..9e4d467 --- /dev/null +++ b/src/main/java/com/skyflow/logs/DebugLogs.java @@ -0,0 +1,16 @@ +package com.skyflow.logs; + +public enum DebugLogs { + + FormatRequestBodyFormUrlFormEncoded("Formatting request body for form-urlencoded content-type"), + FormatRequestBodyFormData("Formatting request body for form-data content-type"); + private final String log; + + DebugLogs(String log) { + this.log = log; + } + + public String getLog() { + return log; + } +} \ No newline at end of file diff --git a/src/test/java/com/skyflow/common/utils/HelpersTest.java b/src/test/java/com/skyflow/common/utils/HelpersTest.java new file mode 100644 index 0000000..6bc447d --- /dev/null +++ b/src/test/java/com/skyflow/common/utils/HelpersTest.java @@ -0,0 +1,38 @@ +package com.skyflow.common.utils; + +import org.json.simple.JSONObject; +import org.junit.Test; + +public class HelpersTest { + + @Test + public void testFormatJsonToFormEncodedString(){ + JSONObject testJson = new JSONObject(); + testJson.put("key1","value1"); + JSONObject nestedObj = new JSONObject(); + nestedObj.put("key2","value2"); + testJson.put("nest",nestedObj); + + String testResponse = Helpers.formatJsonToFormEncodedString(testJson); + System.out.println(testResponse); + assert testResponse.contains("key1=value1"); + assert testResponse.contains("nest[key2]=value2"); + } + + @Test + public void testFormatJsonToMultiPartFormDataString(){ + JSONObject testJson = new JSONObject(); + testJson.put("key1","value1"); + JSONObject nestedObj = new JSONObject(); + nestedObj.put("key2","value2"); + testJson.put("nest",nestedObj); + String testBoundary = "123"; + String testResponse = Helpers.formatJsonToMultiPartFormDataString(testJson,testBoundary); + assert testResponse.contains("--"+testBoundary); + assert testResponse.contains("--"+testBoundary+"--"); + assert testResponse.contains("Content-Disposition: form-data; name=\"key1\""); + assert testResponse.contains("value1"); + assert testResponse.contains("Content-Disposition: form-data; name=\"nest[key2]\""); + assert testResponse.contains("value2"); + } +} diff --git a/src/test/java/com/skyflow/serviceaccount/util/TokenTest.java b/src/test/java/com/skyflow/serviceaccount/util/TokenTest.java index 56aa7c0..c12fd19 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/TokenTest.java +++ b/src/test/java/com/skyflow/serviceaccount/util/TokenTest.java @@ -23,7 +23,7 @@ public class TokenTest { @Test public void testInvalidFilePath() { try { - Token.generateBearerToken(""); + Token.GenerateToken(""); } catch (SkyflowException exception) { assertEquals(exception.getMessage(), ErrorCode.EmptyFilePath.getDescription()); } diff --git a/src/test/java/com/skyflow/vault/InvokeConnectionTest.java b/src/test/java/com/skyflow/vault/InvokeConnectionTest.java index e8594c3..e70c795 100644 --- a/src/test/java/com/skyflow/vault/InvokeConnectionTest.java +++ b/src/test/java/com/skyflow/vault/InvokeConnectionTest.java @@ -20,6 +20,7 @@ import java.io.IOException; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; @@ -34,6 +35,7 @@ public String getBearerToken() throws Exception { @RunWith(PowerMockRunner.class) @PrepareForTest(fullyQualifiedNames = "com.skyflow.common.utils.TokenUtils") public class InvokeConnectionTest { + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private JSONObject testConfig; private static Skyflow skyflowClient; @@ -84,8 +86,10 @@ public void testInvokeConnectionValidInput() { Assert.assertEquals(gatewayResponse.toJSONString(), mockResponse); } catch (SkyflowException exception) { Assert.assertNull(exception); + fail(INVALID_EXCEPTION_THROWN); } catch (IOException exception) { exception.printStackTrace(); + fail(INVALID_EXCEPTION_THROWN); } @@ -158,4 +162,78 @@ public void testInvokeConnectionInvalidMethodName() { } } + @Test + @PrepareForTest(fullyQualifiedNames = {"com.skyflow.common.utils.HttpUtility", "com.skyflow.common.utils.TokenUtils"}) + public void testInvokeConnectionWithFormEncoded() { + JSONObject testConfig = new JSONObject(); + testConfig.put("connectionURL", "https://testgatewayurl.com/"); + testConfig.put("methodName", RequestMethod.POST); + + JSONObject requestHeadersJson = new JSONObject(); + requestHeadersJson.put("content-type", "application/x-www-form-urlencoded"); + testConfig.put("requestHeader", requestHeadersJson); + + JSONObject testJson = new JSONObject(); + testJson.put("key1","value1"); + JSONObject nestedObj = new JSONObject(); + nestedObj.put("key2","value2"); + testJson.put("nest",nestedObj); + + testConfig.put("requestBody", testJson); + + try { + PowerMockito.mockStatic(HttpUtility.class); + String mockResponse = "{\"id\":\"12345\"}"; + PowerMockito.when(HttpUtility.sendRequest(anyString(), anyString(), ArgumentMatchers.any(), ArgumentMatchers.anyMap())).thenReturn(mockResponse); + JSONObject gatewayResponse = skyflowClient.invokeConnection(testConfig); + + Assert.assertNotNull(gatewayResponse); + Assert.assertEquals(gatewayResponse.toJSONString(), mockResponse); + } catch (SkyflowException exception) { + Assert.assertNull(exception); + fail(INVALID_EXCEPTION_THROWN); + } catch (IOException exception) { + exception.printStackTrace(); + fail(INVALID_EXCEPTION_THROWN); + } + + } + + @Test + @PrepareForTest(fullyQualifiedNames = {"com.skyflow.common.utils.HttpUtility", "com.skyflow.common.utils.TokenUtils"}) + public void testInvokeConnectionWithMultipartFormData() { + JSONObject testConfig = new JSONObject(); + testConfig.put("connectionURL", "https://testgatewayurl.com/"); + testConfig.put("methodName", RequestMethod.POST); + + JSONObject requestHeadersJson = new JSONObject(); + requestHeadersJson.put("content-type", "multipart/form-data"); + testConfig.put("requestHeader", requestHeadersJson); + + JSONObject testJson = new JSONObject(); + testJson.put("key1","value1"); + JSONObject nestedObj = new JSONObject(); + nestedObj.put("key2","value2"); + testJson.put("nest",nestedObj); + + testConfig.put("requestBody", testJson); + + try { + PowerMockito.mockStatic(HttpUtility.class); + String mockResponse = "{\"id\":\"12345\"}"; + PowerMockito.when(HttpUtility.sendRequest(anyString(), anyString(), ArgumentMatchers.any(), ArgumentMatchers.anyMap())).thenReturn(mockResponse); + JSONObject gatewayResponse = skyflowClient.invokeConnection(testConfig); + + Assert.assertNotNull(gatewayResponse); + Assert.assertEquals(gatewayResponse.toJSONString(), mockResponse); + } catch (SkyflowException exception) { + Assert.assertNull(exception); + fail(INVALID_EXCEPTION_THROWN); + } catch (IOException exception) { + exception.printStackTrace(); + fail(INVALID_EXCEPTION_THROWN); + } + + } + }