Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v2.7.2

### Jul 06, 2026
- Snyk fixes

## v2.7.1

### Jun 29, 2026
Expand Down
11 changes: 3 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.contentstack.sdk</groupId>
<artifactId>java</artifactId>
<version>2.7.1</version>
<version>2.7.2</version>
<packaging>jar</packaging>
<name>contentstack-java</name>
<description>Java SDK for Contentstack Content Delivery API</description>
Expand All @@ -24,7 +24,7 @@
<retrofit-source.version>3.0.0</retrofit-source.version>
<loggin.version>5.3.2</loggin.version>
<jococo-plugin.version>0.8.5</jococo-plugin.version>
<lombok-source.version>1.18.42</lombok-source.version>
<lombok-source.version>1.18.44</lombok-source.version>
<junit-jupiter.version>5.11.4</junit-jupiter.version>
<junit-jupiter-engine.version>5.8.0-M1</junit-jupiter-engine.version>
<gson.version>2.8.8</gson.version>
Expand All @@ -33,7 +33,7 @@
<maven-gpg-plugin.version>1.5</maven-gpg-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<nexus-staging-maven-plugin.version>1.6.13</nexus-staging-maven-plugin.version>
<json-version>20251224</json-version>
<json-version>20260522</json-version>
<jacoco-maven-plugin-version>0.8.11</jacoco-maven-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<contentstack-utils-version>1.5.1</contentstack-utils-version>
Expand Down Expand Up @@ -178,11 +178,6 @@
</exclusions>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.4</version>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>bolt</artifactId>
Expand Down
51 changes: 42 additions & 9 deletions src/main/java/com/contentstack/sdk/CSHttpConnection.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -201,6 +200,41 @@ private JSONObject createOrderedJSONObject(Map<String, Object> 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<String, Object> jsonToOrderedMap(JSONObject object) {
LinkedHashMap<String, Object> 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<Object> 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);
Expand All @@ -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<String, Object> 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<String, Object> 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()) {
Expand Down
124 changes: 124 additions & 0 deletions src/test/java/com/contentstack/sdk/TestCSConnectionRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> 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<String, Object> internalMap = (Map<String, Object>) 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<String, Object> contentTypeMap = new LinkedHashMap<>();
contentTypeMap.put("uid", "blog_post");

JSONObject response = new JSONObject();
Field mapField = JSONObject.class.getDeclaredField("map");
mapField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, Object> internalMap = (Map<String, Object>) 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<String, Object> 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<String, Object> internalMap = (Map<String, Object>) 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 {
Expand Down
Loading
Loading