diff --git a/CHANGELOG.md b/CHANGELOG.md index d2fa8498..40a128d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v2.6.0 + +### Feb 23, 2026 +- Enhancement: Asset localization added + ## v2.5.0 ### Feb 12, 2026 diff --git a/pom.xml b/pom.xml index 862b2c78..88218ed2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.contentstack.sdk java - 2.5.0 + 2.6.0 jar contentstack-java Java SDK for Contentstack Content Delivery API diff --git a/src/main/java/com/contentstack/sdk/Asset.java b/src/main/java/com/contentstack/sdk/Asset.java index 9f498245..adf3e492 100644 --- a/src/main/java/com/contentstack/sdk/Asset.java +++ b/src/main/java/com/contentstack/sdk/Asset.java @@ -43,6 +43,7 @@ public class Asset { protected String fileSize = null; protected String fileName = null; protected String uploadUrl = null; + protected String language = null; protected JSONObject json = null; protected String[] tagsArray = null; protected LinkedHashMap headers; @@ -75,6 +76,7 @@ public Asset configure(JSONObject jsonObject) { this.contentType = model.contentType; this.fileSize = model.fileSize; this.uploadUrl = model.uploadUrl; + this.language = model.language; this.fileName = model.fileName; this.json = model.json; this.assetUid = model.uploadedUid; @@ -558,6 +560,38 @@ public Asset assetFields(String... fields) { return this; } + /** + * Specifies the fields to be included in the asset response. + *

+ * This method allows you to specify one or more field names, and only those fields + * will be included in the returned asset data. This is useful for reducing response size + * and fetching only the required asset properties. + * + * @param fields Variable number of field names to be included in the asset response. + * @return The {@link Asset} instance for chaining further calls. + * + * Example:
+ *

