From 31f117c6b112be855306508bda59d3e77b416945 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 1 Jul 2026 16:52:45 +0530 Subject: [PATCH 1/2] fix: update version to 2.7.2 and add Snyk fixes to CHANGELOG --- CHANGELOG.md | 5 ++ pom.xml | 11 ++-- .../contentstack/sdk/CSHttpConnection.java | 51 +++++++++++++++---- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd5d729..0f4790fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v2.7.2 + +### Jul 06, 2026 +- Snyk fixes + ## v2.7.1 ### Jun 29, 2026 diff --git a/pom.xml b/pom.xml index dc3bce26..3345de94 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.contentstack.sdk java - 2.7.1 + 2.7.2 jar contentstack-java Java SDK for Contentstack Content Delivery API @@ -24,7 +24,7 @@ 3.0.0 5.3.2 0.8.5 - 1.18.42 + 1.18.44 5.11.4 5.8.0-M1 2.8.8 @@ -33,7 +33,7 @@ 1.5 3.8.1 1.6.13 - 20251224 + 20260522 0.8.11 2.5.3 1.5.1 @@ -178,11 +178,6 @@ - - com.fasterxml.jackson.core - jackson-databind - 2.21.4 - com.slack.api bolt diff --git a/src/main/java/com/contentstack/sdk/CSHttpConnection.java b/src/main/java/com/contentstack/sdk/CSHttpConnection.java index 5d5e3549..bada7e46 100644 --- a/src/main/java/com/contentstack/sdk/CSHttpConnection.java +++ b/src/main/java/com/contentstack/sdk/CSHttpConnection.java @@ -1,13 +1,12 @@ package com.contentstack.sdk; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.type.MapType; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.SocketTimeoutException; import java.net.URLEncoder; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -201,6 +200,41 @@ private JSONObject createOrderedJSONObject(Map map) { return json; } + /** + * Recursively converts a parsed {@link JSONObject} into plain Java collections that + * mirror what the response models expect: JSON objects become {@link LinkedHashMap} + * (preserving key order) and JSON arrays become {@link ArrayList}. + */ + private static Map jsonToOrderedMap(JSONObject object) { + LinkedHashMap map = new LinkedHashMap<>(); + for (String key : object.keySet()) { + map.put(key, convertJsonValue(object.get(key))); + } + return map; + } + + private static Object convertJsonValue(Object value) { + if (value == null || value == JSONObject.NULL) { + return null; + } + if (value instanceof JSONObject) { + return jsonToOrderedMap((JSONObject) value); + } + if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + ArrayList list = new ArrayList<>(array.length()); + for (int i = 0; i < array.length(); i++) { + list.add(convertJsonValue(array.get(i))); + } + return list; + } + // Normalize floating-point numbers to Double to match the previous parser's output. + if (value instanceof BigDecimal) { + return ((BigDecimal) value).doubleValue(); + } + return value; + } + private void getService(String requestUrl) throws IOException { this.headers.put(X_USER_AGENT_KEY, "contentstack-delivery-java/" + SDK_VERSION); @@ -226,12 +260,11 @@ private void getService(String requestUrl) throws IOException { response = pluginResponseImp(request, response); } try { - // Use Jackson to parse the JSON while preserving order - ObjectMapper mapper = JsonMapper.builder().build(); - MapType type = mapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, - Object.class); - Map responseMap = mapper.readValue(response.body().string(), type); - + // Parse the JSON into ordered maps/lists using org.json. Nested objects + // become LinkedHashMap and arrays become ArrayList, matching the shape + // the response models expect. + Map responseMap = jsonToOrderedMap(new JSONObject(response.body().string())); + // Use the custom method to create an ordered JSONObject responseJSON = createOrderedJSONObject(responseMap); if (this.config.livePreviewEntry != null && !this.config.livePreviewEntry.isEmpty()) { From e9c2a6255de399255fd8a4c189c880d695da7e46 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 3 Jul 2026 14:48:52 +0530 Subject: [PATCH 2/2] test: add unit tests for CSConnectionRequest and CSHttpConnection JSON handling --- .../sdk/TestCSConnectionRequest.java | 124 ++++++++++++++ .../sdk/TestCSHttpConnection.java | 152 +++++++++++++++++- 2 files changed, 275 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/contentstack/sdk/TestCSConnectionRequest.java b/src/test/java/com/contentstack/sdk/TestCSConnectionRequest.java index 5aa3402d..f0edc19c 100644 --- a/src/test/java/com/contentstack/sdk/TestCSConnectionRequest.java +++ b/src/test/java/com/contentstack/sdk/TestCSConnectionRequest.java @@ -597,6 +597,130 @@ void testOnRequestFailedWithNullCallback() throws Exception { assertDoesNotThrow(() -> request.onRequestFailed(errorResponse, 500, null)); } + @Test + void testOnRequestFailedWithEmptyError() throws Exception { + // Empty error object exercises the "false" side of the has(error_message/ + // error_code/errors) checks in onRequestFailed. + JSONObject errorResponse = new JSONObject(); + + AtomicBoolean callbackCalled = new AtomicBoolean(false); + ResultCallBack callback = new ResultCallBack() { + @Override + public void onRequestFail(ResponseType responseType, Error error) { + callbackCalled.set(true); + } + }; + + CSConnectionRequest request = new CSConnectionRequest(stack); + Field callbackField = CSConnectionRequest.class.getDeclaredField("resultCallBack"); + callbackField.setAccessible(true); + callbackField.set(request, callback); + + assertDoesNotThrow(() -> request.onRequestFailed(errorResponse, 0, callback)); + assertTrue(callbackCalled.get()); + } + + @Test + void testOnRequestFinishedFetchEntryWithNullCallback() throws Exception { + CSHttpConnection mockConnection = createMockConnection(); + + Field controllerField = CSHttpConnection.class.getDeclaredField("controller"); + controllerField.setAccessible(true); + controllerField.set(mockConnection, Constants.FETCHENTRY); + + LinkedHashMap entryMap = new LinkedHashMap<>(); + entryMap.put("uid", "test_entry_uid"); + entryMap.put("title", "Test Entry"); + + JSONObject response = new JSONObject(); + Field mapField = JSONObject.class.getDeclaredField("map"); + mapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map internalMap = (Map) mapField.get(response); + internalMap.put("entry", entryMap); + + Field responseField = CSHttpConnection.class.getDeclaredField("responseJSON"); + responseField.setAccessible(true); + responseField.set(mockConnection, response); + + // No callback set -> exercises the "false" side of the callback null-check. + CSConnectionRequest request = new CSConnectionRequest(entry); + assertDoesNotThrow(() -> request.onRequestFinished(mockConnection)); + assertEquals("test_entry_uid", entry.uid); + } + + @Test + void testOnRequestFinishedFetchSyncWithNullCallback() throws Exception { + CSHttpConnection mockConnection = createMockConnection(); + + Field controllerField = CSHttpConnection.class.getDeclaredField("controller"); + controllerField.setAccessible(true); + controllerField.set(mockConnection, Constants.FETCHSYNC); + + JSONObject response = new JSONObject(); + response.put("sync_token", "test_sync_token"); + response.put("items", new JSONArray()); + + Field responseField = CSHttpConnection.class.getDeclaredField("responseJSON"); + responseField.setAccessible(true); + responseField.set(mockConnection, response); + + CSConnectionRequest request = new CSConnectionRequest(stack); + assertDoesNotThrow(() -> request.onRequestFinished(mockConnection)); + } + + @Test + void testOnRequestFinishedFetchContentTypesWithNullCallback() throws Exception { + CSHttpConnection mockConnection = createMockConnection(); + + Field controllerField = CSHttpConnection.class.getDeclaredField("controller"); + controllerField.setAccessible(true); + controllerField.set(mockConnection, Constants.FETCHCONTENTTYPES); + + LinkedHashMap contentTypeMap = new LinkedHashMap<>(); + contentTypeMap.put("uid", "blog_post"); + + JSONObject response = new JSONObject(); + Field mapField = JSONObject.class.getDeclaredField("map"); + mapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map internalMap = (Map) mapField.get(response); + internalMap.put("content_type", contentTypeMap); + + Field responseField = CSHttpConnection.class.getDeclaredField("responseJSON"); + responseField.setAccessible(true); + responseField.set(mockConnection, response); + + CSConnectionRequest request = new CSConnectionRequest(contentType); + assertDoesNotThrow(() -> request.onRequestFinished(mockConnection)); + } + + @Test + void testOnRequestFinishedFetchGlobalFieldsWithNullCallback() throws Exception { + CSHttpConnection mockConnection = createMockConnection(); + + Field controllerField = CSHttpConnection.class.getDeclaredField("controller"); + controllerField.setAccessible(true); + controllerField.set(mockConnection, Constants.FETCHGLOBALFIELDS); + + LinkedHashMap globalFieldMap = new LinkedHashMap<>(); + globalFieldMap.put("uid", "test_global_field"); + + JSONObject response = new JSONObject(); + Field mapField = JSONObject.class.getDeclaredField("map"); + mapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map internalMap = (Map) mapField.get(response); + internalMap.put("global_field", globalFieldMap); + + Field responseField = CSHttpConnection.class.getDeclaredField("responseJSON"); + responseField.setAccessible(true); + responseField.set(mockConnection, response); + + CSConnectionRequest request = new CSConnectionRequest(globalField); + assertDoesNotThrow(() -> request.onRequestFinished(mockConnection)); + } + // ========== HELPER METHODS ========== private CSHttpConnection createMockConnection() throws Exception { diff --git a/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java b/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java index f029754a..4b125e20 100644 --- a/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java +++ b/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java @@ -9,8 +9,11 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -1058,10 +1061,157 @@ void testSetErrorWithValidJSONButMissingAllFields() { CSHttpConnection conn = new CSHttpConnection("https://test.com", csConnectionRequest); conn.setError("{\"some_field\": \"some_value\"}"); - + assertNotNull(csConnectionRequest.error); assertEquals("An unknown error occurred.", csConnectionRequest.error.getString("error_message")); assertEquals("0", csConnectionRequest.error.getString("error_code")); assertEquals("No additional error details available.", csConnectionRequest.error.getString("errors")); } + + // ========== JSON -> ORDERED MAP CONVERSION TESTS ========== + // These exercise the org.json based response parsing that replaced Jackson. + + private static Method jsonToOrderedMapMethod() throws Exception { + Method m = CSHttpConnection.class.getDeclaredMethod("jsonToOrderedMap", JSONObject.class); + m.setAccessible(true); + return m; + } + + private static Method convertJsonValueMethod() throws Exception { + Method m = CSHttpConnection.class.getDeclaredMethod("convertJsonValue", Object.class); + m.setAccessible(true); + return m; + } + + @SuppressWarnings("unchecked") + @Test + void testJsonToOrderedMapWithPrimitives() throws Exception { + JSONObject input = new JSONObject(); + input.put("uid", "abc123"); + input.put("count", 42); + input.put("active", true); + + Map result = (Map) jsonToOrderedMapMethod().invoke(null, input); + + assertTrue(result instanceof LinkedHashMap); + assertEquals("abc123", result.get("uid")); + assertEquals(42, result.get("count")); + assertEquals(true, result.get("active")); + } + + @SuppressWarnings("unchecked") + @Test + void testJsonToOrderedMapWithEmptyObject() throws Exception { + Map result = (Map) jsonToOrderedMapMethod().invoke(null, new JSONObject()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @SuppressWarnings("unchecked") + @Test + void testJsonToOrderedMapWithNestedObjectAndArray() throws Exception { + JSONObject nested = new JSONObject(); + nested.put("x", 1); + + JSONArray array = new JSONArray(); + array.put("a"); + array.put("b"); + + JSONObject input = new JSONObject(); + input.put("nested", nested); + input.put("arr", array); + + Map result = (Map) jsonToOrderedMapMethod().invoke(null, input); + + // Nested objects must become LinkedHashMap and arrays ArrayList, so the + // response models' instanceof checks keep working after the Jackson removal. + assertTrue(result.get("nested") instanceof LinkedHashMap); + assertEquals(1, ((Map) result.get("nested")).get("x")); + assertTrue(result.get("arr") instanceof ArrayList); + assertEquals(2, ((List) result.get("arr")).size()); + assertEquals("a", ((List) result.get("arr")).get(0)); + } + + @Test + void testConvertJsonValueWithNull() throws Exception { + Object result = convertJsonValueMethod().invoke(null, new Object[] { null }); + assertNull(result); + } + + @Test + void testConvertJsonValueWithJsonNull() throws Exception { + Object result = convertJsonValueMethod().invoke(null, JSONObject.NULL); + assertNull(result); + } + + @SuppressWarnings("unchecked") + @Test + void testConvertJsonValueWithJsonObject() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("k", "v"); + + Object result = convertJsonValueMethod().invoke(null, obj); + + assertTrue(result instanceof LinkedHashMap); + assertEquals("v", ((Map) result).get("k")); + } + + @SuppressWarnings("unchecked") + @Test + void testConvertJsonValueWithEmptyJsonArray() throws Exception { + Object result = convertJsonValueMethod().invoke(null, new JSONArray()); + + assertTrue(result instanceof ArrayList); + assertTrue(((List) result).isEmpty()); + } + + @SuppressWarnings("unchecked") + @Test + void testConvertJsonValueWithPopulatedJsonArray() throws Exception { + JSONArray array = new JSONArray(); + array.put(1); + array.put(2); + array.put(3); + + Object result = convertJsonValueMethod().invoke(null, array); + + assertTrue(result instanceof ArrayList); + assertEquals(3, ((List) result).size()); + assertEquals(1, ((List) result).get(0)); + } + + @Test + void testConvertJsonValueWithBigDecimal() throws Exception { + // Floating point numbers must be normalized to Double to match the old parser. + Object result = convertJsonValueMethod().invoke(null, new BigDecimal("3.14")); + + assertTrue(result instanceof Double); + assertEquals(3.14, (Double) result, 0.0); + } + + @Test + void testConvertJsonValueWithPlainString() throws Exception { + Object result = convertJsonValueMethod().invoke(null, "hello"); + assertEquals("hello", result); + } + + @Test + void testConvertJsonValueWithPlainNumber() throws Exception { + Object result = convertJsonValueMethod().invoke(null, 42); + assertEquals(42, result); + } + + @SuppressWarnings("unchecked") + @Test + void testJsonToOrderedMapDropsJsonNullValues() throws Exception { + JSONObject input = new JSONObject(); + input.put("present", "value"); + input.put("missing", JSONObject.NULL); + + Map result = (Map) jsonToOrderedMapMethod().invoke(null, input); + + assertEquals("value", result.get("present")); + assertNull(result.get("missing")); + } }