diff --git a/src/main/java/com/contentstack/sdk/CSHttpConnection.java b/src/main/java/com/contentstack/sdk/CSHttpConnection.java index b60532db..5d5e3549 100644 --- a/src/main/java/com/contentstack/sdk/CSHttpConnection.java +++ b/src/main/java/com/contentstack/sdk/CSHttpConnection.java @@ -268,17 +268,21 @@ private Response pluginResponseImp(Request request, Response { JSONObject objJSON = (JSONObject) finalEntries.get(idx); handleJSONObject(finalEntries, objJSON, idx); }); } - if (responseJSON.has("entry") && !responseJSON.optJSONObject("entry").isEmpty()) { - JSONObject entry = responseJSON.optJSONObject("entry"); - if (!entry.isEmpty()) { - if (entry.has("uid") && entry.opt("uid").equals(this.config.livePreviewEntry.opt("uid"))) { + JSONObject entryObj = responseJSON.optJSONObject("entry"); + if (responseJSON.has("entry") && entryObj != null && !entryObj.isEmpty()) { + JSONObject entry = entryObj; + if (!entry.isEmpty() && this.config.livePreviewEntry != null) { + Object entryUid = entry.opt("uid"); + Object previewUid = this.config.livePreviewEntry.opt("uid"); + if (entryUid != null && java.util.Objects.equals(entryUid, previewUid)) { responseJSON = new JSONObject().put("entry", this.config.livePreviewEntry); } } @@ -287,8 +291,10 @@ void handleJSONArray() { } void handleJSONObject(JSONArray arrayEntry, JSONObject jsonObj, int idx) { - if (!jsonObj.isEmpty()) { - if (jsonObj.has("uid") && jsonObj.opt("uid").equals(this.config.livePreviewEntry.opt("uid"))) { + if (!jsonObj.isEmpty() && this.config.livePreviewEntry != null) { + Object entryUid = jsonObj.opt("uid"); + Object previewUid = this.config.livePreviewEntry.opt("uid"); + if (entryUid != null && java.util.Objects.equals(entryUid, previewUid)) { arrayEntry.put(idx, this.config.livePreviewEntry); } } diff --git a/src/main/java/com/contentstack/sdk/Config.java b/src/main/java/com/contentstack/sdk/Config.java index 5c3eb101..003cb4c7 100644 --- a/src/main/java/com/contentstack/sdk/Config.java +++ b/src/main/java/com/contentstack/sdk/Config.java @@ -206,6 +206,10 @@ protected Config setLivePreviewEntry(@NotNull JSONObject livePreviewEntry) { return this; } + protected void clearLivePreviewEntry() { + this.livePreviewEntry = null; + } + /** * Sets preview token. * diff --git a/src/main/java/com/contentstack/sdk/Entry.java b/src/main/java/com/contentstack/sdk/Entry.java index 14cb47eb..ab2f02a1 100644 --- a/src/main/java/com/contentstack/sdk/Entry.java +++ b/src/main/java/com/contentstack/sdk/Entry.java @@ -946,6 +946,14 @@ public void fetch(EntryResultCallBack callback) { logger.log(Level.SEVERE, ErrorMessages.ENTRY_FETCH_FAILED, e); } } + Config config = contentType.stackInstance.config; + if (config.enableLivePreview && config.livePreviewEntry != null && !config.livePreviewEntry.isEmpty() + && java.util.Objects.equals(config.livePreviewEntryUid, uid) + && contentTypeUid != null && contentTypeUid.equalsIgnoreCase(config.livePreviewContentType)) { + this.configure(config.livePreviewEntry); + callback.onRequestFinish(ResponseType.NETWORK); + return; + } String urlString = "content_types/" + contentTypeUid + "/entries/" + uid; JSONObject urlQueries = new JSONObject(); urlQueries.put(ENVIRONMENT, headers.get(ENVIRONMENT)); diff --git a/src/main/java/com/contentstack/sdk/ErrorMessages.java b/src/main/java/com/contentstack/sdk/ErrorMessages.java index 80739cc1..e62af06e 100644 --- a/src/main/java/com/contentstack/sdk/ErrorMessages.java +++ b/src/main/java/com/contentstack/sdk/ErrorMessages.java @@ -64,6 +64,7 @@ private ErrorMessages() { public static final String MISSING_PREVIEW_TOKEN = "Missing preview token for rest-preview.contentstack.com. Set the preview token in your configuration to use Live Preview."; public static final String LIVE_PREVIEW_NOT_ENABLED = "Live Preview is not enabled in the configuration. Enable it and try again."; + public static final String LIVE_PREVIEW_HOST_NOT_ENABLED = "Live Preview host is not set. Call config.setLivePreviewHost(\"rest-preview.contentstack.com\") (or your preview host) before using Live Preview."; public static final String EMBEDDED_ITEMS_NOT_INCLUDED = "Embedded items are not included in the entry. Call includeEmbeddedItems() and try again."; // ========== OPERATION ERRORS ========== diff --git a/src/main/java/com/contentstack/sdk/Stack.java b/src/main/java/com/contentstack/sdk/Stack.java index cea8be0d..5e7cd71a 100644 --- a/src/main/java/com/contentstack/sdk/Stack.java +++ b/src/main/java/com/contentstack/sdk/Stack.java @@ -1,29 +1,39 @@ package com.contentstack.sdk; -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; +import java.io.IOException; +import java.net.Proxy; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.logging.Logger; +import java.util.stream.Collectors; + import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONObject; +import static com.contentstack.sdk.Constants.CONTENT_TYPE_UID; +import static com.contentstack.sdk.Constants.ENTRY_UID; +import static com.contentstack.sdk.Constants.ENVIRONMENT; +import static com.contentstack.sdk.Constants.LIVE_PREVIEW; import com.contentstack.sdk.Constants.REQUEST_CONTROLLER; +import static com.contentstack.sdk.Constants.SYNCHRONISATION; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; import retrofit2.Response; import retrofit2.Retrofit; -import java.io.IOException; -import java.net.Proxy; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static com.contentstack.sdk.Constants.*; - /** - * Stack call fetches comprehensive details of a specific stack, It allows multiple users to get content of stack + * Stack call fetches comprehensive details of a specific stack, It allows + * multiple users to get content of stack * information based on user credentials. */ public class Stack { @@ -93,23 +103,24 @@ protected void setConfig(Config config) { logger.fine("Info: configs set"); } - //Setting a global client with the connection pool configuration solved the issue + // Setting a global client with the connection pool configuration solved the + // issue private void client(String endpoint) { Proxy proxy = this.config.getProxy(); ConnectionPool pool = this.config.connectionPool; - + // Build OkHttpClient with optional retry interceptor OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .proxy(proxy) .connectionPool(pool); - + // Add retry interceptor if enabled RetryOptions retryOptions = this.config.getRetryOptions(); if (retryOptions != null && retryOptions.isRetryEnabled()) { clientBuilder.addInterceptor(new RetryInterceptor(retryOptions)); logger.fine("Retry interceptor added with options: " + retryOptions); } - + OkHttpClient client = clientBuilder.build(); Retrofit retrofit = new Retrofit.Builder().baseUrl(endpoint) @@ -119,14 +130,13 @@ private void client(String endpoint) { this.service = retrofit.create(APIService.class); } - private void includeLivePreview() { if (config.enableLivePreview) { String urlLivePreview = config.livePreviewHost; - if(config.region != null && !config.region.name().isEmpty()){ - if(config.region.name().equals("US") ){ + if (config.region != null && !config.region.name().isEmpty()) { + if (config.region.name().equals("US")) { config.livePreviewHost = urlLivePreview; - }else{ + } else { String regionPrefix = config.region.name().toLowerCase(); config.livePreviewHost = regionPrefix + "-" + urlLivePreview; } @@ -174,6 +184,9 @@ public Stack livePreviewQuery(Map query) throws IOException { config.previewTimestamp = null; } + if(config.livePreviewHost == null || config.livePreviewHost.trim().isEmpty()){ + throw new IllegalStateException(ErrorMessages.LIVE_PREVIEW_HOST_NOT_ENABLED); + } String livePreviewUrl = this.livePreviewEndpoint.concat(config.livePreviewContentType).concat("/entries/" + config.livePreviewEntryUid); if (livePreviewUrl.contains("/null/")) { throw new IllegalStateException(ErrorMessages.INVALID_QUERY_URL); @@ -183,6 +196,10 @@ public Stack livePreviewQuery(Map query) throws IOException { LinkedHashMap liveHeader = new LinkedHashMap<>(); liveHeader.put("api_key", this.headers.get("api_key")); + if (config.livePreviewHash != null && !config.livePreviewHash.isEmpty()) { + liveHeader.put("live_preview", config.livePreviewHash); + } + if(config.livePreviewHost.equals("rest-preview.contentstack.com")) { if(config.previewToken != null) { @@ -197,31 +214,66 @@ public Stack livePreviewQuery(Map query) throws IOException { } catch (IOException e) { throw new IllegalStateException(ErrorMessages.LIVE_PREVIEW_URL_FAILED); } - if (response.isSuccessful()) { - assert response.body() != null; + + config.clearLivePreviewEntry(); + if (response.isSuccessful() && response.body() != null) { String resp = response.body().string(); if (!resp.isEmpty()) { - JSONObject liveResponse = new JSONObject(resp); - config.setLivePreviewEntry(liveResponse.getJSONObject("entry")); + try { + JSONObject liveResponse = new JSONObject(resp); + // Parse draft entry from Preview API: support both "entry" (single) and "entries" (array) response formats + JSONObject draftEntry = null; + if (liveResponse.has("entry") && !liveResponse.isNull("entry")) { + draftEntry = liveResponse.getJSONObject("entry"); + } else if (liveResponse.has("entries")) { + JSONArray entries = liveResponse.optJSONArray("entries"); + if (entries != null && entries.length() > 0) { + String targetUid = config.livePreviewEntryUid; + for (int i = 0; i < entries.length(); i++) { + if (!entries.isNull(i)) { + JSONObject e = entries.getJSONObject(i); + if (targetUid != null && targetUid.equals(e.optString("uid", ""))) { + draftEntry = e; + break; + } + } + } + if (draftEntry == null) { + draftEntry = entries.getJSONObject(0); + } + } + } + if (draftEntry != null && !draftEntry.isEmpty()) { + config.setLivePreviewEntry(draftEntry); + } + } catch (Exception e) { + logger.warning(e.getMessage() != null ? e.getMessage() : "Live Preview: failed to parse Preview API response"); + } } } - } else { - throw new IllegalStateException(ErrorMessages.LIVE_PREVIEW_NOT_ENABLED); - } + + } else { + config.clearLivePreviewEntry(); + throw new IllegalStateException(ErrorMessages.LIVE_PREVIEW_NOT_ENABLED); + } return this; } /** - * Content type defines the structure or schema of a page or a section of your web or mobile property. To create - * content for your application, you are required to first create a content type, and then create entries using the + * Content type defines the structure or schema of a page or a section of your + * web or mobile property. To create + * content for your application, you are required to first create a content + * type, and then create entries using the * content type. * - * @param contentTypeUid Enter the unique ID of the content type of which you want to retrieve the entries. The UID is often based - * on the title of the content type, and it is unique across a stack. + * @param contentTypeUid Enter the unique ID of the content type of which you + * want to retrieve the entries. The UID is often based + * on the title of the content type, and it is unique + * across a stack. * @return the {@link ContentType} - *

- * Example - * + *

+ * Example + * * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); ContentType contentType = * stack.contentType("contentTypeUid") * @@ -233,7 +285,7 @@ public ContentType contentType(String contentTypeUid) { return ct; } - public GlobalField globalField(@NotNull String globalFieldUid) { + public GlobalField globalField(@NotNull String globalFieldUid) { this.globalField = globalFieldUid; GlobalField gf = new GlobalField(globalFieldUid); gf.setStackInstance(this); @@ -247,19 +299,26 @@ public GlobalField globalField() { } /** - * Assets refer to all the media files (images, videos, PDFs, audio files, and so on) uploaded in your Contentstack - * repository for future use. These files can be attached and used in multiple entries. + * Assets refer to all the media files (images, videos, PDFs, audio files, and + * so on) uploaded in your Contentstack + * repository for future use. These files can be attached and used in multiple + * entries. *

- * The Get a single asset request fetches the latest version of a specific asset of a particular stack. + * The Get a single asset request fetches the latest version of a specific asset + * of a particular stack. *

* * @param uid uid of {@link Asset} - * @return {@link Asset} instance Tip: If no version is mentioned, the request will retrieve the latest - * published version of the asset. To retrieve a specific version, use the version parameter, keep the environment - * parameter blank, and use the management token instead of the delivery token. - *

- * Example Stack stack = contentstack.Stack("apiKey", - * "deliveryToken", "environment"); Asset asset = stack.asset("assetUid"); + * @return {@link Asset} instance Tip: If no version is mentioned, the + * request will retrieve the latest + * published version of the asset. To retrieve a specific version, use + * the version parameter, keep the environment + * parameter blank, and use the management token instead of the delivery + * token. + *

+ * Example Stack stack = contentstack.Stack("apiKey", + * "deliveryToken", "environment"); Asset asset = + * stack.asset("assetUid"); */ public Asset asset(@NotNull String uid) { Asset asset = new Asset(uid); @@ -274,14 +333,15 @@ protected Asset asset() { } /** - * The Get all assets request fetches the list of all the assets of a particular stack. It returns the content of + * The Get all assets request fetches the list of all the assets of a particular + * stack. It returns the content of * each asset in JSON format. * * @return {@link AssetLibrary} asset library - *

- * Example - *

- * + *

+ * Example + *

+ * * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); AssetLibrary assets = * stack.assetLibrary(); * @@ -301,7 +361,6 @@ public String getApplicationKey() { return apiKey; } - /** * Returns deliveryToken of particular stack * @@ -335,8 +394,10 @@ public void setHeader(@NotNull String headerKey, @NotNull String headerValue) { } /** - * Image transform string. This document is a detailed reference to Contentstack Image Delivery API and covers the - * parameters that you can add to the URL to retrieve, manipulate (or convert) image files and display it to your + * Image transform string. This document is a detailed reference to Contentstack + * Image Delivery API and covers the + * parameters that you can add to the URL to retrieve, manipulate (or convert) + * image files and display it to your * web or mobile properties. * * @param imageUrl the image url @@ -361,11 +422,13 @@ protected String getQueryParam(Map params) { } /** - * The Get all content types call returns comprehensive information of all the content types available in a + * The Get all content types call returns comprehensive information of all the + * content types available in a * particular stack in your account.. * * @param params query parameters - * @param callback ContentTypesCallback This call returns comprehensive information of all the content types available in a + * @param callback ContentTypesCallback This call returns comprehensive + * information of all the content types available in a * particular stack in your account. */ public void getContentTypes(@NotNull JSONObject params, final ContentTypesCallback callback) { @@ -383,8 +446,10 @@ public void getContentTypes(@NotNull JSONObject params, final ContentTypesCallba } /** - * The Sync request performs a complete sync of your app data. It returns all the published entries and assets of - * the specified stack in response. The response also contains a sync token, which you need to store, since this + * The Sync request performs a complete sync of your app data. It returns all + * the published entries and assets of + * the specified stack in response. The response also contains a sync token, + * which you need to store, since this * token is used to get subsequent delta * * @param syncCallBack returns callback for sync result. @@ -398,18 +463,26 @@ public void sync(SyncResultCallBack syncCallBack) { /** * Sync pagination token. * - * @param paginationToken If the response is paginated, use the pagination token under this parameter. + * @param paginationToken If the response is paginated, use the pagination token + * under this parameter. * @param syncCallBack returns callback for sync result *

- * If the result of the initial sync (or subsequent sync) contains more than 100 records, the response would - * be paginated. It provides pagination token in the response. However, you do not have to use the - * pagination token manually to get the next batch, the SDK does that automatically until the sync is - * complete. Pagination token can be used in case you want to fetch only selected batches. It is especially - * useful if the sync process is interrupted midway (due to network issues, etc.). In such cases, this token - * can be used to restart the sync process from where it was interrupted.
+ * If the result of the initial sync (or subsequent sync) + * contains more than 100 records, the response would + * be paginated. It provides pagination token in the + * response. However, you do not have to use the + * pagination token manually to get the next batch, the + * SDK does that automatically until the sync is + * complete. Pagination token can be used in case you + * want to fetch only selected batches. It is especially + * useful if the sync process is interrupted midway (due + * to network issues, etc.). In such cases, this token + * can be used to restart the sync process from where it + * was interrupted.
*
* Example :
- * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); + * Stack stack = contentstack.Stack("apiKey", + * "deliveryToken", "environment"); * stack.syncPaginationToken("paginationToken) */ public void syncPaginationToken(@NotNull String paginationToken, SyncResultCallBack syncCallBack) { @@ -421,16 +494,21 @@ public void syncPaginationToken(@NotNull String paginationToken, SyncResultCallB /** * Sync token. * - * @param syncToken Use the sync token that you received in the previous/initial sync under this parameter. + * @param syncToken Use the sync token that you received in the + * previous/initial sync under this parameter. * @param syncCallBack returns callback for sync result *

- * You can use the sync token (that you receive after initial sync) to get the updated content next time. - * The sync token fetches only the content that was added after your last sync, and the details of the + * You can use the sync token (that you receive after + * initial sync) to get the updated content next time. + * The sync token fetches only the content that was added + * after your last sync, and the details of the * content that was deleted or updated.
*
* Example :
+ * *

-     *   Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment");                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
+ * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); + * */ public void syncToken(String syncToken, SyncResultCallBack syncCallBack) { syncParams = new JSONObject(); @@ -444,11 +522,13 @@ public void syncToken(String syncToken, SyncResultCallBack syncCallBack) { * @param fromDate Enter the start date for initial sync. * @param syncCallBack Returns callback for sync result. *

- * You can also initialize sync with entries published after a specific date. To do this, use syncWithDate + * You can also initialize sync with entries published after + * a specific date. To do this, use syncWithDate * and specify the start date as its value.
*
* Example :
- * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); + * Stack stack = contentstack.Stack("apiKey", + * "deliveryToken", "environment"); * stack.syncFromDate("fromDate") */ public void syncFromDate(@NotNull Date fromDate, SyncResultCallBack syncCallBack) { @@ -472,13 +552,16 @@ protected String convertUTCToISO(Date date) { * @param contentType Provide uid of your content_type * @param syncCallBack Returns callback for sync result. *

- * You can also initialize sync with entries of only specific content_type. To do this, use syncContentType - * and specify the content type uid as its value. However, if you do this, the subsequent syncs will only + * You can also initialize sync with entries of only + * specific content_type. To do this, use syncContentType + * and specify the content type uid as its value. However, + * if you do this, the subsequent syncs will only * include the entries of the specified content_type.
*
* Example : *

- * stack.syncContentType(String content_type, new SyncResultCallBack()){ } + * stack.syncContentType(String content_type, new + * SyncResultCallBack()){ } */ public void syncContentType(@NotNull String contentType, SyncResultCallBack syncCallBack) { syncParams = new JSONObject(); @@ -493,12 +576,16 @@ public void syncContentType(@NotNull String contentType, SyncResultCallBack sync * @param localeCode Select the required locale code. * @param syncCallBack Returns callback for sync result. *

- * You can also initialize sync with entries of only specific locales. To do this, use syncLocale and - * specify the locale code as its value. However, if you do this, the subsequent syncs will only include the + * You can also initialize sync with entries of only + * specific locales. To do this, use syncLocale and + * specify the locale code as its value. However, if you do + * this, the subsequent syncs will only include the * entries of the specified locales.
*
* Example :
- * Stack stack = contentstack.Stack("apiKey", "deliveryToken", "environment"); stack.syncContentType(String + * Stack stack = contentstack.Stack("apiKey", + * "deliveryToken", "environment"); + * stack.syncContentType(String * content_type, new SyncResultCallBack()){ } */ public void syncLocale(String localeCode, SyncResultCallBack syncCallBack) { @@ -511,15 +598,20 @@ public void syncLocale(String localeCode, SyncResultCallBack syncCallBack) { /** * Sync publish type. * - * @param publishType Use the type parameter to get a specific type of content like + * @param publishType Use the type parameter to get a specific type of content + * like *

- * (asset_published, entry_published, asset_unpublished, asset_deleted, entry_unpublished, entry_deleted, + * (asset_published, entry_published, asset_unpublished, + * asset_deleted, entry_unpublished, entry_deleted, * content_type_deleted.) * @param syncCallBack returns callback for sync result. *

- * Use the type parameter to get a specific type of content. You can pass one of the following values: - * asset_published, entry_published, asset_unpublished, asset_deleted, entry_unpublished, entry_deleted, - * content_type_deleted. If you do not specify any value, it will bring all published entries and published + * Use the type parameter to get a specific type of content. + * You can pass one of the following values: + * asset_published, entry_published, asset_unpublished, + * asset_deleted, entry_unpublished, entry_deleted, + * content_type_deleted. If you do not specify any value, it + * will bring all published entries and published * assets. *
*
@@ -545,14 +637,16 @@ public void syncPublishType(PublishType publishType, SyncResultCallBack syncCall * @param publishType type as PublishType * @param syncCallBack Callback *

- * You can also initialize sync with entries that satisfy multiple parameters. To do this, use syncWith and - * specify the parameters. However, if you do this, the subsequent syncs will only include the entries of + * You can also initialize sync with entries that satisfy + * multiple parameters. To do this, use syncWith and + * specify the parameters. However, if you do this, the + * subsequent syncs will only include the entries of * the specified parameters
*
* Example :
*/ public void sync(String contentType, Date fromDate, String localeCode, - PublishType publishType, SyncResultCallBack syncCallBack) { + PublishType publishType, SyncResultCallBack syncCallBack) { String newDate = convertUTCToISO(fromDate); syncParams = new JSONObject(); syncParams.put("init", true); @@ -570,9 +664,8 @@ private void requestSync(final SyncResultCallBack callback) { fetchFromNetwork(SYNCHRONISATION, syncParams, this.headers, callback); } - private void fetchContentTypes(String urlString, JSONObject - contentTypeParam, HashMap headers, - ContentTypesCallback callback) { + private void fetchContentTypes(String urlString, JSONObject contentTypeParam, HashMap headers, + ContentTypesCallback callback) { if (callback != null) { HashMap queryParam = getUrlParams(contentTypeParam); String requestInfo = REQUEST_CONTROLLER.CONTENTTYPES.toString(); @@ -582,7 +675,7 @@ private void fetchContentTypes(String urlString, JSONObject } private void fetchFromNetwork(String urlString, JSONObject urlQueries, - HashMap headers, SyncResultCallBack callback) { + HashMap headers, SyncResultCallBack callback) { if (callback != null) { HashMap urlParams = getUrlParams(urlQueries); String requestInfo = REQUEST_CONTROLLER.SYNC.toString(); @@ -603,18 +696,18 @@ private HashMap getUrlParams(JSONObject jsonQuery) { return hashMap; } - public Taxonomy taxonomy() { - return new Taxonomy(this.service,this.config, this.headers); + return new Taxonomy(this.service, this.config, this.headers); } - /** * The enum Publish type. + * * @since : v3.11.0 */ public enum PublishType { - //asset_deleted, asset_published, asset_unpublished, content_type_deleted, entry_deleted, entry_published, + // asset_deleted, asset_published, asset_unpublished, content_type_deleted, + // entry_deleted, entry_published, // Breaking change in v3.11.0 ASSET_DELETED, ASSET_PUBLISHED, @@ -624,6 +717,7 @@ public enum PublishType { ENTRY_PUBLISHED, ENTRY_UNPUBLISHED } + public void updateAssetUrl(Entry entry) { JSONObject entryJson = entry.toJSON(); // Check if entry consists of _embedded_items object @@ -644,37 +738,38 @@ public void updateAssetUrl(Entry entry) { if ("sys_assets".equals(item.getString("_content_type_uid")) && item.has("filename")) { String url = item.getString("url"); String uid = item.getString("uid"); - assetUrls.put(uid,url); + assetUrls.put(uid, url); } } } } updateChildObjects(entryJson, assetUrls); } + private void updateChildObjects(JSONObject entryJson, Map assetUrls) { Iterator mainKeys = entryJson.keys(); while (mainKeys.hasNext()) { String key = mainKeys.next(); Object childObj = entryJson.get(key); - if(childObj instanceof JSONObject) - { JSONObject mainKey = (JSONObject) childObj; - if (mainKey.has("children")) { - JSONArray mainList = mainKey.getJSONArray("children"); - for (int i = 0; i < mainList.length(); i++) { - JSONObject list = mainList.getJSONObject(i); - if (list.has("attrs") ) { - JSONObject childList = list.getJSONObject("attrs"); - if(childList.has("asset-uid") && childList.has("asset-link")){ - String assetUid = childList.getString("asset-uid"); - if (assetUrls.containsKey(assetUid)) { - childList.put("asset-link", assetUrls.get(assetUid)); + if (childObj instanceof JSONObject) { + JSONObject mainKey = (JSONObject) childObj; + if (mainKey.has("children")) { + JSONArray mainList = mainKey.getJSONArray("children"); + for (int i = 0; i < mainList.length(); i++) { + JSONObject list = mainList.getJSONObject(i); + if (list.has("attrs")) { + JSONObject childList = list.getJSONObject("attrs"); + if (childList.has("asset-uid") && childList.has("asset-link")) { + String assetUid = childList.getString("asset-uid"); + if (assetUrls.containsKey(assetUid)) { + childList.put("asset-link", assetUrls.get(assetUid)); + } + } } } } } } - } - } } } diff --git a/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java b/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java index bf2a097e..f029754a 100644 --- a/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java +++ b/src/test/java/com/contentstack/sdk/TestCSHttpConnection.java @@ -598,6 +598,36 @@ void testHandleJSONArrayWithSingleEntry() throws Exception { assertTrue(updatedResponse.has("entry")); } + @Test + void testHandleJSONArrayWhenEntryUidDoesNotMatch() throws Exception { + Config config = new Config(); + JSONObject livePreviewEntry = new JSONObject(); + livePreviewEntry.put("uid", "preview_uid"); + livePreviewEntry.put("title", "Preview Title"); + config.setLivePreviewEntry(livePreviewEntry); + connection.setConfig(config); + + JSONObject responseJSON = new JSONObject(); + JSONObject entry = new JSONObject(); + entry.put("uid", "other_uid"); + entry.put("title", "Original Title"); + responseJSON.put("entry", entry); + + Field responseField = CSHttpConnection.class.getDeclaredField("responseJSON"); + responseField.setAccessible(true); + responseField.set(connection, responseJSON); + + Method handleJSONArrayMethod = CSHttpConnection.class.getDeclaredMethod("handleJSONArray"); + handleJSONArrayMethod.setAccessible(true); + handleJSONArrayMethod.invoke(connection); + + JSONObject updatedResponse = (JSONObject) responseField.get(connection); + assertNotNull(updatedResponse); + assertTrue(updatedResponse.has("entry")); + assertEquals("other_uid", updatedResponse.getJSONObject("entry").optString("uid")); + assertEquals("Original Title", updatedResponse.getJSONObject("entry").optString("title")); + } + @Test void testHandleJSONObjectWithMatchingUid() throws Exception { // Create a config with livePreviewEntry diff --git a/src/test/java/com/contentstack/sdk/TestConfig.java b/src/test/java/com/contentstack/sdk/TestConfig.java index da0eba65..b1137fd9 100644 --- a/src/test/java/com/contentstack/sdk/TestConfig.java +++ b/src/test/java/com/contentstack/sdk/TestConfig.java @@ -265,6 +265,18 @@ void testSetLivePreviewEntryChaining() { assertEquals("blog_post", config.livePreviewEntry.opt("content_type")); } + @Test + void testClearLivePreviewEntry() throws Exception { + JSONObject entry = new JSONObject(); + entry.put("uid", "entry_123"); + config.setLivePreviewEntry(entry); + assertNotNull(config.livePreviewEntry); + java.lang.reflect.Method clearMethod = Config.class.getDeclaredMethod("clearLivePreviewEntry"); + clearMethod.setAccessible(true); + clearMethod.invoke(config); + assertNull(config.livePreviewEntry); + } + // ========== PREVIEW TOKEN TESTS ========== @Test diff --git a/src/test/java/com/contentstack/sdk/TestEntry.java b/src/test/java/com/contentstack/sdk/TestEntry.java index 827cfabc..c73cd9ce 100644 --- a/src/test/java/com/contentstack/sdk/TestEntry.java +++ b/src/test/java/com/contentstack/sdk/TestEntry.java @@ -1448,4 +1448,31 @@ public void onCompletion(ResponseType responseType, Error error) { // This will call setIncludeJSON with all these params assertDoesNotThrow(() -> entry.fetch(callback)); } + + @Test + void testFetchReturnsCachedDraftWhenLivePreviewMatchingEntry() throws IllegalAccessException { + Config config = new Config(); + config.enableLivePreview(true); + config.setLivePreviewHost("rest-preview.contentstack.com"); + JSONObject draftEntry = new JSONObject(); + draftEntry.put("uid", "entry1"); + draftEntry.put("title", "Draft Title"); + draftEntry.put("locale", "en-us"); + config.setLivePreviewEntry(draftEntry); + config.livePreviewEntryUid = "entry1"; + config.livePreviewContentType = "page"; + Stack stack = Contentstack.stack("api_key", "delivery_token", "env", config); + ContentType ct = stack.contentType("page"); + Entry entry = ct.entry("entry1"); + final boolean[] callbackInvoked = { false }; + EntryResultCallBack callback = new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + callbackInvoked[0] = true; + } + }; + entry.fetch(callback); + assertTrue(callbackInvoked[0], "Callback should be invoked when returning cached draft"); + assertEquals("Draft Title", entry.getTitle()); + } } diff --git a/src/test/java/com/contentstack/sdk/TestStack.java b/src/test/java/com/contentstack/sdk/TestStack.java index 84a5672f..1232ad4d 100644 --- a/src/test/java/com/contentstack/sdk/TestStack.java +++ b/src/test/java/com/contentstack/sdk/TestStack.java @@ -1542,6 +1542,42 @@ void testLivePreviewQueryWithDisabledLivePreview() { assertThrows(IllegalStateException.class, () -> stack.livePreviewQuery(query)); } + @Test + void testLivePreviewQueryThrowsWhenLivePreviewHostIsNull() throws IllegalAccessException { + Config config = new Config(); + config.setHost("api.contentstack.io"); + config.enableLivePreview(false); + stack.setConfig(config); + config.enableLivePreview(true); + stack.headers.put("api_key", "test_api_key"); + + Map query = new HashMap<>(); + query.put("live_preview", "hash123"); + query.put("content_type_uid", "blog"); + query.put("entry_uid", "entry123"); + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> stack.livePreviewQuery(query)); + assertTrue(ex.getMessage().contains(ErrorMessages.LIVE_PREVIEW_HOST_NOT_ENABLED)); + } + + @Test + void testLivePreviewQueryThrowsWhenLivePreviewHostIsEmpty() throws IllegalAccessException { + Config config = new Config(); + config.setHost("api.contentstack.io"); + config.enableLivePreview(true); + config.setLivePreviewHost(""); + stack.setConfig(config); + stack.headers.put("api_key", "test_api_key"); + + Map query = new HashMap<>(); + query.put("live_preview", "hash123"); + query.put("content_type_uid", "blog"); + query.put("entry_uid", "entry123"); + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> stack.livePreviewQuery(query)); + assertTrue(ex.getMessage().contains(ErrorMessages.LIVE_PREVIEW_HOST_NOT_ENABLED)); + } + @Test void testLivePreviewQueryWithNullContentType() { Config config = new Config();