Skip to content
Draft
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# CHANGELOG

## Version 4.4.0

### Date: 20-Jul-2026

### Feature

- Branch support in entry variants

## Version 4.3.0

### Date: 29-Jun-2026
Expand Down
7 changes: 6 additions & 1 deletion contentstack/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
ext {
PUBLISH_GROUP_ID = 'com.contentstack.sdk'
PUBLISH_ARTIFACT_ID = 'android'
PUBLISH_VERSION = '4.3.0'
PUBLISH_VERSION = '4.4.0'
}

android {
Expand Down Expand Up @@ -108,6 +108,7 @@ android {
buildConfigField "String", "variantUID", getPropertyOrEmpty('variantUID')
buildConfigField "String", "variantEntryUID", getPropertyOrEmpty('variantEntryUID')
buildConfigField "String[]", "variantsUID", variantsAsArrayString
buildConfigField "String", "variantBranch", getPropertyOrEmpty('variantBranch')
}
release {
minifyEnabled false
Expand Down Expand Up @@ -144,6 +145,10 @@ dependencies {
}
}
}
// androidx.test/espresso pull bcprov-jdk18on 1.80 (test-only); force the patched
// release for Snyk SNYK-JAVA-ORGBOUNCYCASTLE-16074612 (CVE-2026-5598, Timing Attack)
// and SNYK-JAVA-ORGBOUNCYCASTLE-16075266 (CVE-2025-14813). Not shipped in the AAR.
configurations.configureEach { resolutionStrategy.force 'org.bouncycastle:bcprov-jdk18on:1.85' }
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.android.volley:volley:$volley"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class EntryTestCase {
private static String variantUID = BuildConfig.variantUID;
private static String variantEntryUID = BuildConfig.variantEntryUID;
private static String[] variantsUID = BuildConfig.variantsUID;
private static String variantBranch = BuildConfig.variantBranch;


@BeforeClass
Expand Down Expand Up @@ -373,4 +374,44 @@ public void onCompletion(ResponseType responseType, Error error) {
latch.await();
}

