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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
</includes>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<!-- Skip during default lifecycle (e.g. publish); run tests locally with: mvn test -DskipTests=false -->
<!-- <skipTests>true</skipTests> -->
<skipTests>true</skipTests>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/contentstack/cms/Contentstack.java
Original file line number Diff line number Diff line change
Expand Up @@ -439,16 +439,20 @@ public Stack stack(@NotNull String key) {
* property). Within a stack, you can create content structures, content
* entries, users, etc. related to the project
* <p>
* Passing {@code branch} sets the {@value com.contentstack.cms.core.Util#BRANCH} request header for this stack.
* That header applies to entries and entry-variant operations ({@code …/variants/…}) unless overridden per instance,
* e.g. {@link com.contentstack.cms.stack.Entry#addBranch(String)} replaces {@code branch} for that entry only.
* <p>
* <b> Example </b>
*
* <pre>
* Contentstack client = new Contentstack.Builder().build();
* Stack org = client.stack();
* Stack stack = client.stack("API_KEY", "MANAGEMENT_TOKEN", "feature-branch");
* </pre>
*
* @param managementToken the authorization for the stack
* @param apiKey the apiKey for the stack
* @param branch the branch that include branching in the response
* @param branch branch UID or alias for the {@value com.contentstack.cms.core.Util#BRANCH} header
* @return the stack instance
*/
public Stack stack(@NotNull String apiKey, @NotNull String managementToken, @NotNull String branch) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ private ErrorMessages() {
public static final String RELEASE_UID_REQUIRED = "Release UID is required. Provide a valid Release UID and try again.";
public static final String ROLE_UID_REQUIRED = "Role UID is required. Provide a valid Role UID and try again.";
public static final String VARIANT_GROUP_UID_REQUIRED = "Variant Group UID is required. Provide a valid Variant Group UID and try again.";
public static final String VARIANT_UID_REQUIRED = "Variant UID is required. Provide a valid Variant UID and try again.";
public static final String WEBHOOK_UID_REQUIRED = "Webhook UID is required. Provide a valid Webhook UID and try again.";
public static final String WORKFLOW_UID_REQUIRED = "Workflow UID is required. Provide a valid Workflow UID and try again.";

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/contentstack/cms/core/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ public class Util {
public static final String AUTHTOKEN = "authtoken";
public static final String EARLY_ACCESS_HEADER = "x-header-ea";
public static final String BRANCH = "branch";

/**
* Request header to fetch a base entry with a specific entry variant applied (personalization).
*/
public static final String X_CS_VARIANT_UID = "x-cs-variant-uid";

/**
* Required on publish/unpublish when the request body includes {@code entry.variants} (entry variant flows).
*/
public static final String API_VERSION = "api_version";

/** Value for {@link #API_VERSION} when publishing or unpublishing entry variants per Content Management API. */
public static final String API_VERSION_ENTRY_VARIANTS_PUBLISH = "3.2";

public static final String X_USER_AGENT = "X-User-Agent";
public static final String USER_AGENT = "User-Agent";
public static final String CONTENT_TYPE = "Content-Type";
Expand Down
180 changes: 178 additions & 2 deletions src/main/java/com/contentstack/cms/stack/Entry.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.contentstack.cms.stack;

import com.contentstack.cms.core.ErrorMessages;
import com.contentstack.cms.core.Util;

import com.contentstack.cms.BaseImplementation;
import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.simple.JSONObject;
import retrofit2.Call;
import retrofit2.Retrofit;
Expand Down Expand Up @@ -68,6 +70,52 @@ private void validateCT() {
Objects.requireNonNull(this.contentTypeUid, ERROR_CT_UID);
}

private void validateVariantUid(@NotNull String variantUid) {
Objects.requireNonNull(variantUid, ErrorMessages.VARIANT_UID_REQUIRED);
if (variantUid.isEmpty()) {
throw new IllegalArgumentException(ErrorMessages.VARIANT_UID_REQUIRED);
}
}

/**
* Header map for a variant request when {@code branchUid} is supplied for this call only (does not mutate {@link #headers}).
* Null or blank {@code branchUid} keeps {@link #headers} as-is (stack / {@link #addBranch(String)} behavior).
*/
private Map<String, Object> variantHeadersWithOptionalBranch(@Nullable String branchUid) {
if (branchUid == null || branchUid.isEmpty()) {
return this.headers;
}
HashMap<String, Object> copy = new HashMap<>(this.headers);
copy.put(Util.BRANCH, branchUid);
return copy;
}

/**
* Sets the branch header for requests scoped to a stack branch (e.g. development).
* Overrides the branch set via {@link com.contentstack.cms.Contentstack#stack(String, String, String)} for this
* {@link Entry} instance only (including entry-variant CRUD, publish, and unpublish). Uses header key {@value Util#BRANCH}.
*
* @param branchUid branch UID or alias target branch UID
* @return this entry instance for chaining
*/
public Entry addBranch(@NotNull String branchUid) {
this.headers.put(Util.BRANCH, branchUid);
return this;
}

/**
* Sets {@value Util#X_CS_VARIANT_UID} for {@link #fetch()} / {@link #fetchAsPojo()} to retrieve the base entry with a
* specific variant applied (personalization).
*
* @param variantUid Content variant UID (e.g. {@code cs…})
* @return this entry instance for chaining
*/
public Entry withAppliedVariantUid(@NotNull String variantUid) {
validateVariantUid(variantUid);
this.headers.put(Util.X_CS_VARIANT_UID, variantUid);
return this;
}

/**
* Sets header for the request
*
Expand Down Expand Up @@ -710,6 +758,102 @@ public Call<ResponseBody> importExisting() {
return this.service.importExisting(this.headers, this.contentTypeUid, this.entryUid, this.params);
}

/**
* Retrieves all entry variants for this entry.
* <p>
* Use {@link #addParam(String, Object)} for optional queries such as {@code locale}, {@code include_workflow}.
* Branch scope: stack {@value Util#BRANCH} from {@link com.contentstack.cms.Contentstack#stack(String, String, String)}
* is forwarded; {@link #addBranch(String)} overrides for this entry only.
*
* @return Retrofit call for GET …/entries/{entry_uid}/variants
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#get-all-entry-variants">Get all entry variants</a>
*/
public Call<ResponseBody> fetchEntryVariants() {
validateCT();
validateEntry();
return this.service.fetchEntryVariants(this.headers, this.contentTypeUid, this.entryUid, this.params);
}

/**
* Retrieves a single entry variant using {@link #headers} for {@value Util#BRANCH} (stack default and/or {@link #addBranch(String)}).
*
* @param variantUid variant UID path segment
* @return Retrofit call for GET …/variants/{variant_uid}
* @see #fetchEntryVariant(String, String)
*/
public Call<ResponseBody> fetchEntryVariant(@NotNull String variantUid) {
return fetchEntryVariant(variantUid, null);
}

/**
* Retrieves a single entry variant with an optional per-call {@value Util#BRANCH} override.
* <p>
* When {@code branchUid} is non-blank, it replaces {@value Util#BRANCH} on this request only (stack and {@link #addBranch(String)}
* values are not mutated on the entry). When {@code branchUid} is {@code null} or blank, behavior matches {@link #fetchEntryVariant(String)}.
* {@link #withAppliedVariantUid(String)} ({@value Util#X_CS_VARIANT_UID}) is unrelated to branch.
*
* @param variantUid variant UID path segment
* @param branchUid optional branch UID or alias for this request only; {@code null} or empty to use entry headers
* @return Retrofit call for GET …/variants/{variant_uid}
*/
public Call<ResponseBody> fetchEntryVariant(@NotNull String variantUid, @Nullable String branchUid) {
validateCT();
validateEntry();
validateVariantUid(variantUid);
return this.service.fetchEntryVariant(variantHeadersWithOptionalBranch(branchUid), this.contentTypeUid,
this.entryUid, variantUid, this.params);
}

/**
* Creates an entry variant. Uses PUT …/variants/{variant_uid} (CMA upsert — same URL as {@link #updateEntryVariant}).
* <p>
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
* ({@value Util#BRANCH}) on this entry. Variant personalization header {@value Util#X_CS_VARIANT_UID} is orthogonal.
*
* @param variantUid variant UID path segment
* @param requestBody JSON body per API (typically wraps fields under {@code entry})
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#create-entry-variant">Create Entry Variant</a>
*/
public Call<ResponseBody> createEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
validateCT();
validateEntry();
validateVariantUid(variantUid);
return this.service.createEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
requestBody);
}

/**
* Updates an entry variant. Same HTTP request shape as create (PUT upsert).
* <p>
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
* ({@value Util#BRANCH}) on this entry.
*
* @see <a href="https://www.contentstack.com/docs/developers/apis/content-management-api/#update-entry-variant">Update Entry Variant</a>
*/
public Call<ResponseBody> updateEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
validateCT();
validateEntry();
validateVariantUid(variantUid);
return this.service.updateEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
requestBody);
}

/**
* Deletes an entry variant.
* <p>
* Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
* ({@value Util#BRANCH}) on this entry.
*
* @param variantUid variant UID path segment
* @return Retrofit call for DELETE …/variants/{variant_uid}
*/
public Call<ResponseBody> deleteEntryVariant(@NotNull String variantUid) {
validateCT();
validateEntry();
validateVariantUid(variantUid);
return this.service.deleteEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params);
}

