Skip to content
Merged
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
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v1.9.0

### Oct 13, 2025

- Feature : Variant Group support

## v1.8.0

### Sep 15, 2025
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>cms</artifactId>
<packaging>jar</packaging>
<name>contentstack-management-java</name>
<version>1.8.0</version>
<version>1.9.0</version>
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
</description>
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/contentstack/cms/stack/Stack.java
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,29 @@ public GlobalField globalField(@NotNull String globalFiledUid) {
return new GlobalField(this.client,this.headers,globalFiledUid);
}

/**
* Creates a new instance of VariantGroup for managing variant groups.
* This method is used when you want to create a new variant group.
*
* @return A new VariantGroup instanceroup variantGroup() {
return new VariantGroup(this.client,this.headers);
}
*/
public VariantGroup variantGroup() {
return new VariantGroup(this.client,this.headers);
}

/**
* Creates a new instance of VariantGroup for managing a specific variant group.
* This method is used when you want to work with an existing variant group.
*
* @param variantGroupUid The UID of the variant group to manage
* @return A new VariantGroup instance configured for the specified variant group
*/
public VariantGroup variantGroup(@NotNull String variantGroupUid) {
return new VariantGroup(this.client,this.headers,variantGroupUid);
}

/**
* Contentstack has a sophisticated multilingual capability. It allows you to
* create and publish entries in any
Expand Down
236 changes: 236 additions & 0 deletions src/main/java/com/contentstack/cms/stack/VariantGroup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package com.contentstack.cms.stack;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jetbrains.annotations.NotNull;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.contentstack.cms.BaseImplementation;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Retrofit;

/**
* The VariantGroup class provides functionality to manage variant groups in Contentstack.
* Variant groups allow you to manage different versions of your content for various use cases,
* such as A/B testing, localization, or personalization.
*/
public class VariantGroup implements BaseImplementation<VariantGroup> {
protected final VariantsService service;
protected final Map<String, Object> headers;
protected Map<String, Object> params;
private final Retrofit instance;
private String variantGroupUid;
private List<String> branches;

/**
* Creates a new VariantGroup instance without a specific variant group UID.
* This constructor is used when creating new variant groups.
*
* @param instance The Retrofit instance for making API calls
* @param headers The headers to be included in API requests
*/
protected VariantGroup(Retrofit instance, Map<String, Object> headers) {
this.headers = new HashMap<>();
this.headers.putAll(headers);
this.params = new HashMap<>();
this.instance = instance;
this.service = instance.create(VariantsService.class);
this.branches = Arrays.asList("main"); // Default to main branch
}

/**
* Creates a new VariantGroup instance with a specific variant group UID.
* This constructor is used when working with existing variant groups.
*
* @param instance The Retrofit instance for making API calls
* @param headers The headers to be included in API requests
* @param variantGroupUid The unique identifier of the variant group
*/
protected VariantGroup(Retrofit instance, Map<String, Object> headers, String variantGroupUid) {
this.headers = new HashMap<>();
this.headers.putAll(headers);
this.params = new HashMap<>();
this.instance = instance;
this.variantGroupUid = variantGroupUid;
this.service = instance.create(VariantsService.class);
this.branches = Arrays.asList("main"); // Default to main branch
}

/**
* Validates that the variant group UID is not null or empty.
* This method is called before operations that require a valid variant group UID.
*
* @throws IllegalAccessError if the variant group UID is null or empty
*/
void validate() {
if (this.variantGroupUid == null || this.variantGroupUid.isEmpty())
throw new IllegalAccessError("Variant group uid can not be null or empty");
}

/**
* Sets the branches for the variant group using a List of branch names.
* These branches will be used when linking or unlinking content types to the variant group.
*
* @param branches A List of String values representing the branch names
* @return The current VariantGroup instance for method chaining
*/
public VariantGroup setBranches(List<String> branches) {
this.branches = branches;
return this;
}

/**
* Sets the branches for the variant group using varargs (variable number of arguments).
* This is a convenience method that allows passing branch names directly as arguments.
* These branches will be used when linking or unlinking content types to the variant group.
*
* @param branches Variable number of String arguments representing branch names
* @return The current VariantGroup instance for method chaining
*/
public VariantGroup setBranches(String... branches) {
this.branches = Arrays.asList(branches);
return this;
}

/**
* @param key A string representing the key of the parameter. It cannot be
* null and must be
* provided as a non-null value.
* @param value The "value" parameter is of type Object, which means it can
* accept any type of
* object as its value.
* @return instance of VariantGroup
*/
@Override
public VariantGroup addParam(@NotNull String key, @NotNull Object value) {
this.params.put(key, value);
return this;
}

/**
* @param key The key parameter is a string that represents the name or
* identifier of the header.
* It is used to specify the type of information being sent in the
* header.
* @param value The value parameter is a string that represents the value of the
* header.
* @return instance of VariantGroup
*/
@Override
public VariantGroup addHeader(@NotNull String key, @NotNull String value) {
this.headers.put(key, value);
return this;
}

/**
* @param headers A HashMap containing key-value pairs of headers, where the key
* is a String
* representing the header name and the value is a String
* representing the header value.
* @return instance of VariantGroup
*/
@Override
public VariantGroup addHeaders(@NotNull HashMap<String, String> headers) {
this.headers.putAll(headers);
return this;
}


/**
* @param headers The "params" parameter is a HashMap that maps String keys to
* Object values. It is
* annotated with @NotNull, indicating that it cannot be null.
* @return instance of VariantGroup
*/
@Override
public VariantGroup addParams(@NotNull HashMap<String, Object> headers) {
this.params.putAll(headers);
return this;
}


/**
* clears all params in the request
*/
protected void clearParams() {
this.params.clear();
}

/**
* Retrieves a list of all variant groups.
* This method does not require a variant group UID to be set.
*
* @return A Call object that can be executed to perform the API request to fetch all variant groups
*/
public Call<ResponseBody> find() {
return this.service.fetchVariantGroups(this.headers, this.params);
}

/**
* Links content types to the variant group.
*
* @param contentTypeUids Array of content type UIDs to link to the variant group
* @return A Call object that can be executed to perform the API request
* @throws IllegalAccessError if the variant group UID is not set
* @throws IllegalArgumentException if contentTypeUids is empty
*/
public Call<ResponseBody> linkContentTypes(@NotNull String... contentTypeUids) {
if (contentTypeUids.length == 0) {
throw new IllegalArgumentException("Content type UIDs cannot be empty");
}
return updateContentTypeLinks(contentTypeUids, true);
}

/**
* Unlinks content types from the variant group.
*
* @param contentTypeUids Array of content type UIDs to unlink from the variant group
* @return A Call object that can be executed to perform the API request
* @throws IllegalAccessError if the variant group UID is not set
* @throws IllegalArgumentException if contentTypeUids is empty
*/
public Call<ResponseBody> unlinkContentTypes(@NotNull String... contentTypeUids) {
if (contentTypeUids.length == 0) {
throw new IllegalArgumentException("Content type UIDs cannot be empty");
}
return updateContentTypeLinks(contentTypeUids, false);
}

/**
* Updates the linking status of content types to a variant group.
* This private method handles both linking and unlinking operations.
*
* @param contentTypeUids Array of content type UIDs to update
* @param isLink true to link content types, false to unlink
* @return A Call object that can be executed to perform the API request
* @throws IllegalAccessError if the variant group UID is not set
*/
@SuppressWarnings("unchecked")
private Call<ResponseBody> updateContentTypeLinks(String[] contentTypeUids, boolean isLink) {
validate();

// Construct the request body
JSONObject requestBody = new JSONObject();
JSONArray contentTypes = new JSONArray();
JSONArray branches = new JSONArray();
for (String branch : this.branches) {
branches.add(branch);
}

for (String uid : contentTypeUids) {
JSONObject contentType = new JSONObject();
contentType.put("uid", uid);
contentType.put("status", isLink ? "linked" : "unlinked");
contentTypes.add(contentType);
}
requestBody.put("uid", this.variantGroupUid);
requestBody.put("branches", branches);
requestBody.put("content_types", contentTypes);
return this.service.updateVariantGroupContentTypes(this.headers, this.variantGroupUid, this.params, requestBody);
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/contentstack/cms/stack/VariantsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.contentstack.cms.stack;

import java.util.Map;

import org.json.simple.JSONObject;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.HeaderMap;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.QueryMap;

/**
* Service interface for variant group related API endpoints.
*/
public interface VariantsService {

@GET("variant_groups")
Call<ResponseBody> fetchVariantGroups(
@HeaderMap Map<String, Object> headers,
@QueryMap Map<String, Object> queryParam);

@PUT("variant_groups/{variant_group_uid}/variants")
Call<ResponseBody> updateVariantGroupContentTypes(
@HeaderMap Map<String, Object> headers,
@Path("variant_group_uid") String variantGroupUid,
@QueryMap Map<String, Object> queryParam,
@Body JSONObject body);
}
3 changes: 3 additions & 0 deletions src/test/java/com/contentstack/cms/TestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ public class TestClient {
public final static String USER_ID = (env.get("userId") != null) ? env.get("userId") : "c11e668e0295477f";
public final static String OWNERSHIP = (env.get("ownershipToken") != null) ? env.get("ownershipToken")
: "ownershipTokenId";
// file deepcode ignore NonCryptoHardcodedSecret/test: <please specify a reason of ignoring this>
public final static String API_KEY = (env.get("apiKey") != null) ? env.get("apiKey") : "apiKey99999999";
public final static String MANAGEMENT_TOKEN = (env.get("managementToken") != null) ? env.get("managementToken")
: "managementToken99999999";

public final static String DEV_HOST = "api.contentstack.io";
// (env.get("dev_host") != null) ? env.get("dev_host") : "api.contentstack.io";
public final static String VARIANT_GROUP_UID = (env.get("variantGroupUid") != null) ? env.get("variantGroupUid")
: "variantGroupUid99999999";
private static Contentstack instance;
private static Stack stackInstance;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ void getterSetterUserModelLastName() {
@Test
void getterSetterUserModelUsername() {
UserModel userModel = new UserModel();
// deepcode ignore NoHardcodedCredentials/test: <please specify a reason of ignoring this>
userModel.setUsername("***REMOVED***");
Assertions.assertEquals("***REMOVED***",
userModel.getUsername());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
StackAPITest.class,
TokenAPITest.class,
OrgApiTests.class,
GlobalFieldAPITest.class
GlobalFieldAPITest.class,
VariantGroupAPITest.class,
VariantGroupTest.class

})
})
public class APISanityTestSuite {

}
Loading
Loading