@Test
public void VariantsTestSingleUidWithBranch() throws InterruptedException {
Assume.assumeFalse("variantEntryUID not configured", variantEntryUID == null || variantEntryUID.trim().isEmpty());
Assume.assumeFalse("variantUID not configured", variantUID == null || variantUID.trim().isEmpty());
Assume.assumeFalse("variantBranch not configured", variantBranch == null || variantBranch.trim().isEmpty());
final CountDownLatch latch = new CountDownLatch(1);
final Entry entry = stack.contentType("product").entry(variantEntryUID).variants(variantUID, variantBranch);
assertEquals(variantUID.trim(), entry.getHeaders().get("x-cs-variant-uid"));
assertEquals(variantBranch.trim(), entry.getHeaders().get("branch"));

entry.fetch(new EntryResultCallBack() {
@Override
public void onCompletion(ResponseType responseType, Error error) {
System.out.println(entry.toJSON());
latch.countDown();
}
});
assertTrue("fetch() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

@Test
public void VariantsTestArrayWithBranch() throws InterruptedException {
Assume.assumeFalse("variantEntryUID not configured", variantEntryUID == null || variantEntryUID.trim().isEmpty());
Assume.assumeFalse("variantsUID not configured", variantsUID == null || variantsUID.length == 0);
Assume.assumeFalse("variantBranch not configured", variantBranch == null || variantBranch.trim().isEmpty());
final CountDownLatch latch = new CountDownLatch(1);
final Entry entry = stack.contentType("product").entry(variantEntryUID).variants(variantsUID, variantBranch);
assertNotNull(entry.getHeaders().get("x-cs-variant-uid"));
assertEquals(variantBranch.trim(), entry.getHeaders().get("branch"));

entry.fetch(new EntryResultCallBack() {
@Override
public void onCompletion(ResponseType responseType, Error error) {
System.out.println(entry.toJSON());
latch.countDown();
}
});
assertTrue("fetch() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.*;


public class QueryTestCase {

private static final String TAG = AssetTestCase.class.getSimpleName();
private static final String contentTypeUID = BuildConfig.contentTypeUID;
private static final String variantUID = BuildConfig.variantUID;
private static final String[] variantsUID = BuildConfig.variantsUID;
private static final String variantBranch = BuildConfig.variantBranch;
private static Query query;

static {
Expand Down Expand Up @@ -116,4 +121,90 @@ public void onCompletion(ResponseType responseType, QueryResult queryresult, Err
});
}

@Test
public void test_42_variants_single_uid_find() throws Exception {
Assume.assumeFalse("variantUID not configured", variantUID == null || variantUID.trim().isEmpty());
final Query q = TestCred.stack().contentType(contentTypeUID).query();
q.variants(variantUID);
assertEquals(variantUID.trim(), q.getHeaders().get("x-cs-variant-uid"));
assertNull(q.getHeaders().get("branch"));

final CountDownLatch latch = new CountDownLatch(1);
q.find(new QueryResultsCallBack() {
@Override
public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) {
if (error == null) {
Log.d(TAG, "variants single find: " + queryresult.getResultObjects().size() + " entries");
}
latch.countDown();
}
});
assertTrue("find() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

@Test
public void test_43_variants_array_find() throws Exception {
Assume.assumeFalse("variantsUID not configured", variantsUID == null || variantsUID.length == 0);
final Query q = TestCred.stack().contentType(contentTypeUID).query();
q.variants(variantsUID);
assertNotNull(q.getHeaders().get("x-cs-variant-uid"));
assertNull(q.getHeaders().get("branch"));

final CountDownLatch latch = new CountDownLatch(1);
q.find(new QueryResultsCallBack() {
@Override
public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) {
if (error == null) {
Log.d(TAG, "variants array find: " + queryresult.getResultObjects().size() + " entries");
}
latch.countDown();
}
});
assertTrue("find() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

@Test
public void test_44_variants_single_uid_with_branch_find() throws Exception {
Assume.assumeFalse("variantUID not configured", variantUID == null || variantUID.trim().isEmpty());
Assume.assumeFalse("variantBranch not configured", variantBranch == null || variantBranch.trim().isEmpty());
final Query q = TestCred.stack().contentType(contentTypeUID).query();
q.variants(variantUID, variantBranch);
assertEquals(variantUID.trim(), q.getHeaders().get("x-cs-variant-uid"));
assertEquals(variantBranch.trim(), q.getHeaders().get("branch"));

final CountDownLatch latch = new CountDownLatch(1);
q.find(new QueryResultsCallBack() {
@Override
public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) {
if (error == null) {
Log.d(TAG, "variants single+branch find: " + queryresult.getResultObjects().size() + " entries");
}
latch.countDown();
}
});
assertTrue("find() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

@Test
public void test_45_variants_array_with_branch_find() throws Exception {
Assume.assumeFalse("variantsUID not configured", variantsUID == null || variantsUID.length == 0);
Assume.assumeFalse("variantBranch not configured", variantBranch == null || variantBranch.trim().isEmpty());
final Query q = TestCred.stack().contentType(contentTypeUID).query();
q.variants(variantsUID, variantBranch);
assertNotNull(q.getHeaders().get("x-cs-variant-uid"));
assertEquals(variantBranch.trim(), q.getHeaders().get("branch"));

final CountDownLatch latch = new CountDownLatch(1);
q.find(new QueryResultsCallBack() {
@Override
public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) {
if (error == null) {
Log.d(TAG, "variants array+branch find: " + queryresult.getResultObjects().size() + " entries");
}
latch.countDown();
}
});
assertTrue("find() callback timed out", latch.await(30, TimeUnit.SECONDS));
}

}
34 changes: 33 additions & 1 deletion contentstack/src/main/java/com/contentstack/sdk/Entry.java
Original file line number Diff line number Diff line change
Expand Up @@ -1532,7 +1532,7 @@ public Entry includeMetadata() {
* stack.contentType("user").entry("entry_uid").variant("variant_uid").fetch();
*/
public Entry variants(String variants){
if (variants != null && variants.length() > 0) {
if (variants != null && !variants.trim().isEmpty()) {
this.localHeader.put("x-cs-variant-uid", variants.trim());
}
return this;
Expand All @@ -1551,6 +1551,38 @@ public Entry variants(String[] variants){
}
return this;
}

/**
* Sets the variant UID and scopes the request to the given branch.
* Sets {@code x-cs-variant-uid} and {@code branch} request headers.
*
* @param variantUid single variant UID
* @param branch branch name to scope the request
* @return {@link Entry}
*/
public Entry variants(String variantUid, String branch) {
variants(variantUid);
if (branch != null && !branch.trim().isEmpty()) {
this.localHeader.put("branch", branch.trim());
}
return this;
}
Comment thread
reeshika-h marked this conversation as resolved.

/**
* Sets multiple variant UIDs and scopes the request to the given branch.
* Sets {@code x-cs-variant-uid} and {@code branch} request headers.
*
* @param variantUids array of variant UIDs
* @param branch branch name to scope the request
* @return {@link Entry}
*/
public Entry variants(String[] variantUids, String branch) {
variants(variantUids);
if (branch != null && !branch.trim().isEmpty()) {
this.localHeader.put("branch", branch.trim());
}
return this;
}
public ArrayMap<String, Object> getHeaders() {
return localHeader;
}
Expand Down
70 changes: 70 additions & 0 deletions contentstack/src/main/java/com/contentstack/sdk/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -2025,5 +2025,75 @@ public Query includeMetadata() {
return this;
}

/**
* Sets the variant UID for this query via the {@code x-cs-variant-uid} request header.
*
* @param variantUid single variant UID
* @return {@link Query}
*/
public Query variants(String variantUid) {
if (variantUid != null && !variantUid.trim().isEmpty()) {
localHeader.put("x-cs-variant-uid", variantUid.trim());
}
return this;
}

/**
* Sets multiple variant UIDs for this query via the {@code x-cs-variant-uid} request header.
*
* @param variantUids array of variant UIDs
* @return {@link Query}
*/
public Query variants(String[] variantUids) {
if (variantUids != null && variantUids.length > 0) {
List<String> variantList = new ArrayList<>();
for (String variant : variantUids) {
if (variant != null && !variant.trim().isEmpty()) {
variantList.add(variant.trim());
}
}
if (!variantList.isEmpty()) {
localHeader.put("x-cs-variant-uid", String.join(", ", variantList));
}
}
return this;
}

/**
* Sets the variant UID and scopes the query to the given branch.
* Sets {@code x-cs-variant-uid} and {@code branch} request headers.
*
* @param variantUid single variant UID
* @param branch branch name to scope the request
* @return {@link Query}
*/
public Query variants(String variantUid, String branch) {
variants(variantUid);
if (branch != null && !branch.trim().isEmpty()) {
localHeader.put("branch", branch.trim());
}
return this;
}

/**
* Sets multiple variant UIDs and scopes the query to the given branch.
* Sets {@code x-cs-variant-uid} and {@code branch} request headers.
*
* @param variantUids array of variant UIDs
* @param branch branch name to scope the request
* @return {@link Query}
*/
public Query variants(String[] variantUids, String branch) {
variants(variantUids);
if (branch != null && !branch.trim().isEmpty()) {
localHeader.put("branch", branch.trim());
}
return this;
}

public ArrayMap<String, Object> getHeaders() {
return localHeader;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,50 @@ public void testVariantsArrayNull() {
assertNotNull(result);
}

@Test
public void testVariantsSingleWithBranch() {
Entry result = entry.variants("xyz", "branch_name");
assertNotNull(result);
assertEquals("xyz", entry.getHeaders().get("x-cs-variant-uid"));
assertEquals("branch_name", entry.getHeaders().get("branch"));
}

@Test
public void testVariantsArrayWithBranch() {
Entry result = entry.variants(new String[]{"variant1", "variant2"}, "branch_name");
assertNotNull(result);
assertEquals("variant1, variant2", entry.getHeaders().get("x-cs-variant-uid"));
assertEquals("branch_name", entry.getHeaders().get("branch"));
}

@Test
public void testVariantsArrayWithBranchFiltersNullsAndBlanks() {
entry.variants(new String[]{"v1", null, " ", "v2"}, "staging");
assertEquals("v1, v2", entry.getHeaders().get("x-cs-variant-uid"));
assertEquals("staging", entry.getHeaders().get("branch"));
}

@Test
public void testVariantsSingleNullBranchDoesNotSetBranchHeader() {
entry.variants("xyz", null);
assertEquals("xyz", entry.getHeaders().get("x-cs-variant-uid"));
assertNull(entry.getHeaders().get("branch"));
}

@Test
public void testVariantsArrayNullBranchDoesNotSetBranchHeader() {
entry.variants(new String[]{"v1", "v2"}, null);
assertEquals("v1, v2", entry.getHeaders().get("x-cs-variant-uid"));
assertNull(entry.getHeaders().get("branch"));
}

@Test
public void testVariantsSingleEmptyBranchDoesNotSetBranchHeader() {
entry.variants("xyz", " ");
assertEquals("xyz", entry.getHeaders().get("x-cs-variant-uid"));
assertNull(entry.getHeaders().get("branch"));
}

// ==================== Locale ====================

@Test
Expand Down
Loading
Loading