Skip to content

Commit 9163a5d

Browse files
authored
Merge pull request #240 from contentstack/feat/DX-7266-variant-branch-support
feat: entry variants branch support
2 parents 5f6b898 + 12bbaea commit 9163a5d

16 files changed

Lines changed: 831 additions & 7 deletions

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.13.0
4+
5+
### Jul 27, 2026
6+
7+
- Feature: Branch support in entry variants
8+
39
## v1.12.2
410

511
### Jul 06, 2026

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<artifactId>cms</artifactId>
88
<packaging>jar</packaging>
99
<name>contentstack-management-java</name>
10-
<version>1.12.2</version>
10+
<version>1.13.0</version>
1111
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
1212
API-first approach
1313
</description>
@@ -254,7 +254,7 @@
254254
</includes>
255255
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
256256
<!-- Skip during default lifecycle (e.g. publish); run tests locally with: mvn test -DskipTests=false -->
257-
<!-- <skipTests>true</skipTests> -->
257+
<skipTests>true</skipTests>
258258
<testFailureIgnore>true</testFailureIgnore>
259259
</configuration>
260260
</plugin>

src/main/java/com/contentstack/cms/Contentstack.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,16 +443,20 @@ public Stack stack(@NotNull String key) {
443443
* property). Within a stack, you can create content structures, content
444444
* entries, users, etc. related to the project
445445
* <p>
446+
* Passing {@code branch} sets the {@value com.contentstack.cms.core.Util#BRANCH} request header for this stack.
447+
* That header applies to entries and entry-variant operations ({@code …/variants/…}) unless overridden per instance,
448+
* e.g. {@link com.contentstack.cms.stack.Entry#addBranch(String)} replaces {@code branch} for that entry only.
449+
* <p>
446450
* <b> Example </b>
447451
*
448452
* <pre>
449453
* Contentstack client = new Contentstack.Builder().build();
450-
* Stack org = client.stack();
454+
* Stack stack = client.stack("API_KEY", "MANAGEMENT_TOKEN", "feature-branch");
451455
* </pre>
452456
*
453457
* @param managementToken the authorization for the stack
454458
* @param apiKey the apiKey for the stack
455-
* @param branch the branch that include branching in the response
459+
* @param branch branch UID or alias for the {@value com.contentstack.cms.core.Util#BRANCH} header
456460
* @return the stack instance
457461
*/
458462
public Stack stack(@NotNull String apiKey, @NotNull String managementToken, @NotNull String branch) {

src/main/java/com/contentstack/cms/core/ErrorMessages.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ private ErrorMessages() {
3737
public static final String RELEASE_UID_REQUIRED = "Release UID is required. Provide a valid Release UID and try again.";
3838
public static final String ROLE_UID_REQUIRED = "Role UID is required. Provide a valid Role UID and try again.";
3939
public static final String VARIANT_GROUP_UID_REQUIRED = "Variant Group UID is required. Provide a valid Variant Group UID and try again.";
40+
public static final String VARIANT_UID_REQUIRED = "Variant UID is required. Provide a valid Variant UID and try again.";
4041
public static final String WEBHOOK_UID_REQUIRED = "Webhook UID is required. Provide a valid Webhook UID and try again.";
4142
public static final String WORKFLOW_UID_REQUIRED = "Workflow UID is required. Provide a valid Workflow UID and try again.";
4243

src/main/java/com/contentstack/cms/core/Util.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ public class Util {
3939
public static final String AUTHTOKEN = "authtoken";
4040
public static final String EARLY_ACCESS_HEADER = "x-header-ea";
4141
public static final String BRANCH = "branch";
42+
43+
/**
44+
* Request header to fetch a base entry with a specific entry variant applied (personalization).
45+
*/
46+
public static final String X_CS_VARIANT_UID = "x-cs-variant-uid";
47+
48+
/**
49+
* Required on publish/unpublish when the request body includes {@code entry.variants} (entry variant flows).
50+
*/
51+
public static final String API_VERSION = "api_version";
52+
53+
/** Value for {@link #API_VERSION} when publishing or unpublishing entry variants per Content Management API. */
54+
public static final String API_VERSION_ENTRY_VARIANTS_PUBLISH = "3.2";
55+
4256
public static final String X_USER_AGENT = "X-User-Agent";
4357
public static final String USER_AGENT = "User-Agent";
4458
public static final String CONTENT_TYPE = "Content-Type";

src/main/java/com/contentstack/cms/stack/Entry.java

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.contentstack.cms.stack;
22

33
import com.contentstack.cms.core.ErrorMessages;
4+
import com.contentstack.cms.core.Util;
45

56
import com.contentstack.cms.BaseImplementation;
67
import okhttp3.ResponseBody;
78
import org.jetbrains.annotations.NotNull;
9+
import org.jetbrains.annotations.Nullable;
810
import org.json.simple.JSONObject;
911
import retrofit2.Call;
1012
import retrofit2.Retrofit;
@@ -68,6 +70,56 @@ private void validateCT() {
6870
Objects.requireNonNull(this.contentTypeUid, ERROR_CT_UID);
6971
}
7072

73+
private void validateVariantUid(@NotNull String variantUid) {
74+
Objects.requireNonNull(variantUid, ErrorMessages.VARIANT_UID_REQUIRED);
75+
if (variantUid.trim().isEmpty()) {
76+
throw new IllegalArgumentException(ErrorMessages.VARIANT_UID_REQUIRED);
77+
}
78+
}
79+
80+
/**
81+
* Header map for a variant request when {@code branchUid} is supplied for this call only (does not mutate {@link #headers}).
82+
* Null or blank {@code branchUid} keeps {@link #headers} as-is (stack / {@link #addBranch(String)} behavior).
83+
*/
84+
private Map<String, Object> variantHeadersWithOptionalBranch(@Nullable String branchUid) {
85+
if (branchUid == null || branchUid.trim().isEmpty()) {
86+
return this.headers;
87+
}
88+
HashMap<String, Object> copy = new HashMap<>(this.headers);
89+
copy.put(Util.BRANCH, branchUid);
90+
return copy;
91+
}
92+
93+
/**
94+
* Sets the branch header for requests scoped to a stack branch (e.g. development).
95+
* Overrides the branch set via {@link com.contentstack.cms.Contentstack#stack(String, String, String)} for this
96+
* {@link Entry} instance only (including entry-variant CRUD, publish, and unpublish). Uses header key {@value Util#BRANCH}.
97+
*
98+
* @param branchUid branch UID or alias target branch UID
99+
* @return this entry instance for chaining
100+
*/
101+
public Entry addBranch(@NotNull String branchUid) {
102+
Objects.requireNonNull(branchUid, ErrorMessages.BRANCH_UID_REQUIRED);
103+
if (branchUid.trim().isEmpty()) {
104+
throw new IllegalArgumentException(ErrorMessages.BRANCH_UID_REQUIRED);
105+
}
106+
this.headers.put(Util.BRANCH, branchUid);
107+
return this;
108+
}
109+
110+
/**
111+
* Sets {@value Util#X_CS_VARIANT_UID} for {@link #fetch()} / {@link #fetchAsPojo()} to retrieve the base entry with a
112+
* specific variant applied (personalization).
113+
*
114+
* @param variantUid Content variant UID (e.g. {@code cs…})
115+
* @return this entry instance for chaining
116+
*/
117+
public Entry withAppliedVariantUid(@NotNull String variantUid) {
118+
validateVariantUid(variantUid);
119+
this.headers.put(Util.X_CS_VARIANT_UID, variantUid);
120+
return this;
121+
}
122+
71123
/**
72124
* Sets header for the request
73125
*
@@ -710,6 +762,102 @@ public Call<ResponseBody> importExisting() {
710762
return this.service.importExisting(this.headers, this.contentTypeUid, this.entryUid, this.params);
711763
}
712764

765+
/**
766+
* Retrieves all entry variants for this entry.
767+
* <p>
768+
* Use {@link #addParam(String, Object)} for optional queries such as {@code locale}, {@code include_workflow}.
769+
* Branch scope: stack {@value Util#BRANCH} from {@link com.contentstack.cms.Contentstack#stack(String, String, String)}
770+
* is forwarded; {@link #addBranch(String)} overrides for this entry only.
771+
*
772+
* @return Retrofit call for GET …/entries/{entry_uid}/variants
773+
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#get-all-entry-variants">Get all entry variants</a>
774+
*/
775+
public Call<ResponseBody> fetchEntryVariants() {
776+
validateCT();
777+
validateEntry();
778+
return this.service.fetchEntryVariants(this.headers, this.contentTypeUid, this.entryUid, this.params);
779+
}
780+
781+
/**
782+
* Retrieves a single entry variant using {@link #headers} for {@value Util#BRANCH} (stack default and/or {@link #addBranch(String)}).
783+
*
784+
* @param variantUid variant UID path segment
785+
* @return Retrofit call for GET …/variants/{variant_uid}
786+
* @see #fetchEntryVariant(String, String)
787+
*/
788+
public Call<ResponseBody> fetchEntryVariant(@NotNull String variantUid) {
789+
return fetchEntryVariant(variantUid, null);
790+
}
791+
792+
/**
793+
* Retrieves a single entry variant with an optional per-call {@value Util#BRANCH} override.
794+
* <p>
795+
* When {@code branchUid} is non-blank, it replaces {@value Util#BRANCH} on this request only (stack and {@link #addBranch(String)}
796+
* values are not mutated on the entry). When {@code branchUid} is {@code null} or blank, behavior matches {@link #fetchEntryVariant(String)}.
797+
* {@link #withAppliedVariantUid(String)} ({@value Util#X_CS_VARIANT_UID}) is unrelated to branch.
798+
*
799+
* @param variantUid variant UID path segment
800+
* @param branchUid optional branch UID or alias for this request only; {@code null} or empty to use entry headers
801+
* @return Retrofit call for GET …/variants/{variant_uid}
802+
*/
803+
public Call<ResponseBody> fetchEntryVariant(@NotNull String variantUid, @Nullable String branchUid) {
804+
validateCT();
805+
validateEntry();
806+
validateVariantUid(variantUid);
807+
return this.service.fetchEntryVariant(variantHeadersWithOptionalBranch(branchUid), this.contentTypeUid,
808+
this.entryUid, variantUid, this.params);
809+
}
810+
811+
/**
812+
* Creates an entry variant. Uses PUT …/variants/{variant_uid} (CMA upsert — same URL as {@link #updateEntryVariant}).
813+
* <p>
814+
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
815+
* ({@value Util#BRANCH}) on this entry. Variant personalization header {@value Util#X_CS_VARIANT_UID} is orthogonal.
816+
*
817+
* @param variantUid variant UID path segment
818+
* @param requestBody JSON body per API (typically wraps fields under {@code entry})
819+
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#create-entry-variant">Create Entry Variant</a>
820+
*/
821+
public Call<ResponseBody> createEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
822+
validateCT();
823+
validateEntry();
824+
validateVariantUid(variantUid);
825+
return this.service.createEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
826+
requestBody);
827+
}
828+
829+
/**
830+
* Updates an entry variant. Same HTTP request shape as create (PUT upsert).
831+
* <p>
832+
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
833+
* ({@value Util#BRANCH}) on this entry.
834+
*
835+
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#update-entry-variant">Update Entry Variant</a>
836+
*/
837+
public Call<ResponseBody> updateEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
838+
validateCT();
839+
validateEntry();
840+
validateVariantUid(variantUid);
841+
return this.service.updateEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
842+
requestBody);
843+
}
844+
845+
/**
846+
* Deletes an entry variant.
847+
* <p>
848+
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
849+
* ({@value Util#BRANCH}) on this entry.
850+
*
851+
* @param variantUid variant UID path segment
852+
* @return Retrofit call for DELETE …/variants/{variant_uid}
853+
*/
854+
public Call<ResponseBody> deleteEntryVariant(@NotNull String variantUid) {
855+
validateCT();
856+
validateEntry();
857+
validateVariantUid(variantUid);
858+
return this.service.deleteEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params);
859+
}
860+
713861
/**
714862
* To Publish an entry request lets you publish an entry either immediately or
715863
* schedule it for a later date/time.
@@ -752,7 +900,25 @@ public Call<ResponseBody> importExisting() {
752900
public Call<ResponseBody> publish(@NotNull JSONObject requestBody) {
753901
validateCT();
754902
validateEntry();
755-
return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
903+
return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
904+
}
905+
906+
/**
907+
* Publishes entry variants using the entry publish endpoint with {@code entry.variants} in the body.
908+
* Sends header {@value Util#API_VERSION}={@value Util#API_VERSION_ENTRY_VARIANTS_PUBLISH} unless already set on this entry instance.
909+
* Use {@link #addParam(String, Object)} for optional {@code locale} query parameter.
910+
* <p>
911+
* Branch scope: stack {@value Util#BRANCH} is copied into the publish request headers together with {@code api_version};
912+
* override with {@link #addBranch(String)} or {@link #addHeader(String, String)} ({@value Util#BRANCH}) on this entry.
913+
*
914+
* @param requestBody full publish payload including {@code entry}, {@code locale}, etc.
915+
*/
916+
public Call<ResponseBody> publishEntryVariants(@NotNull JSONObject requestBody) {
917+
validateCT();
918+
validateEntry();
919+
HashMap<String, Object> publishHeaders = new HashMap<>(this.headers);
920+
publishHeaders.putIfAbsent(Util.API_VERSION, Util.API_VERSION_ENTRY_VARIANTS_PUBLISH);
921+
return this.service.publish(publishHeaders, this.contentTypeUid, this.entryUid, this.params, requestBody);
756922
}
757923

758924
/**
@@ -816,9 +982,23 @@ public Call<ResponseBody> publishWithReference(@NotNull JSONObject requestBody)
816982
public Call<ResponseBody> unpublish(@NotNull JSONObject requestBody) {
817983
validateCT();
818984
validateEntry();
819-
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
985+
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
820986
}
821987

988+
/**
989+
* Unpublishes entry variants via the entry unpublish endpoint with {@code entry.variants} in the body.
990+
* Sends header {@value Util#API_VERSION}={@value Util#API_VERSION_ENTRY_VARIANTS_PUBLISH} unless already set.
991+
* <p>
992+
* Branch scope: stack {@value Util#BRANCH} is forwarded; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
993+
* ({@value Util#BRANCH}) on this entry.
994+
*/
995+
public Call<ResponseBody> unpublishEntryVariants(@NotNull JSONObject requestBody) {
996+
validateCT();
997+
validateEntry();
998+
HashMap<String, Object> unpublishHeaders = new HashMap<>(this.headers);
999+
unpublishHeaders.putIfAbsent(Util.API_VERSION, Util.API_VERSION_ENTRY_VARIANTS_PUBLISH);
1000+
return this.service.unpublish(unpublishHeaders, this.contentTypeUid, this.entryUid, this.params, requestBody);
1001+
}
8221002

8231003
/**
8241004
* Get instance of taxonomy search filter class instance through which we can query on taxonomy based on content type

src/main/java/com/contentstack/cms/stack/EntryService.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import retrofit2.Call;
66
import retrofit2.http.*;
77

8-
import java.util.List;
98
import java.util.Map;
109

1110
public interface EntryService {
@@ -147,6 +146,7 @@ Call<ResponseBody> publish(
147146
@HeaderMap Map<String, Object> headers,
148147
@Path("content_type_uid") String contentTypeUid,
149148
@Path("entry_uid") String entryUid,
149+
@QueryMap(encoded = true) Map<String, Object> queryParameters,
150150
@Body JSONObject requestBody);
151151

152152
@POST("bulk/publish?x-bulk-action=publish")
@@ -160,8 +160,58 @@ Call<ResponseBody> unpublish(
160160
@HeaderMap Map<String, Object> headers,
161161
@Path("content_type_uid") String contentTypeUid,
162162
@Path("entry_uid") String entryUid,
163+
@QueryMap(encoded = true) Map<String, Object> queryParameters,
163164
@Body JSONObject requestBody);
164165

166+
@GET("content_types/{content_type_uid}/entries/{entry_uid}/variants")
167+
Call<ResponseBody> fetchEntryVariants(
168+
@HeaderMap Map<String, Object> headers,
169+
@Path("content_type_uid") String contentTypeUid,
170+
@Path("entry_uid") String entryUid,
171+
@QueryMap(encoded = true) Map<String, Object> queryParameters);
172+
173+
@GET("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
174+
Call<ResponseBody> fetchEntryVariant(
175+
@HeaderMap Map<String, Object> headers,
176+
@Path("content_type_uid") String contentTypeUid,
177+
@Path("entry_uid") String entryUid,
178+
@Path("variant_uid") String variantUid,
179+
@QueryMap(encoded = true) Map<String, Object> queryParameters);
180+
181+
/**
182+
* Create entry variant (PUT …/variants/{variant_uid}). Same HTTP contract as update — CMA upserts on this path.
183+
*/
184+
@Headers("Content-Type: application/json")
185+
@PUT("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
186+
Call<ResponseBody> createEntryVariant(
187+
@HeaderMap Map<String, Object> headers,
188+
@Path("content_type_uid") String contentTypeUid,
189+
@Path("entry_uid") String entryUid,
190+
@Path("variant_uid") String variantUid,
191+
@QueryMap(encoded = true) Map<String, Object> queryParameters,
192+
@Body JSONObject requestBody);
193+
194+
/**
195+
* Update entry variant — delegates to {@link #createEntryVariant}; API uses one PUT upsert for both operations.
196+
*/
197+
default Call<ResponseBody> updateEntryVariant(
198+
Map<String, Object> headers,
199+
String contentTypeUid,
200+
String entryUid,
201+
String variantUid,
202+
Map<String, Object> queryParameters,
203+
JSONObject requestBody) {
204+
return createEntryVariant(headers, contentTypeUid, entryUid, variantUid, queryParameters, requestBody);
205+
}
206+
207+
@DELETE("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
208+
Call<ResponseBody> deleteEntryVariant(
209+
@HeaderMap Map<String, Object> headers,
210+
@Path("content_type_uid") String contentTypeUid,
211+
@Path("entry_uid") String entryUid,
212+
@Path("variant_uid") String variantUid,
213+
@QueryMap(encoded = true) Map<String, Object> queryParameters);
214+
165215
@GET("content_types/{content_type_uid}/entries")
166216
Call<ResponseBody> filterTaxonomy(
167217
@HeaderMap Map<String, Object> headers,

0 commit comments

Comments
 (0)