Skip to content

Commit ccc3a51

Browse files
Merge remote-tracking branch 'origin/feat/DX-7266-variant-branch-support' into fix/environment-tests-and-security-cleanup
# Conflicts: # src/test/java/com/contentstack/cms/TestClient.java # src/test/java/com/contentstack/cms/UnitTestSuite.java # src/test/java/com/contentstack/cms/stack/APISanityTestSuite.java
2 parents aab2483 + 0f793e6 commit ccc3a51

15 files changed

Lines changed: 803 additions & 6 deletions

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 178 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,52 @@ 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.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.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+
this.headers.put(Util.BRANCH, branchUid);
103+
return this;
104+
}
105+
106+
/**
107+
* Sets {@value Util#X_CS_VARIANT_UID} for {@link #fetch()} / {@link #fetchAsPojo()} to retrieve the base entry with a
108+
* specific variant applied (personalization).
109+
*
110+
* @param variantUid Content variant UID (e.g. {@code cs…})
111+
* @return this entry instance for chaining
112+
*/
113+
public Entry withAppliedVariantUid(@NotNull String variantUid) {
114+
validateVariantUid(variantUid);
115+
this.headers.put(Util.X_CS_VARIANT_UID, variantUid);
116+
return this;
117+
}
118+
71119
/**
72120
* Sets header for the request
73121
*
@@ -710,6 +758,102 @@ public Call<ResponseBody> importExisting() {
710758
return this.service.importExisting(this.headers, this.contentTypeUid, this.entryUid, this.params);
711759
}
712760

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

758920
/**
@@ -816,9 +978,23 @@ public Call<ResponseBody> publishWithReference(@NotNull JSONObject requestBody)
816978
public Call<ResponseBody> unpublish(@NotNull JSONObject requestBody) {
817979
validateCT();
818980
validateEntry();
819-
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
981+
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
820982
}
821983

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

823999
/**
8241000
* 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,

src/test/java/com/contentstack/cms/TestClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public class TestClient {
2727
public final static String REGION = getEnvValue("REGION", "region", "na");
2828
public final static String VARIANT_GROUP_UID = getEnvValue("VARIANT_GROUP_UID", "variantGroupUid", "variantGroupUid99999999");
2929

30+
public static final String ENTRY_VARIANT_CONTENT_TYPE_UID = getEnvValue("ENTRY_VARIANT_CONTENT_TYPE_UID", "entryVariantContentTypeUid", "blog");
31+
public static final String ENTRY_VARIANT_BRANCH = getEnvValue("ENTRY_VARIANT_BRANCH", "entryVariantBranch", "develop");
32+
public static final String ENTRY_VARIANT_LOCALE = getEnvValue("ENTRY_VARIANT_LOCALE", "entryVariantLocale", "en-us");
33+
3034
// Credentials for normal login (without 2FA)
3135
public final static String EMAIL = getEnvValue("EMAIL", "email", null);
3236
public final static String PASSWORD = getEnvValue("PASSWORD", "password", null);
@@ -44,6 +48,10 @@ public class TestClient {
4448
private static final String ENV_AUTHTOKEN = getEnvValue("AUTHTOKEN", "authToken", null);
4549

4650
public static String AUTHTOKEN = getAuthToken();
51+
52+
public static boolean isUsingDefaultStackCredentials() {
53+
return env.get("apiKey") == null || env.get("managementToken") == null;
54+
}
4755
private static Contentstack instance;
4856
private static Stack stackInstance;
4957

src/test/java/com/contentstack/cms/UnitTestSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.contentstack.cms.core.AuthInterceptorTest;
44
import com.contentstack.cms.core.EndpointTest;
55
import com.contentstack.cms.stack.AssetUnitTest;
6+
import com.contentstack.cms.stack.EntryVariantUnitTest;
67
import com.contentstack.cms.stack.EnvironmentUnitTest;
78
import com.contentstack.cms.stack.GlobalFieldUnitTests;
89
import com.contentstack.cms.stack.LocaleUnitTest;
@@ -29,6 +30,7 @@
2930

3031
// Stack module tests (only public classes)
3132
AssetUnitTest.class,
33+
EntryVariantUnitTest.class,
3234
EnvironmentUnitTest.class,
3335
GlobalFieldUnitTests.class,
3436
LocaleUnitTest.class,

0 commit comments

Comments
 (0)