diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md
new file mode 100644
index 0000000..f5c1f87
--- /dev/null
+++ b/.cursor/rules/README.md
@@ -0,0 +1,5 @@
+# Cursor (optional)
+
+**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**.
+
+This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs.
diff --git a/.gitignore b/.gitignore
index e21cc92..9fdd99d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -260,6 +260,9 @@ gradle-app.setting
sample/
+# Local-only Spring Boot LP demo (not committed by default)
+sample-lp-demo/
+
# End of https://www.toptal.com/developers/gitignore/api/macos,code-java,java-web,maven,gradle,intellij,visualstudiocode,eclipse
.idea/compiler.xml
.idea/encodings.xml
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..95bc73e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,53 @@
+# Contentstack Utils (Java) – Agent guide
+
+**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**.
+
+## What this repo is
+
+| Field | Detail |
+|--------|--------|
+| **Name:** | [contentstack-utils-java](https://github.com/contentstack/contentstack-utils-java) — Maven `com.contentstack.sdk:utils` |
+| **Purpose:** | Library for rendering **Rich Text Editor (RTE)** content and **embedded items** from Contentstack entry JSON (REST/CDA-style with `_embedded_items`) and GraphQL-shaped responses. Consumed by the [Contentstack Java Delivery SDK](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/java) and apps that already hold entry JSON. |
+| **Out of scope (if any):** | **No HTTP client** in this package: no stack API calls, tokens, or `includeEmbeddedItems()` — those belong to the Delivery SDK or your app. Optional **`sample/`** wires the SDK + Utils for manual testing only. |
+
+## Tech stack (at a glance)
+
+| Area | Details |
+|------|---------|
+| **Language** | Java **17** — `maven-compiler-plugin` `17` in root `pom.xml` (legacy `1.8` properties in `pom.xml` are not authoritative). |
+| **Build** | **Maven** — root `pom.xml`; optional module `sample/pom.xml`. |
+| **Tests** | **JUnit 4**, Maven **Surefire** (`src/test/java/com/contentstack/utils/**/*.java`). Surefire **`testFailureIgnore`** is `true` — check `target/surefire-reports/`. |
+| **Lint / coverage** | No Checkstyle/Spotless in repo — match existing style. **JaCoCo** (`target/site/jacoco/` after `mvn test`). |
+| **Other** | JSON: `org.json`, `json-simple` (provided). HTML: **Jsoup**. `spring-web` compile dependency — not a public REST client API for this module. **Snyk** on PRs (`.github/workflows/sca-scan.yml`). |
+
+## Commands (quick reference)
+
+| Command type | Command |
+|--------------|---------|
+| **Build** | `mvn clean compile` |
+| **Test** | `mvn clean test` |
+| **Lint** | *(none configured — rely on IDE and code review)* |
+
+| Optional | Command / location |
+|----------|---------------------|
+| Single test class | `mvn test -Dtest=UtilTests` |
+| Javadoc | `mvn javadoc:javadoc` |
+| Sample (after `mvn install` with skips if needed) | `mvn -f sample/pom.xml compile` |
+| **CI** | Java **17** publish: `.github/workflows/maven-publish.yml` · SCA: `.github/workflows/sca-scan.yml` · branch rules: `.github/workflows/check-branch.yml` |
+
+## Where the documentation lives: skills
+
+| Skill | Path | What it covers |
+|-------|------|----------------|
+| **Development workflow** | [`skills/dev-workflow/SKILL.md`](skills/dev-workflow/SKILL.md) | Branches, CI, build/test commands, PR expectations, optional TDD. |
+| **Java (language & layout)** | [`skills/java/SKILL.md`](skills/java/SKILL.md) | Java 17, `com.contentstack.utils` packages, naming, JSON/Jsoup, dependencies. |
+| **Contentstack Utils API** | [`skills/contentstack-utils-java/SKILL.md`](skills/contentstack-utils-java/SKILL.md) | Public API: `Utils`, `GQL`, `DefaultOption`, JSON contracts, RTE/embedded boundaries. |
+| **Testing** | [`skills/testing/SKILL.md`](skills/testing/SKILL.md) | JUnit 4, fixtures, Surefire/JaCoCo, offline tests vs `sample/`. |
+| **Code review** | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist, optional Blocker/Major/Minor. |
+| **Framework (build & tooling)** | [`skills/framework/SKILL.md`](skills/framework/SKILL.md) | Maven plugins, publishing, GPG, Central, `sample/` dependency hygiene. |
+
+An index with **when to use** hints is in [`skills/README.md`](skills/README.md).
+
+## Using Cursor (optional)
+
+If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`** — same docs as everyone else.
diff --git a/Changelog.md b/Changelog.md
index 3bae033..ae598d0 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,13 @@
# Changelog
A brief description of what changes project contains
+
+## Apr 20, 2026
+
+#### v1.5.0
+
+- Enhancement: Live Preview Editable tags
+
## Mar 23, 2026
#### v1.4.0
diff --git a/pom.xml b/pom.xml
index 3f15ef1..c643cf7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.contentstack.sdk
utils
- 1.4.0
+ 1.5.0
jar
Contentstack-utils
Java Utils SDK for Contentstack Content Delivery API, Contentstack is a headless CMS
diff --git a/skills/README.md b/skills/README.md
new file mode 100644
index 0000000..c6774a7
--- /dev/null
+++ b/skills/README.md
@@ -0,0 +1,16 @@
+# Skills – Contentstack Utils (Java)
+
+Source of truth for detailed guidance. Read **`AGENTS.md`** first, then open the skill that matches your task.
+
+## When to use which skill
+
+| Skill folder | Use when |
+|--------------|----------|
+| **dev-workflow** | Branches, CI, how to run build/test, PR expectations, optional TDD. |
+| **java** | Java 17 conventions, package layout under `com.contentstack.utils`, JSON/Jsoup, Javadoc, dependency discipline. |
+| **contentstack-utils-java** | Changing `Utils`, `GQL`, `DefaultOption`, callbacks, embedded/RTE behavior or JSON contracts. |
+| **testing** | Writing or refactoring tests, fixtures, Surefire/JaCoCo, offline tests. |
+| **code-review** | Reviewing a PR or self-review before merge. |
+| **framework** | Editing `pom.xml`, plugins, release signing, Maven Central, or `sample/` dependency versions. |
+
+Each folder contains **`SKILL.md`** with YAML frontmatter (`name`, `description`).
diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md
new file mode 100644
index 0000000..8f3e938
--- /dev/null
+++ b/skills/code-review/SKILL.md
@@ -0,0 +1,49 @@
+---
+name: code-review
+description: PR checklist and optional Blocker/Major/Minor — use when reviewing or before submitting a PR
+---
+
+# Code review – Contentstack Utils (Java)
+
+## When to use
+
+- Reviewing another contributor’s pull request.
+- Self-review before merge.
+- Auditing API, security, or test coverage for a change set.
+
+## Instructions
+
+### API design and stability
+
+- [ ] **Public API:** New or changed methods on `Utils`, `GQL`, `DefaultOption`, or `interfaces` are necessary, Javadoc’d, and safe for `com.contentstack.sdk:utils` consumers.
+- [ ] **Backward compatibility:** Breaking changes only with major version / **`Changelog.md`** plan.
+- [ ] **Naming:** Consistent with existing Utils and RTE/embedded terminology.
+
+### Error handling and robustness
+
+- [ ] **JSON:** Missing keys / `_embedded_items` behave predictably; no accidental NPEs or silent semantic changes.
+- [ ] **Null safety:** `JSONObject` / `JSONArray` access follows existing `opt*` / `has` patterns.
+
+### Dependencies and security
+
+- [ ] **Dependencies:** `pom.xml` changes are justified; consider downstream Java SDK consumers.
+- [ ] **SCA:** Snyk / team process (`.github/workflows/sca-scan.yml`) — address or defer with a ticket.
+
+### Testing
+
+- [ ] **Coverage:** New behavior has tests and fixtures under `src/test/java` / `src/test/resources` as needed.
+- [ ] **Surefire:** With `testFailureIgnore`, verify **`target/surefire-reports/`**, not only exit code.
+
+### Severity (optional)
+
+| Level | Examples |
+|-------|----------|
+| **Blocker** | Unapproved breaking public API; critical CVE; no tests for new behavior. |
+| **Major** | Undocumented HTML/JSON behavior change; missing Javadoc on new public API; risky dependency bump. |
+| **Minor** | Style, typos, internal refactor with equivalent coverage. |
+
+## References
+
+- **`skills/testing/SKILL.md`** — test conventions and Surefire.
+- **`skills/contentstack-utils-java/SKILL.md`** — API boundaries.
+- **`AGENTS.md`** — stack and commands.
diff --git a/skills/contentstack-utils-java/SKILL.md b/skills/contentstack-utils-java/SKILL.md
new file mode 100644
index 0000000..d5973b2
--- /dev/null
+++ b/skills/contentstack-utils-java/SKILL.md
@@ -0,0 +1,44 @@
+---
+name: contentstack-utils-java
+description: Public API — Utils, GQL, DefaultOption, RTE/embedded JSON; use when changing SDK behavior or contracts
+---
+
+# Contentstack Utils API – Contentstack Utils (Java)
+
+## When to use
+
+- Changing **`Utils`**, **`GQL`**, **`DefaultOption`**, **`Option`**, callbacks, or embedded/RTE logic.
+- Reviewing JSON shape assumptions for CDA or GraphQL responses.
+
+## Instructions
+
+### Scope
+
+- Artifact **`com.contentstack.sdk:utils`** only **transforms** JSON that apps or the [Java Delivery SDK](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/java) already fetched.
+- **Authentication, API keys, delivery tokens, and `includeEmbeddedItems()`** are out of scope here — handled by the SDK or app code.
+
+### Entry points
+
+- **`com.contentstack.utils.Utils`** — `render`, `renderContent`, `renderContents`, `jsonToHTML` for REST/CDA-style JSON with `_embedded_items`; dot-paths into entries (e.g. `group.field`). Variant-related helpers as documented in `Utils`.
+- **`com.contentstack.utils.GQL`** — `jsonToHTML` for GraphQL-shaped entries (`embedded_itemsConnection`, `edges`, `node`, JSON RTE under `json`). Do not instantiate `GQL` (private constructor).
+
+### Rendering and options
+
+- Implement **`com.contentstack.utils.interfaces.Option`** or extend **`com.contentstack.utils.render.DefaultOption`** for custom embedded HTML, marks, and nodes.
+- Use **`com.contentstack.utils.interfaces.NodeCallback`** and **`com.contentstack.utils.helper.Metadata`** with **`embedded.StyleType`** / **`embedded.ItemType`** as in existing code.
+
+### Data flow and compatibility
+
+- Shared traversal: **`AutomateCommon`**; JSON RTE trees: **`NodeToHTML`**.
+- Preserve keys and HTML class names (`_embedded_items`, `embedded-entry`, etc.) unless shipping a **breaking** version with changelog.
+- Prefer null-safe **`opt*`** / **`has`** on `JSONObject` / `JSONArray`.
+
+### Alignment with Contentstack
+
+- Entry JSON shapes align with the [Content Delivery API](https://www.contentstack.com/docs/apis/content-delivery-api/) as consumed by the Java SDK; root **`README.md`** shows `Contentstack.stack`, `Entry`, `Query` usage **outside** this JAR.
+
+## References
+
+- **`skills/java/SKILL.md`** — language and package conventions.
+- **`skills/testing/SKILL.md`** — tests for API changes.
+- Root **`README.md`** — Maven coordinates and embedded-items examples.
diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md
new file mode 100644
index 0000000..3a988df
--- /dev/null
+++ b/skills/dev-workflow/SKILL.md
@@ -0,0 +1,52 @@
+---
+name: dev-workflow
+description: Branches, CI, build and test commands, PR expectations, optional TDD — use when onboarding or before a PR
+---
+
+# Development workflow – Contentstack Utils (Java)
+
+## When to use
+
+- Starting work in this repository or onboarding.
+- Before opening or updating a pull request.
+- You need the canonical commands for build, test, sample, and CI pointers.
+
+## Instructions
+
+### Branches
+
+- Default integration for PRs is often **`staging`**; merging into **`master`** may be restricted (see `.github/workflows/check-branch.yml`).
+- Feature/fix branches often use ticket-style names (e.g. `fix/DX-5734`).
+
+### Running tests and builds
+
+- **All tests:** `mvn test`
+- **Compile only:** `mvn clean compile`
+- **Full check before PR:** `mvn clean test` (and `mvn -f sample/pom.xml compile` if you changed `sample/` — install parent first; see `skills/framework/SKILL.md`).
+- Review **`target/surefire-reports/`** when debugging: **`testFailureIgnore`** is `true` in `pom.xml` (see `skills/testing/SKILL.md`).
+
+### Pull requests
+
+- Describe the change; link issues/tickets when applicable.
+- Keep public API backward-compatible unless releasing a breaking version; update **`Changelog.md`** for user-visible behavior.
+- Use **`skills/code-review/SKILL.md`** as the review checklist.
+
+### Optional: TDD
+
+- If the team uses TDD: RED → GREEN → REFACTOR. Structure and Surefire behavior are in **`skills/testing/SKILL.md`**.
+
+### CI and security
+
+- **Java 17** in `.github/workflows/maven-publish.yml`.
+- **Snyk** Maven scan on PRs: `.github/workflows/sca-scan.yml`.
+- **Javadoc:** optional locally `mvn javadoc:javadoc`; attach phase uses `-Xdoclint:none` per `pom.xml`.
+
+### Lint / format
+
+- No repo-wide Checkstyle/Spotless — match existing style (`skills/java/SKILL.md`).
+
+## References
+
+- **`skills/testing/SKILL.md`** — Surefire, JaCoCo, fixtures.
+- **`skills/framework/SKILL.md`** — Maven install skips, publishing, `sample/pom.xml`.
+- **`AGENTS.md`** — commands quick reference.
diff --git a/skills/framework/SKILL.md b/skills/framework/SKILL.md
new file mode 100644
index 0000000..43d679f
--- /dev/null
+++ b/skills/framework/SKILL.md
@@ -0,0 +1,64 @@
+---
+name: framework
+description: Maven, plugins, Java 17, publishing, sample module deps — use when editing pom.xml or release tooling
+---
+
+# Framework (build & tooling) – Contentstack Utils (Java)
+
+## When to use
+
+- Editing root **`pom.xml`** or **`sample/pom.xml`**.
+- Changing release/publish flow, signing, or CI-related Maven settings.
+- Aligning **`sample/`** `contentstack.utils.version` with a new library release.
+
+## Instructions
+
+### Build commands
+
+```bash
+mvn clean compile
+mvn test
+mvn package
+```
+
+Local install without GPG/Javadoc (common for dev before `sample/`):
+
+```bash
+mvn install -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true
+```
+
+### Java version
+
+- **`maven-compiler-plugin`** — **`17`**. Match **Java 17** locally and in `.github/workflows/maven-publish.yml`.
+
+### Key plugins (root `pom.xml`)
+
+| Plugin | Role |
+|--------|------|
+| `maven-surefire-plugin` | Runs JUnit; note `testFailureIgnore` |
+| `jacoco-maven-plugin` | Coverage → `target/site/jacoco/` |
+| `maven-javadoc-plugin` | Javadoc JAR; `-Xdoclint:none` in execution |
+| `central-publishing-maven-plugin` | Maven Central publishing |
+| `maven-gpg-plugin` | Signs artifacts on release |
+
+### Coordinates
+
+- **groupId:** `com.contentstack.sdk`
+- **artifactId:** `utils`
+- **version:** root `pom.xml` ``
+
+### Dependency hygiene
+
+- Keep the dependency set small; this JAR is pulled in by the Contentstack Java SDK.
+- **Snyk** on PRs: `.github/workflows/sca-scan.yml`.
+- **`sample/pom.xml`:** Keep **`contentstack.utils.version`** in sync when bumping the root version; use **`dependencyManagement`** for transitive CVE overrides when needed.
+
+### Formatting
+
+- No Checkstyle/Spotless — match surrounding code (`skills/java/SKILL.md`).
+
+## References
+
+- **`skills/dev-workflow/SKILL.md`** — when to run full build/test.
+- **`skills/testing/SKILL.md`** — Surefire/JaCoCo behavior.
+- **`AGENTS.md`** — CI file pointers.
diff --git a/skills/java/SKILL.md b/skills/java/SKILL.md
new file mode 100644
index 0000000..494e745
--- /dev/null
+++ b/skills/java/SKILL.md
@@ -0,0 +1,54 @@
+---
+name: java
+description: Java 17, package layout, naming, JSON/Jsoup, Javadoc — use when editing any production or test Java in this repo
+---
+
+# Java – Contentstack Utils (Java)
+
+## When to use
+
+- Editing any `.java` file under this repository.
+- Adding dependencies or debating style vs other Contentstack SDKs.
+
+## Instructions
+
+### Language and runtime
+
+- **Java 17** via `maven-compiler-plugin` `17` in `pom.xml`. Ignore legacy `maven.compiler.source/target` `1.8` — the compiler plugin wins.
+- Avoid raw types; use generics where applicable.
+
+### Package and layout
+
+- Production code lives under **`com.contentstack.utils`** and subpackages: `render`, `node`, `embedded`, `helper`, `interfaces`.
+- Do not add new top-level packages without team alignment.
+
+### Naming
+
+- **Classes:** PascalCase (`Utils`, `DefaultOption`, `AutomateCommon`).
+- **Methods / variables:** camelCase.
+- **Tests:** Mixed conventions already in use — see **`skills/testing/SKILL.md`**.
+
+### JSON and HTML
+
+- Prefer **`org.json`** (`JSONObject`, `JSONArray`) for APIs and internals used by `Utils` and `GQL`.
+- Use **Jsoup** for RTE HTML; follow patterns in `AutomateCommon` and `Utils`.
+
+### Validation and utility types
+
+- `javax.validation.constraints` (e.g. `@NotNull`) on some public methods — keep Javadoc aligned with null behavior.
+- Private constructors for non-instantiable types where the codebase already does (`GQL`, `AutomateCommon`).
+
+### Dependencies
+
+- Small JAR consumed by the Contentstack Java SDK — prefer minimal dependencies; justify additions in `pom.xml`.
+- **No Lombok** in this repo unless explicitly adopted by the team.
+
+### Documentation
+
+- Javadoc on public API; examples must match real entry points (`Utils.render`, `GQL.jsonToHTML`, `DefaultOption`).
+
+## References
+
+- **`skills/contentstack-utils-java/SKILL.md`** — RTE, embedded, GraphQL shapes.
+- **`skills/testing/SKILL.md`** — test naming and layout.
+- **`AGENTS.md`** — tech stack summary.
diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md
new file mode 100644
index 0000000..d2dc2d7
--- /dev/null
+++ b/skills/testing/SKILL.md
@@ -0,0 +1,47 @@
+---
+name: testing
+description: JUnit 4, Surefire, JaCoCo, fixtures, offline policy — use when adding or changing tests
+---
+
+# Testing – Contentstack Utils (Java)
+
+## When to use
+
+- Creating or editing tests under `src/test/java/com/contentstack/utils/`.
+- Adding JSON under `src/test/resources/`.
+- Investigating CI failures or coverage gaps.
+
+## Instructions
+
+### Framework
+
+- **JUnit 4** (`junit:junit`, test scope in `pom.xml`).
+- **Maven Surefire** runs classes from `src/test/java`.
+
+### Surefire caveat
+
+- **`testFailureIgnore`** is **`true`** in `pom.xml`. Always inspect **`target/surefire-reports/`** — a successful Maven exit code does not guarantee all tests passed.
+
+### Naming and layout
+
+- Mirror package **`com.contentstack.utils`**.
+- Existing patterns: `UtilTests`, `DefaultOptionTests`, `AssetLinkTest`, `TestRte`, `TestMetadata`, `Test*`, `*Test`, `*Tests`.
+- No separate `*IT` Maven profile in this repo; optional **`sample/`** uses the Delivery SDK with env vars (see `sample/README.md`).
+
+### Fixtures
+
+- JSON and assets under **`src/test/resources/`** (e.g. `multiple_rich_text_content.json`, `reports/`). Loading patterns: `ReadResource`, `UtilTests` `@BeforeClass`.
+
+### Helpers
+
+- `ReadResource`, `DefaultOptionClass`, and similar helpers — keep tests **deterministic** and **offline** (no live API for default unit tests).
+
+### Coverage
+
+- **JaCoCo** on `mvn test`; HTML report **`target/site/jacoco/index.html`**.
+
+## References
+
+- **`skills/dev-workflow/SKILL.md`** — when to run tests before PRs.
+- **`skills/framework/SKILL.md`** — Maven/Surefire configuration.
+- **`skills/code-review/SKILL.md`** — test expectations for reviews.
diff --git a/src/main/java/com/contentstack/utils/EditableTags.java b/src/main/java/com/contentstack/utils/EditableTags.java
new file mode 100644
index 0000000..cc38868
--- /dev/null
+++ b/src/main/java/com/contentstack/utils/EditableTags.java
@@ -0,0 +1,351 @@
+package com.contentstack.utils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Live Preview editable tags (CSLP) — parity with contentstack-utils-javascript
+ * {@code entry-editable.ts}.
+ */
+public final class EditableTags {
+
+ /**
+ * Variant / meta-key state threaded through {@link #getTag(Object, String, boolean, String, AppliedVariantsState)}.
+ */
+ public static final class AppliedVariantsState {
+ private final JSONObject appliedVariants;
+ private final boolean shouldApplyVariant;
+ private final String metaKey;
+
+ public AppliedVariantsState(JSONObject appliedVariants, boolean shouldApplyVariant, String metaKey) {
+ this.appliedVariants = appliedVariants;
+ this.shouldApplyVariant = shouldApplyVariant;
+ this.metaKey = metaKey != null ? metaKey : "";
+ }
+
+ public JSONObject getAppliedVariants() {
+ return appliedVariants;
+ }
+
+ public boolean isShouldApplyVariant() {
+ return shouldApplyVariant;
+ }
+
+ public String getMetaKey() {
+ return metaKey;
+ }
+ }
+
+ private EditableTags() {
+ }
+
+ /**
+ * Adds Contentstack Live Preview (CSLP) data tags to an entry for editable UIs.
+ * Mutates the entry by attaching a {@code $} property with tag strings or objects
+ * ({@code data-cslp} / {@code data-cslp-parent-field}) for each field.
+ *
+ * @param entry CDA-style entry JSON (must not be {@code null}); must contain {@code uid}
+ * @param contentTypeUid content type UID (e.g. {@code blog_post})
+ * @param tagsAsObject if {@code true}, tags are JSON objects; if {@code false}, {@code data-cslp=...} strings
+ * @param locale locale code (default in overloads: {@code en-us})
+ * @param options optional; controls locale casing (default lowercases locale)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale,
+ EditableTagsOptions options) {
+ if (entry == null) {
+ return;
+ }
+ boolean useLowerCaseLocale = true;
+ if (options != null) {
+ useLowerCaseLocale = options.isUseLowerCaseLocale();
+ }
+ String ct = contentTypeUid == null ? "" : contentTypeUid.toLowerCase();
+ String loc = locale == null ? "en-us" : locale;
+ if (useLowerCaseLocale) {
+ loc = loc.toLowerCase();
+ }
+ JSONObject applied = entry.optJSONObject("_applied_variants");
+ if (applied == null) {
+ JSONObject system = entry.optJSONObject("system");
+ if (system != null) {
+ applied = system.optJSONObject("applied_variants");
+ }
+ }
+ boolean shouldApply = applied != null;
+ String uid = entry.optString("uid", "");
+ String prefix = ct + "." + uid + "." + loc;
+ AppliedVariantsState state = new AppliedVariantsState(applied, shouldApply, "");
+ entry.put("$", getTag(entry, prefix, tagsAsObject, loc, state));
+ }
+
+ /**
+ * @see #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject) {
+ addEditableTags(entry, contentTypeUid, tagsAsObject, "en-us", null);
+ }
+
+ /**
+ * @see #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale) {
+ addEditableTags(entry, contentTypeUid, tagsAsObject, locale, null);
+ }
+
+ /**
+ * Alias for {@link #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)} — matches JS
+ * {@code addTags}.
+ */
+ public static void addTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale,
+ EditableTagsOptions options) {
+ addEditableTags(entry, contentTypeUid, tagsAsObject, locale, options);
+ }
+
+ /**
+ * Recursive tag map for the given content (entry object or array). Exposed for parity with JS tests.
+ *
+ * @param content {@link JSONObject}, {@link JSONArray}, or null
+ * @param prefix path prefix ({@code contentTypeUid.entryUid.locale...})
+ * @param tagsAsObject string vs object tag form
+ * @param locale locale for reference entries
+ * @param appliedVariants variant state
+ * @return map of field keys to tag string or tag object
+ */
+ public static JSONObject getTag(Object content, String prefix, boolean tagsAsObject, String locale,
+ AppliedVariantsState appliedVariants) {
+ if (content == null || JSONObject.NULL.equals(content)) {
+ return new JSONObject();
+ }
+ if (content instanceof JSONArray) {
+ return getTagForArray((JSONArray) content, prefix, tagsAsObject, locale, appliedVariants);
+ }
+ if (content instanceof JSONObject) {
+ return getTagForJSONObject((JSONObject) content, prefix, tagsAsObject, locale, appliedVariants);
+ }
+ return new JSONObject();
+ }
+
+ private static JSONObject getTagForJSONObject(JSONObject content, String prefix, boolean tagsAsObject,
+ String locale, AppliedVariantsState appliedVariants) {
+ JSONObject tags = new JSONObject();
+ Iterator keys = content.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ handleKey(tags, key, content.opt(key), prefix, tagsAsObject, locale, appliedVariants);
+ }
+ return tags;
+ }
+
+ private static JSONObject getTagForArray(JSONArray content, String prefix, boolean tagsAsObject, String locale,
+ AppliedVariantsState appliedVariants) {
+ JSONObject tags = new JSONObject();
+ for (int i = 0; i < content.length(); i++) {
+ String key = Integer.toString(i);
+ handleKey(tags, key, content.opt(i), prefix, tagsAsObject, locale, appliedVariants);
+ }
+ return tags;
+ }
+
+ /** One entry from {@code Object.entries} — same structure for {@link JSONObject} and {@link JSONArray}. */
+ private static void handleKey(JSONObject tags, String key, Object value, String prefix, boolean tagsAsObject,
+ String locale, AppliedVariantsState appliedVariants) {
+ if ("$".equals(key)) {
+ return;
+ }
+ boolean shouldApplyVariant = appliedVariants.isShouldApplyVariant();
+ JSONObject applied = appliedVariants.getAppliedVariants();
+
+ String metaUid = metaUidFromValue(value);
+ String metaKeyPrefix = appliedVariants.getMetaKey().isEmpty() ? "" : appliedVariants.getMetaKey() + ".";
+ String updatedMetakey = shouldApplyVariant ? metaKeyPrefix + key : "";
+ if (!metaUid.isEmpty() && !updatedMetakey.isEmpty()) {
+ updatedMetakey = updatedMetakey + "." + metaUid;
+ }
+ // For array fields, per-element processing below must not overwrite this — line 220's field tag uses it.
+ String fieldMetakey = updatedMetakey;
+
+ if (value instanceof JSONArray) {
+ JSONArray arr = (JSONArray) value;
+ for (int index = 0; index < arr.length(); index++) {
+ Object obj = arr.opt(index);
+ if (obj == null || JSONObject.NULL.equals(obj)) {
+ continue;
+ }
+ String childKey = key + "__" + index;
+ String parentKey = key + "__parent";
+ metaUid = metaUidFromValue(obj);
+ String elementMetakey = shouldApplyVariant ? metaKeyPrefix + key : "";
+ if (!metaUid.isEmpty() && !elementMetakey.isEmpty()) {
+ elementMetakey = elementMetakey + "." + metaUid;
+ }
+ String indexPath = prefix + "." + key + "." + index;
+ String fieldPath = prefix + "." + key;
+ putTag(tags, childKey, indexPath, tagsAsObject, applied, shouldApplyVariant, elementMetakey);
+ putParentTag(tags, parentKey, fieldPath, tagsAsObject);
+ if (obj instanceof JSONObject) {
+ JSONObject jobj = (JSONObject) obj;
+ if (jobj.has("_content_type_uid") && jobj.has("uid")) {
+ JSONObject newApplied = jobj.optJSONObject("_applied_variants");
+ if (newApplied == null) {
+ JSONObject sys = jobj.optJSONObject("system");
+ if (sys != null) {
+ newApplied = sys.optJSONObject("applied_variants");
+ }
+ }
+ boolean newShould = newApplied != null;
+ String refLocale = jobj.has("locale") && !jobj.isNull("locale")
+ ? jobj.optString("locale", locale)
+ : locale;
+ String refPrefix = jobj.optString("_content_type_uid") + "." + jobj.optString("uid") + "."
+ + refLocale;
+ jobj.put("$", getTag(jobj, refPrefix, tagsAsObject, refLocale,
+ new AppliedVariantsState(newApplied, newShould, "")));
+ } else {
+ jobj.put("$", getTag(jobj, indexPath, tagsAsObject, locale,
+ new AppliedVariantsState(applied, shouldApplyVariant, elementMetakey)));
+ }
+ }
+ }
+ } else if (value instanceof JSONObject) {
+ JSONObject valueObj = (JSONObject) value;
+ valueObj.put("$", getTag(valueObj, prefix + "." + key, tagsAsObject, locale,
+ new AppliedVariantsState(applied, shouldApplyVariant, updatedMetakey)));
+ }
+
+ String fieldTagPath = prefix + "." + key;
+ putTag(tags, key, fieldTagPath, tagsAsObject, applied, shouldApplyVariant, fieldMetakey);
+ }
+
+ private static String metaUidFromValue(Object value) {
+ if (!(value instanceof JSONObject)) {
+ return "";
+ }
+ JSONObject jo = (JSONObject) value;
+ JSONObject meta = jo.optJSONObject("_metadata");
+ if (meta == null) {
+ return "";
+ }
+ return meta.optString("uid", "");
+ }
+
+ private static void putTag(JSONObject tags, String key, String dataValue, boolean tagsAsObject,
+ JSONObject appliedVariants, boolean shouldApplyVariant, String metaKey) {
+ TagsPayload payload = new TagsPayload(appliedVariants, shouldApplyVariant, metaKey);
+ if (tagsAsObject) {
+ tags.put(key, getTagsValueAsObject(dataValue, payload));
+ } else {
+ tags.put(key, getTagsValueAsString(dataValue, payload));
+ }
+ }
+
+ private static void putParentTag(JSONObject tags, String key, String dataValue, boolean tagsAsObject) {
+ if (tagsAsObject) {
+ tags.put(key, getParentTagsValueAsObject(dataValue));
+ } else {
+ tags.put(key, getParentTagsValueAsString(dataValue));
+ }
+ }
+
+ private static final class TagsPayload {
+ private final JSONObject appliedVariants;
+ private final boolean shouldApplyVariant;
+ private final String metaKey;
+
+ private TagsPayload(JSONObject appliedVariants, boolean shouldApplyVariant, String metaKey) {
+ this.appliedVariants = appliedVariants;
+ this.shouldApplyVariant = shouldApplyVariant;
+ this.metaKey = metaKey != null ? metaKey : "";
+ }
+ }
+
+ static String applyVariantToDataValue(String dataValue, JSONObject appliedVariants, boolean shouldApplyVariant,
+ String metaKey) {
+ if (shouldApplyVariant && appliedVariants != null) {
+ Object direct = appliedVariants.opt(metaKey);
+ if (direct != null && !JSONObject.NULL.equals(direct)) {
+ String variant = String.valueOf(direct);
+ String[] newDataValueArray = ("v2:" + dataValue).split("\\.", -1);
+ if (newDataValueArray.length > 1) {
+ newDataValueArray[1] = newDataValueArray[1] + "_" + variant;
+ return String.join(".", newDataValueArray);
+ }
+ }
+ String parentVariantisedPath = getParentVariantisedPath(appliedVariants, metaKey);
+ if (parentVariantisedPath != null && !parentVariantisedPath.isEmpty()) {
+ Object v = appliedVariants.opt(parentVariantisedPath);
+ if (v != null && !JSONObject.NULL.equals(v)) {
+ String variant = String.valueOf(v);
+ String[] newDataValueArray = ("v2:" + dataValue).split("\\.", -1);
+ if (newDataValueArray.length > 1) {
+ newDataValueArray[1] = newDataValueArray[1] + "_" + variant;
+ return String.join(".", newDataValueArray);
+ }
+ }
+ }
+ }
+ return dataValue;
+ }
+
+ static String getParentVariantisedPath(JSONObject appliedVariants, String metaKey) {
+ try {
+ if (appliedVariants == null) {
+ return "";
+ }
+ List variantisedFieldPaths = new ArrayList<>(appliedVariants.keySet());
+ variantisedFieldPaths.sort(Comparator.comparingInt(String::length).reversed());
+ String[] childPathFragments = metaKey.split("\\.", -1);
+ if (childPathFragments.length == 0 || variantisedFieldPaths.isEmpty()) {
+ return "";
+ }
+ for (String path : variantisedFieldPaths) {
+ String[] parentFragments = path.split("\\.", -1);
+ if (parentFragments.length > childPathFragments.length) {
+ continue;
+ }
+ boolean all = true;
+ for (int i = 0; i < parentFragments.length; i++) {
+ if (!Objects.equals(parentFragments[i], childPathFragments[i])) {
+ all = false;
+ break;
+ }
+ }
+ if (all) {
+ return path;
+ }
+ }
+ return "";
+ } catch (RuntimeException e) {
+ return "";
+ }
+ }
+
+ private static JSONObject getTagsValueAsObject(String dataValue, TagsPayload payload) {
+ String resolved = applyVariantToDataValue(dataValue, payload.appliedVariants, payload.shouldApplyVariant,
+ payload.metaKey);
+ JSONObject o = new JSONObject();
+ o.put("data-cslp", resolved);
+ return o;
+ }
+
+ private static String getTagsValueAsString(String dataValue, TagsPayload payload) {
+ String resolved = applyVariantToDataValue(dataValue, payload.appliedVariants, payload.shouldApplyVariant,
+ payload.metaKey);
+ return "data-cslp=" + resolved;
+ }
+
+ private static JSONObject getParentTagsValueAsObject(String dataValue) {
+ JSONObject o = new JSONObject();
+ o.put("data-cslp-parent-field", dataValue);
+ return o;
+ }
+
+ private static String getParentTagsValueAsString(String dataValue) {
+ return "data-cslp-parent-field=" + dataValue;
+ }
+}
diff --git a/src/main/java/com/contentstack/utils/EditableTagsOptions.java b/src/main/java/com/contentstack/utils/EditableTagsOptions.java
new file mode 100644
index 0000000..3055b39
--- /dev/null
+++ b/src/main/java/com/contentstack/utils/EditableTagsOptions.java
@@ -0,0 +1,30 @@
+package com.contentstack.utils;
+
+/**
+ * Options for {@link Utils#addEditableTags(org.json.JSONObject, String, boolean, String, EditableTagsOptions)}.
+ */
+public final class EditableTagsOptions {
+
+ private boolean useLowerCaseLocale = true;
+
+ public EditableTagsOptions() {
+ }
+
+ /**
+ * When {@code true} (default), the locale string is lowercased to match the JavaScript Utils default.
+ *
+ * @return whether locale is normalized to lowercase
+ */
+ public boolean isUseLowerCaseLocale() {
+ return useLowerCaseLocale;
+ }
+
+ /**
+ * @param useLowerCaseLocale if {@code true}, locale is lowercased; if {@code false}, locale is left as-is
+ * @return this instance for chaining
+ */
+ public EditableTagsOptions setUseLowerCaseLocale(boolean useLowerCaseLocale) {
+ this.useLowerCaseLocale = useLowerCaseLocale;
+ return this;
+ }
+}
diff --git a/src/main/java/com/contentstack/utils/Utils.java b/src/main/java/com/contentstack/utils/Utils.java
index 86ea789..6f4f810 100644
--- a/src/main/java/com/contentstack/utils/Utils.java
+++ b/src/main/java/com/contentstack/utils/Utils.java
@@ -399,6 +399,53 @@ public static JSONObject getDataCsvariantsAttribute(JSONArray entries, String co
return getVariantMetadataTags(entries, contentTypeUid);
}
+ /**
+ * Adds Contentstack Live Preview (CSLP) editable tags to an entry. Mutates {@code entry} by attaching a
+ * {@code $} object with {@code data-cslp} / {@code data-cslp-parent-field} values. Behavior matches
+ * contentstack-utils-javascript {@code addTags} / {@code entry-editable.ts}.
+ *
+ * @param entry CDA-style entry JSON; if {@code null}, the method returns without changes
+ * @param contentTypeUid content type UID (lower-cased internally)
+ * @param tagsAsObject if {@code true}, tag values are JSON objects; if {@code false}, {@code data-cslp=...} strings
+ * @param locale locale segment for paths (default in overloads: {@code en-us})
+ * @param options optional; default lowercases locale unless disabled
+ * @see EditableTags#addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale,
+ EditableTagsOptions options) {
+ EditableTags.addEditableTags(entry, contentTypeUid, tagsAsObject, locale, options);
+ }
+
+ /**
+ * @see #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject) {
+ EditableTags.addEditableTags(entry, contentTypeUid, tagsAsObject);
+ }
+
+ /**
+ * @see #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)
+ */
+ public static void addEditableTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale) {
+ EditableTags.addEditableTags(entry, contentTypeUid, tagsAsObject, locale);
+ }
+
+ /**
+ * Alias for {@link #addEditableTags(JSONObject, String, boolean, String, EditableTagsOptions)} (JS {@code addTags}).
+ */
+ public static void addTags(JSONObject entry, String contentTypeUid, boolean tagsAsObject, String locale,
+ EditableTagsOptions options) {
+ EditableTags.addTags(entry, contentTypeUid, tagsAsObject, locale, options);
+ }
+
+ /**
+ * Recursive CSLP tag map for tests and advanced use — see {@link EditableTags#getTag(Object, String, boolean, String, EditableTags.AppliedVariantsState)}.
+ */
+ public static JSONObject getTag(Object content, String prefix, boolean tagsAsObject, String locale,
+ EditableTags.AppliedVariantsState appliedVariants) {
+ return EditableTags.getTag(content, prefix, tagsAsObject, locale, appliedVariants);
+ }
+
private static JSONArray extractVariantAliasesFromEntry(JSONObject entry) {
JSONArray variantArray = new JSONArray();
JSONObject publishDetails = entry.optJSONObject("publish_details");
diff --git a/src/test/java/com/contentstack/utils/EditableTagsTest.java b/src/test/java/com/contentstack/utils/EditableTagsTest.java
new file mode 100644
index 0000000..30a11a1
--- /dev/null
+++ b/src/test/java/com/contentstack/utils/EditableTagsTest.java
@@ -0,0 +1,219 @@
+package com.contentstack.utils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Live Preview editable tags — parity with contentstack-utils-javascript {@code entry-editable.ts}.
+ */
+public class EditableTagsTest {
+
+ @Test
+ public void getTagReturnsEmptyForNullContent() {
+ JSONObject tags = Utils.getTag(null, "a.b.c", false, "en-us",
+ new EditableTags.AppliedVariantsState(null, false, ""));
+ Assert.assertNotNull(tags);
+ Assert.assertEquals(0, tags.length());
+ }
+
+ @Test
+ public void addEditableTagsPrimitivesAsStrings() {
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "entry1");
+ entry.put("title", "Hello");
+ entry.put("count", 42);
+ Utils.addEditableTags(entry, "Blog", false, "en-us", null);
+ JSONObject dollar = entry.getJSONObject("$");
+ Assert.assertEquals("data-cslp=blog.entry1.en-us.title", dollar.getString("title"));
+ Assert.assertEquals("data-cslp=blog.entry1.en-us.count", dollar.getString("count"));
+ }
+
+ @Test
+ public void addEditableTagsPrimitivesAsObjects() {
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("title", "Hi");
+ Utils.addEditableTags(entry, "Post", true, "en-us", null);
+ JSONObject cslp = entry.getJSONObject("$").getJSONObject("title");
+ Assert.assertEquals("post.e1.en-us.title", cslp.getString("data-cslp"));
+ }
+
+ @Test
+ public void contentTypeUidLowercasedAndLocaleLowercasedByDefault() {
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "u1");
+ entry.put("title", "x");
+ Utils.addEditableTags(entry, "LANDING", false, "EN-US", null);
+ Assert.assertTrue(entry.getJSONObject("$").getString("title").contains(".en-us."));
+ }
+
+ @Test
+ public void useLowerCaseLocaleFalsePreservesLocaleCasing() {
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "u1");
+ entry.put("title", "x");
+ EditableTagsOptions opt = new EditableTagsOptions().setUseLowerCaseLocale(false);
+ Utils.addEditableTags(entry, "ct", false, "EN-US", opt);
+ Assert.assertTrue(entry.getJSONObject("$").getString("title").contains(".EN-US."));
+ }
+
+ @Test
+ public void nestedObjectGetsChildDollarMap() {
+ JSONObject inner = new JSONObject();
+ inner.put("name", "inner");
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("group", inner);
+ Utils.addEditableTags(entry, "ct", false, "en-us", null);
+ JSONObject groupDollar = inner.getJSONObject("$");
+ Assert.assertEquals("data-cslp=ct.e1.en-us.group.name", groupDollar.getString("name"));
+ Assert.assertEquals("data-cslp=ct.e1.en-us.group", entry.getJSONObject("$").getString("group"));
+ }
+
+ @Test
+ public void arrayFieldIndexAndParentTags() {
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("items", new JSONArray().put("a").put("b"));
+ Utils.addEditableTags(entry, "ct", false, "en-us", null);
+ JSONObject dollar = entry.getJSONObject("$");
+ Assert.assertEquals("data-cslp=ct.e1.en-us.items.0", dollar.getString("items__0"));
+ Assert.assertEquals("data-cslp=ct.e1.en-us.items.1", dollar.getString("items__1"));
+ Assert.assertEquals("data-cslp-parent-field=ct.e1.en-us.items", dollar.getString("items__parent"));
+ Assert.assertEquals("data-cslp=ct.e1.en-us.items", dollar.getString("items"));
+ }
+
+ /**
+ * Parent field tag for an array must use the field-level metakey (for variant resolution), not the last
+ * element's {@code _metadata.uid} suffix — otherwise a per-element variant key (e.g. {@code items.uidB})
+ * would incorrectly win for the parent {@code items} tag.
+ */
+ @Test
+ public void arrayFieldParentTagUsesFieldMetakeyNotLastElementMetadata() {
+ JSONObject applied = new JSONObject();
+ applied.put("items", "fieldVar");
+ applied.put("items.uidB", "wrongVar");
+ JSONObject metaA = new JSONObject();
+ metaA.put("uid", "uidA");
+ JSONObject metaB = new JSONObject();
+ metaB.put("uid", "uidB");
+ JSONObject el0 = new JSONObject();
+ el0.put("_metadata", metaA);
+ el0.put("x", "a");
+ JSONObject el1 = new JSONObject();
+ el1.put("_metadata", metaB);
+ el1.put("x", "b");
+ JSONArray arr = new JSONArray().put(el0).put(el1);
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("_applied_variants", applied);
+ entry.put("items", arr);
+ Utils.addEditableTags(entry, "ct", false, "en-us", null);
+ String parentItemsTag = entry.getJSONObject("$").getString("items");
+ Assert.assertTrue("parent field should resolve variant via key \"items\"",
+ parentItemsTag.contains("ct.e1_fieldVar.en-us.items"));
+ Assert.assertFalse("parent field must not apply last element's variant (items.uidB -> wrongVar)",
+ parentItemsTag.contains("e1_wrongVar"));
+ }
+
+ @Test
+ public void referenceInArrayUsesRefPrefix() {
+ JSONObject ref = new JSONObject();
+ ref.put("_content_type_uid", "author_ct");
+ ref.put("uid", "refuid");
+ ref.put("title", "Author");
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("authors", new JSONArray().put(ref));
+ Utils.addEditableTags(entry, "post", false, "en-us", null);
+ JSONObject refDollar = ref.getJSONObject("$");
+ Assert.assertEquals("data-cslp=author_ct.refuid.en-us.title", refDollar.getString("title"));
+ }
+
+ /**
+ * Referenced entry may declare its own {@code locale}; recursive {@code getTag} must receive {@code refLocale}
+ * (not the parent entry locale) so nested plain objects use the correct path segment, and nested refs in arrays
+ * without {@code locale} fall back to that reference locale (not the top-level entry locale).
+ */
+ @Test
+ public void referenceInArrayPassesRefLocaleToNestedGetTag() {
+ JSONObject nested = new JSONObject();
+ nested.put("name", "Nested");
+ JSONObject subRef = new JSONObject();
+ subRef.put("_content_type_uid", "child_ct");
+ subRef.put("uid", "c1");
+ subRef.put("x", "v");
+ JSONObject ref = new JSONObject();
+ ref.put("_content_type_uid", "author_ct");
+ ref.put("uid", "refuid");
+ ref.put("locale", "fr-fr");
+ ref.put("profile", nested);
+ ref.put("nested_refs", new JSONArray().put(subRef));
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("authors", new JSONArray().put(ref));
+ Utils.addEditableTags(entry, "post", false, "en-us", null);
+ Assert.assertEquals("data-cslp=author_ct.refuid.fr-fr.profile.name", nested.getJSONObject("$").getString("name"));
+ Assert.assertEquals("data-cslp=child_ct.c1.fr-fr.x", subRef.getJSONObject("$").getString("x"));
+ }
+
+ @Test
+ public void variantDirectFieldAppendsVariantToUidSegment() {
+ JSONObject applied = new JSONObject();
+ applied.put("title", "varA");
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "eu1");
+ entry.put("_applied_variants", applied);
+ entry.put("title", "T");
+ Utils.addEditableTags(entry, "blog", false, "en-us", null);
+ String tag = entry.getJSONObject("$").getString("title");
+ Assert.assertTrue(tag.startsWith("data-cslp=v2:"));
+ Assert.assertTrue(tag.contains("blog.eu1_varA.en-us.title"));
+ }
+
+ @Test
+ public void appliedVariantsFromSystem() {
+ JSONObject applied = new JSONObject();
+ applied.put("field1", "v1");
+ JSONObject system = new JSONObject();
+ system.put("applied_variants", applied);
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "u1");
+ entry.put("system", system);
+ entry.put("field1", "x");
+ Utils.addEditableTags(entry, "ct", false, "en-us", null);
+ String tag = entry.getJSONObject("$").getString("field1");
+ Assert.assertTrue(tag.contains("v2:"));
+ Assert.assertTrue(tag.contains("u1_v1"));
+ }
+
+ @Test
+ public void parentVariantisedPathInheritance() {
+ JSONObject applied = new JSONObject();
+ applied.put("parent", "pv");
+ JSONObject entry = new JSONObject();
+ entry.put("uid", "e1");
+ entry.put("_applied_variants", applied);
+ entry.put("parent", new JSONObject().put("child", "val"));
+ Utils.addEditableTags(entry, "ct", false, "en-us", null);
+ JSONObject parentObj = entry.getJSONObject("parent");
+ String childTag = parentObj.getJSONObject("$").getString("child");
+ Assert.assertTrue(childTag.startsWith("data-cslp=v2:"));
+ Assert.assertTrue(childTag.contains("e1_pv"));
+ }
+
+ @Test
+ public void addTagsAliasMatchesAddEditableTags() {
+ JSONObject a = new JSONObject();
+ a.put("uid", "1");
+ a.put("t", "x");
+ JSONObject b = new JSONObject();
+ b.put("uid", "1");
+ b.put("t", "x");
+ Utils.addEditableTags(a, "c", false, "en-us", null);
+ Utils.addTags(b, "c", false, "en-us", null);
+ Assert.assertEquals(a.getJSONObject("$").toString(), b.getJSONObject("$").toString());
+ }
+}