+     * Asset asset = stack.asset("asset_uid");
+     * asset.assetFields("title", "filename");
+     * 
+ */ + + public Asset setLocale(String locale) { + urlQueries.put("locale",locale); + return this; + } + + + /** + * Returns the locale (language) associated with this asset. + * + * @return The asset's locale as a {@link String}, or {@code null} if not set. + */ + public String getLocale() { + return this.language; + } + /** * Fetch. * diff --git a/src/main/java/com/contentstack/sdk/AssetLibrary.java b/src/main/java/com/contentstack/sdk/AssetLibrary.java index 29fd0c4b..628ee1ab 100644 --- a/src/main/java/com/contentstack/sdk/AssetLibrary.java +++ b/src/main/java/com/contentstack/sdk/AssetLibrary.java @@ -151,6 +151,28 @@ public AssetLibrary includeMetadata() { return this; } + /** + * Sets the locale for asset queries. + *

+ * This method allows you to specify a locale code, so asset results are returned + * for a particular language or region. If not explicitly set, the default locale + * configured in the stack will be used. + * + * @param locale The locale code to filter assets by (e.g., "en-us"). + * @return The {@link AssetLibrary} instance for method chaining. + * + * Example: + *

+     * Stack stack = Contentstack.stack("apiKey", "deliveryToken", "environment");
+     * AssetLibrary assetLibrary = stack.assetLibrary();
+     * assetLibrary.setLocale("en-us");
+     * 
+ */ + public AssetLibrary setLocale(String locale) { + urlQueries.put("locale",locale); + return this; + } + /** * Gets count. * diff --git a/src/main/java/com/contentstack/sdk/AssetModel.java b/src/main/java/com/contentstack/sdk/AssetModel.java index 3aa4a122..5d0a0b1b 100644 --- a/src/main/java/com/contentstack/sdk/AssetModel.java +++ b/src/main/java/com/contentstack/sdk/AssetModel.java @@ -21,6 +21,7 @@ class AssetModel { String fileSize; String fileName; String uploadUrl; + String language; String[] tags; JSONObject json; int count = 0; @@ -45,6 +46,7 @@ public AssetModel(JSONObject response, boolean isArray) { fileSize = (String) json.opt("file_size"); fileName = (String) json.opt("filename"); uploadUrl = (String) json.opt("url"); + language = (String) json.opt("locale"); if (json.opt("tags") instanceof JSONArray) { extractTags(); } 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/TestAsset.java b/src/test/java/com/contentstack/sdk/TestAsset.java index 715c3777..6a59f09b 100644 --- a/src/test/java/com/contentstack/sdk/TestAsset.java +++ b/src/test/java/com/contentstack/sdk/TestAsset.java @@ -81,6 +81,79 @@ void testConfigureWithMinimalJson() { assertSame(asset, result); } + @Test + void testConfigureWithLocaleSetsLanguage() { + JSONObject json = new JSONObject(); + json.put("uid", "locale_asset_uid"); + json.put("locale", "en-us"); + json.put("filename", "localized.jpg"); + + asset.configure(json); + assertEquals("en-us", asset.getLocale()); + assertEquals("en-us", asset.language); + } + + @Test + void testConfigureWithoutLocaleLeavesLanguageNull() { + JSONObject json = new JSONObject(); + json.put("uid", "no_locale_uid"); + json.put("filename", "test.jpg"); + + asset.configure(json); + assertNull(asset.getLocale()); + assertNull(asset.language); + } + + // ========== LOCALE TESTS (setLocale / getLocale) ========== + + @Test + void testSetLocale() { + Asset result = asset.setLocale("en-us"); + assertSame(asset, result); + assertTrue(asset.urlQueries.has("locale")); + assertEquals("en-us", asset.urlQueries.get("locale")); + } + + @Test + void testSetLocaleReturnsThisForChaining() { + Asset result = asset.setLocale("fr-fr").includeDimension().includeMetadata(); + assertSame(asset, result); + assertEquals("fr-fr", asset.urlQueries.get("locale")); + assertTrue(asset.urlQueries.has("include_dimension")); + assertTrue(asset.urlQueries.has("include_metadata")); + } + + @Test + void testSetLocaleOverwritesPrevious() { + asset.setLocale("en-us"); + assertEquals("en-us", asset.urlQueries.get("locale")); + asset.setLocale("de-de"); + assertEquals("de-de", asset.urlQueries.get("locale")); + } + + @Test + void testGetLocaleBeforeConfigureReturnsNull() { + assertNull(asset.getLocale()); + } + + @Test + void testGetLocaleAfterConfigureWithLocale() { + JSONObject json = new JSONObject(); + json.put("uid", "uid"); + json.put("locale", "ja-jp"); + asset.configure(json); + assertEquals("ja-jp", asset.getLocale()); + } + + @Test + void testGetLocaleAfterSetLocaleOnlySetsQueryNotLanguage() { + asset.setLocale("es-es"); + // setLocale only puts in urlQueries; it does not set this.language + assertTrue(asset.urlQueries.has("locale")); + assertEquals("es-es", asset.urlQueries.get("locale")); + assertNull(asset.getLocale()); // getLocale returns this.language, not urlQueries + } + // ========== HEADER TESTS ========== @Test diff --git a/src/test/java/com/contentstack/sdk/TestAssetLibrary.java b/src/test/java/com/contentstack/sdk/TestAssetLibrary.java index 9092c4f1..8846d843 100644 --- a/src/test/java/com/contentstack/sdk/TestAssetLibrary.java +++ b/src/test/java/com/contentstack/sdk/TestAssetLibrary.java @@ -136,6 +136,41 @@ void testIncludeMetadata() { assertEquals(true, assetLibrary.urlQueries.get("include_metadata")); } + // ========== LOCALE TESTS (setLocale for asset localisation) ========== + + @Test + void testSetLocale() { + AssetLibrary result = assetLibrary.setLocale("en-us"); + assertSame(assetLibrary, result); + assertTrue(assetLibrary.urlQueries.has("locale")); + assertEquals("en-us", assetLibrary.urlQueries.get("locale")); + } + + @Test + void testSetLocaleReturnsThisForChaining() { + AssetLibrary result = assetLibrary.setLocale("fr-fr").includeCount().limit(10); + assertSame(assetLibrary, result); + assertEquals("fr-fr", assetLibrary.urlQueries.get("locale")); + assertTrue(assetLibrary.urlQueries.has("include_count")); + assertEquals(10, assetLibrary.urlQueries.get("limit")); + } + + @Test + void testSetLocaleOverwritesPrevious() { + assetLibrary.setLocale("en-us"); + assertEquals("en-us", assetLibrary.urlQueries.get("locale")); + assetLibrary.setLocale("de-de"); + assertEquals("de-de", assetLibrary.urlQueries.get("locale")); + } + + @Test + void testSetLocaleWithVariousLocaleCodes() { + assetLibrary.setLocale("ja-jp"); + assertEquals("ja-jp", assetLibrary.urlQueries.get("locale")); + assetLibrary.setLocale("pt-br"); + assertEquals("pt-br", assetLibrary.urlQueries.get("locale")); + } + // ========== ASSET FIELDS TESTS (CDA asset_fields[] parameter) ========== @Test diff --git a/src/test/java/com/contentstack/sdk/TestAssetModel.java b/src/test/java/com/contentstack/sdk/TestAssetModel.java index 605c263a..97fd14c6 100644 --- a/src/test/java/com/contentstack/sdk/TestAssetModel.java +++ b/src/test/java/com/contentstack/sdk/TestAssetModel.java @@ -212,6 +212,70 @@ void testConstructorWithAllFields() { assertEquals(2, model.tags.length); } + // ========== LOCALE / LANGUAGE TESTS (asset localisation) ========== + + @Test + void testConstructorWithLocaleIsArrayTrue() { + JSONObject response = new JSONObject(); + response.put("uid", "localized_asset"); + response.put("filename", "localized.jpg"); + response.put("locale", "en-us"); + + AssetModel model = new AssetModel(response, true); + + assertNotNull(model); + assertEquals("localized_asset", model.uploadedUid); + assertEquals("en-us", model.language); + } + + @Test + void testConstructorWithoutLocaleLeavesLanguageNull() { + JSONObject response = new JSONObject(); + response.put("uid", "no_locale_asset"); + response.put("filename", "test.jpg"); + + AssetModel model = new AssetModel(response, true); + + assertNotNull(model); + assertNull(model.language); + } + + @Test + void testConstructorWithVariousLocaleCodes() { + JSONObject response = new JSONObject(); + response.put("uid", "uid"); + response.put("locale", "fr-fr"); + + AssetModel model = new AssetModel(response, true); + assertEquals("fr-fr", model.language); + + JSONObject response2 = new JSONObject(); + response2.put("uid", "uid2"); + response2.put("locale", "ja-jp"); + AssetModel model2 = new AssetModel(response2, true); + assertEquals("ja-jp", model2.language); + } + + @Test + void testConstructorWithLocaleAndOtherFields() { + JSONObject response = new JSONObject(); + response.put("uid", "full_localized"); + response.put("content_type", "image/png"); + response.put("file_size", "1024"); + response.put("filename", "image.png"); + response.put("url", "https://cdn.example.com/image.png"); + response.put("locale", "de-de"); + + AssetModel model = new AssetModel(response, true); + + assertEquals("full_localized", model.uploadedUid); + assertEquals("image/png", model.contentType); + assertEquals("1024", model.fileSize); + assertEquals("image.png", model.fileName); + assertEquals("https://cdn.example.com/image.png", model.uploadUrl); + assertEquals("de-de", model.language); + } + @Test void testConstructorWithMinimalData() { JSONObject response = new JSONObject(); 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();