/**
* To Publish an entry request lets you publish an entry either immediately or
* schedule it for a later date/time.
Expand Down Expand Up @@ -752,7 +896,25 @@ public Call<ResponseBody> importExisting() {
public Call<ResponseBody> publish(@NotNull JSONObject requestBody) {
validateCT();
validateEntry();
return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
}

/**
* Publishes entry variants using the entry publish endpoint with {@code entry.variants} in the body.
* Sends header {@value Util#API_VERSION}={@value Util#API_VERSION_ENTRY_VARIANTS_PUBLISH} unless already set on this entry instance.
* Use {@link #addParam(String, Object)} for optional {@code locale} query parameter.
* <p>
* Branch scope: stack {@value Util#BRANCH} is copied into the publish request headers together with {@code api_version};
* override with {@link #addBranch(String)} or {@link #addHeader(String, String)} ({@value Util#BRANCH}) on this entry.
*
* @param requestBody full publish payload including {@code entry}, {@code locale}, etc.
*/
public Call<ResponseBody> publishEntryVariants(@NotNull JSONObject requestBody) {
validateCT();
validateEntry();
HashMap<String, Object> publishHeaders = new HashMap<>(this.headers);
publishHeaders.putIfAbsent(Util.API_VERSION, Util.API_VERSION_ENTRY_VARIANTS_PUBLISH);
return this.service.publish(publishHeaders, this.contentTypeUid, this.entryUid, this.params, requestBody);
}

/**
Expand Down Expand Up @@ -816,9 +978,23 @@ public Call<ResponseBody> publishWithReference(@NotNull JSONObject requestBody)
public Call<ResponseBody> unpublish(@NotNull JSONObject requestBody) {
validateCT();
validateEntry();
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
}

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

/**
* Get instance of taxonomy search filter class instance through which we can query on taxonomy based on content type
Expand Down
52 changes: 51 additions & 1 deletion src/main/java/com/contentstack/cms/stack/EntryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import retrofit2.Call;
import retrofit2.http.*;

import java.util.List;
import java.util.Map;

public interface EntryService {
Expand Down Expand Up @@ -147,6 +146,7 @@ Call<ResponseBody> publish(
@HeaderMap Map<String, Object> headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
@QueryMap(encoded = true) Map<String, Object> queryParameters,
@Body JSONObject requestBody);

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

@GET("content_types/{content_type_uid}/entries/{entry_uid}/variants")
Call<ResponseBody> fetchEntryVariants(
@HeaderMap Map<String, Object> headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
@QueryMap(encoded = true) Map<String, Object> queryParameters);

@GET("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
Call<ResponseBody> fetchEntryVariant(
@HeaderMap Map<String, Object> headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
@Path("variant_uid") String variantUid,
@QueryMap(encoded = true) Map<String, Object> queryParameters);

/**
* Create entry variant (PUT …/variants/{variant_uid}). Same HTTP contract as update — CMA upserts on this path.
*/
@Headers("Content-Type: application/json")
@PUT("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
Call<ResponseBody> createEntryVariant(
@HeaderMap Map<String, Object> headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
@Path("variant_uid") String variantUid,
@QueryMap(encoded = true) Map<String, Object> queryParameters,
@Body JSONObject requestBody);

/**
* Update entry variant — delegates to {@link #createEntryVariant}; API uses one PUT upsert for both operations.
*/
default Call<ResponseBody> updateEntryVariant(
Map<String, Object> headers,
String contentTypeUid,
String entryUid,
String variantUid,
Map<String, Object> queryParameters,
JSONObject requestBody) {
return createEntryVariant(headers, contentTypeUid, entryUid, variantUid, queryParameters, requestBody);
}

@DELETE("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
Call<ResponseBody> deleteEntryVariant(
@HeaderMap Map<String, Object> headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
@Path("variant_uid") String variantUid,
@QueryMap(encoded = true) Map<String, Object> queryParameters);

@GET("content_types/{content_type_uid}/entries")
Call<ResponseBody> filterTaxonomy(
@HeaderMap Map<String, Object> headers,
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/com/contentstack/cms/TestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ public class TestClient {
public final static String DEV_HOST = (env.get("dev_host") != null) ? env.get("dev_host").trim() : "api.contentstack.io";
public final static String VARIANT_GROUP_UID = (env.get("variantGroupUid") != null) ? env.get("variantGroupUid")
: "variantGroupUid99999999";

/** Content type UID for entry-variant tests (default {@code blog}). */
public static final String ENTRY_VARIANT_CONTENT_TYPE_UID =
env.get("entryVariantContentTypeUid") != null ? env.get("entryVariantContentTypeUid") : "blog";

/** Stack branch for entry-variant tests (default {@code develop}). */
public static final String ENTRY_VARIANT_BRANCH =
env.get("entryVariantBranch") != null ? env.get("entryVariantBranch") : "develop";

/** Locale query param for entry-variant tests (default {@code en-us}). */
public static final String ENTRY_VARIANT_LOCALE =
env.get("entryVariantLocale") != null ? env.get("entryVariantLocale") : "en-us";

/**
* {@code true} when {@code apiKey} / {@code managementToken} were not loaded from {@code .env} (defaults apply).
*/
public static boolean isUsingDefaultStackCredentials() {
return env.get("apiKey") == null || env.get("managementToken") == null;
}

private static Contentstack instance;
private static Stack stackInstance;

Expand Down
2 changes: 2 additions & 0 deletions src/test/java/com/contentstack/cms/UnitTestSuite.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.contentstack.cms;

import com.contentstack.cms.core.AuthInterceptorTest;
import com.contentstack.cms.stack.EntryVariantUnitTest;
import com.contentstack.cms.stack.EnvironmentUnitTest;
import com.contentstack.cms.stack.GlobalFieldUnitTests;
import com.contentstack.cms.stack.LocaleUnitTest;
Expand All @@ -25,6 +26,7 @@
ContentstackUnitTest.class,

// Stack module tests (only public classes)
EntryVariantUnitTest.class,
EnvironmentUnitTest.class,
GlobalFieldUnitTests.class,
LocaleUnitTest.class,
Expand Down
Loading
Loading