org.apache.maven.plugins
maven-compiler-plugin
diff --git a/scripts/download-regions.sh b/scripts/download-regions.sh
new file mode 100755
index 0000000..54081ef
--- /dev/null
+++ b/scripts/download-regions.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+#
+# Downloads the Contentstack regions registry from the official source and
+# saves it to src/main/resources/regions.json.
+#
+# Invoked automatically by Maven on the generate-resources phase, and
+# manually via: bash scripts/download-regions.sh
+#
+# Requires: curl (preferred) or wget as fallback
+
+set -euo pipefail
+
+URL="https://artifacts.contentstack.com/regions.json"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEST="${SCRIPT_DIR}/../src/main/resources/regions.json"
+DIR="$(dirname "$DEST")"
+
+mkdir -p "$DIR"
+
+data=""
+
+# --- Attempt 1: curl (preferred) --------------------------------------------
+if command -v curl &>/dev/null; then
+ data=$(curl --silent --fail --location --max-time 30 "$URL") || data=""
+fi
+
+# --- Attempt 2: wget fallback -----------------------------------------------
+if [[ -z "$data" ]] && command -v wget &>/dev/null; then
+ data=$(wget --quiet --timeout=30 -O - "$URL") || data=""
+fi
+
+# --- Validate and write ------------------------------------------------------
+if [[ -z "$data" ]]; then
+ echo "contentstack/utils: Warning — could not download regions.json." >&2
+ echo " The SDK will attempt to download it at runtime on first use." >&2
+ exit 0 # non-fatal: runtime fallback in Endpoint.java handles it
+fi
+
+# Basic validation: must contain a "regions" key
+if ! echo "$data" | grep -q '"regions"'; then
+ echo "contentstack/utils: Warning — downloaded data is not valid regions.json." >&2
+ exit 0
+fi
+
+echo "$data" > "$DEST"
+
+region_count=$(echo "$data" | grep -o '"id"' | wc -l | tr -d ' ')
+echo "contentstack/utils: regions.json downloaded (${region_count} regions)."
diff --git a/src/main/java/com/contentstack/utils/Endpoint.java b/src/main/java/com/contentstack/utils/Endpoint.java
new file mode 100644
index 0000000..d0085ad
--- /dev/null
+++ b/src/main/java/com/contentstack/utils/Endpoint.java
@@ -0,0 +1,200 @@
+package com.contentstack.utils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Resolves Contentstack API endpoints for any region and service.
+ *
+ * Endpoint data is loaded from the bundled {@code regions.json} resource.
+ * The parsed result is cached for the lifetime of the JVM process.
+ * If the bundled file is absent, a live download from
+ * {@code https://artifacts.contentstack.com/regions.json} is attempted as a fallback.
+ *
+ *
{@code
+ * // Get a specific service URL
+ * String cdnUrl = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
+ * // → "https://eu-cdn.contentstack.com"
+ *
+ * // Get the host without the https:// scheme
+ * String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
+ * // → "eu-cdn.contentstack.com"
+ *
+ * // Get all endpoints for a region
+ * Map all = Endpoint.getContentstackEndpoint("eu");
+ * }
+ */
+public class Endpoint {
+
+ private static final String REGIONS_URL = "https://artifacts.contentstack.com/regions.json";
+ private static final String REGIONS_RESOURCE = "regions.json";
+
+ private static JSONArray regionsData = null;
+
+ private Endpoint() {}
+
+ /**
+ * Returns the URL for a specific service in the given region.
+ *
+ * @param region canonical region ID ({@code na}, {@code eu}, {@code au}, {@code azure-na},
+ * {@code azure-eu}, {@code gcp-na}, {@code gcp-eu}) or any accepted alias.
+ * Case-insensitive; {@code -} and {@code _} separators are equivalent.
+ * @param service service name (e.g. {@code contentDelivery}, {@code contentManagement})
+ * @return full URL including {@code https://}
+ * @throws IllegalArgumentException if region or service is unknown, or region is empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static String getContentstackEndpoint(String region, String service) {
+ return getContentstackEndpoint(region, service, false);
+ }
+
+ /**
+ * Returns the URL for a specific service in the given region.
+ *
+ * @param region canonical region ID or alias
+ * @param service service name
+ * @param omitHttps when {@code true}, strips {@code https://} from the result
+ * @return URL string, with or without scheme depending on {@code omitHttps}
+ * @throws IllegalArgumentException if region or service is unknown, or region is empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static String getContentstackEndpoint(String region, String service, boolean omitHttps) {
+ if (service == null || service.trim().isEmpty()) {
+ throw new IllegalArgumentException("Service must not be empty. Use getContentstackEndpoint(region) to get all endpoints.");
+ }
+ JSONObject regionRow = resolveRegion(region);
+ JSONObject endpoints = regionRow.getJSONObject("endpoints");
+ if (!endpoints.has(service)) {
+ throw new IllegalArgumentException("Service \"" + service + "\" not found for region \"" + regionRow.getString("id") + "\"");
+ }
+ String url = endpoints.getString(service);
+ return omitHttps ? stripHttps(url) : url;
+ }
+
+ /**
+ * Returns all endpoint URLs for the given region as an ordered map.
+ *
+ * @param region canonical region ID or alias
+ * @return map of service name → URL (includes {@code https://})
+ * @throws IllegalArgumentException if region is unknown or empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static Map getContentstackEndpoint(String region) {
+ return getContentstackEndpoint(region, false);
+ }
+
+ /**
+ * Returns all endpoint URLs for the given region as an ordered map.
+ *
+ * @param region canonical region ID or alias
+ * @param omitHttps when {@code true}, strips {@code https://} from every URL
+ * @return map of service name → URL
+ * @throws IllegalArgumentException if region is unknown or empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static Map getContentstackEndpoint(String region, boolean omitHttps) {
+ JSONObject regionRow = resolveRegion(region);
+ JSONObject endpoints = regionRow.getJSONObject("endpoints");
+ Map result = new LinkedHashMap<>();
+ for (String serviceName : endpoints.keySet()) {
+ String url = endpoints.getString(serviceName);
+ result.put(serviceName, omitHttps ? stripHttps(url) : url);
+ }
+ return result;
+ }
+
+ // ── internal ──────────────────────────────────────────────────────────────
+
+ private static JSONObject resolveRegion(String region) {
+ if (region == null || region.trim().isEmpty()) {
+ throw new IllegalArgumentException("Empty region provided. Please provide a valid region.");
+ }
+ JSONArray regions = loadRegions();
+ String normalized = region.trim().toLowerCase().replace('_', '-');
+
+ // First pass: exact match on region id field
+ for (int i = 0; i < regions.length(); i++) {
+ JSONObject row = regions.getJSONObject(i);
+ if (row.getString("id").equals(normalized)) {
+ return row;
+ }
+ }
+
+ // Second pass: match on accepted alternate names (case-insensitive, normalised separators)
+ for (int i = 0; i < regions.length(); i++) {
+ JSONObject row = regions.getJSONObject(i);
+ JSONArray alternateNames = row.getJSONArray("alias");
+ for (int j = 0; j < alternateNames.length(); j++) {
+ String alternateName = alternateNames.getString(j).toLowerCase().replace('_', '-');
+ if (alternateName.equals(normalized)) {
+ return row;
+ }
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid region: " + region);
+ }
+
+ private static synchronized JSONArray loadRegions() {
+ if (regionsData != null) {
+ return regionsData;
+ }
+
+ // Try bundled classpath resource first
+ InputStream is = Endpoint.class.getClassLoader().getResourceAsStream(REGIONS_RESOURCE);
+ if (is != null) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String json = reader.lines().collect(Collectors.joining("\n"));
+ regionsData = new JSONObject(json).getJSONArray("regions");
+ return regionsData;
+ } catch (Exception e) {
+ // fall through to live download
+ }
+ }
+
+ // Fallback: download from Contentstack
+ try {
+ String json = downloadRegions();
+ regionsData = new JSONObject(json).getJSONArray("regions");
+ return regionsData;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "contentstack/utils: regions.json not found and could not be downloaded. " +
+ "Ensure the JAR was built correctly or network access is available.", e);
+ }
+ }
+
+ private static String downloadRegions() throws IOException {
+ URL url = new URL(REGIONS_URL);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(10000);
+ conn.setReadTimeout(10000);
+ try (InputStream is = conn.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ return reader.lines().collect(Collectors.joining("\n"));
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ private static String stripHttps(String url) {
+ return url.replaceAll("^https?://", "");
+ }
+
+ /** Clears the in-memory cache. For use in tests only. */
+ static void resetCache() {
+ regionsData = null;
+ }
+}
diff --git a/src/main/java/com/contentstack/utils/Utils.java b/src/main/java/com/contentstack/utils/Utils.java
index 6f4f810..6b526e6 100644
--- a/src/main/java/com/contentstack/utils/Utils.java
+++ b/src/main/java/com/contentstack/utils/Utils.java
@@ -559,4 +559,52 @@ private static void updateChildrenArray(JSONArray childrenArray, Map getContentstackEndpoint(String region) {
+ return Endpoint.getContentstackEndpoint(region);
+ }
+
+ /**
+ * Returns all endpoint URLs for the given region.
+ * Proxy for {@link Endpoint#getContentstackEndpoint(String, boolean)}.
+ *
+ * @param region region ID or alias
+ * @param omitHttps when {@code true}, strips {@code https://} from every URL
+ * @return map of service name → URL
+ */
+ public static Map getContentstackEndpoint(String region, boolean omitHttps) {
+ return Endpoint.getContentstackEndpoint(region, omitHttps);
+ }
}
diff --git a/src/test/java/com/contentstack/utils/EndpointTest.java b/src/test/java/com/contentstack/utils/EndpointTest.java
new file mode 100644
index 0000000..30ed333
--- /dev/null
+++ b/src/test/java/com/contentstack/utils/EndpointTest.java
@@ -0,0 +1,353 @@
+package com.contentstack.utils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class EndpointTest {
+
+ @Before
+ @After
+ public void resetCache() {
+ Endpoint.resetCache();
+ }
+
+ // ── canonical IDs ──────────────────────────────────────────────────────
+
+ @Test
+ public void testNaContentDelivery() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentDelivery"));
+ }
+
+ @Test
+ public void testNaContentManagement() {
+ assertEquals("https://api.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentManagement"));
+ }
+
+ @Test
+ public void testEuContentDelivery() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery"));
+ }
+
+ @Test
+ public void testEuContentManagement() {
+ assertEquals("https://eu-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentManagement"));
+ }
+
+ @Test
+ public void testAuContentDelivery() {
+ assertEquals("https://au-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("au", "contentDelivery"));
+ }
+
+ @Test
+ public void testAzureNaContentDelivery() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAzureEuContentDelivery() {
+ assertEquals("https://azure-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-eu", "contentDelivery"));
+ }
+
+ @Test
+ public void testGcpNaContentDelivery() {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testGcpEuContentDelivery() {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery"));
+ }
+
+ // ── region aliases ─────────────────────────────────────────────────────
+
+ @Test
+ public void testAliasUs() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("us", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAwsNaDash() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("aws-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAwsNaUnderscore() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("aws_na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasUppercaseNA() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("NA", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasUppercaseUS() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("US", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasUppercaseAWSNA() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("AWS-NA", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAwsEu() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("aws-eu", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasEuUppercase() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("EU", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAzureNaUnderscore() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure_na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAzureEuUppercase() {
+ assertEquals("https://azure-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("AZURE-EU", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasGcpNaUppercase() {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("GCP-NA", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasGcpEuUnderscore() {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp_eu", "contentDelivery"));
+ }
+
+ // ── omitHttps ──────────────────────────────────────────────────────────
+
+ @Test
+ public void testOmitHttpsFalseReturnsScheme() {
+ String url = Endpoint.getContentstackEndpoint("na", "contentDelivery", false);
+ assertTrue(url.startsWith("https://"));
+ }
+
+ @Test
+ public void testOmitHttpsTrueStripsScheme() {
+ String host = Endpoint.getContentstackEndpoint("na", "contentDelivery", true);
+ assertFalse(host.startsWith("https://"));
+ assertEquals("cdn.contentstack.io", host);
+ }
+
+ @Test
+ public void testOmitHttpsEu() {
+ assertEquals("eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery", true));
+ }
+
+ @Test
+ public void testOmitHttpsAzureNa() {
+ assertEquals("azure-na-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentManagement", true));
+ }
+
+ @Test
+ public void testOmitHttpsGcpEu() {
+ assertEquals("gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery", true));
+ }
+
+ // ── various service keys ───────────────────────────────────────────────
+
+ @Test
+ public void testNaAuth() {
+ assertEquals("https://auth-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "auth"));
+ }
+
+ @Test
+ public void testNaGraphqlDelivery() {
+ assertEquals("https://graphql.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "graphqlDelivery"));
+ }
+
+ @Test
+ public void testNaPreview() {
+ assertEquals("https://rest-preview.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "preview"));
+ }
+
+ @Test
+ public void testNaApplication() {
+ assertEquals("https://app.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "application"));
+ }
+
+ @Test
+ public void testNaAssetManagement() {
+ assertEquals("https://am-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "assetManagement"));
+ }
+
+ @Test
+ public void testEuAutomate() {
+ assertEquals("https://eu-prod-automations-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "automate"));
+ }
+
+ // ── all endpoints map ──────────────────────────────────────────────────
+
+ @Test
+ public void testGetAllEndpointsForNa() {
+ Map endpoints = Endpoint.getContentstackEndpoint("na");
+ assertNotNull(endpoints);
+ assertFalse(endpoints.isEmpty());
+ assertEquals("https://cdn.contentstack.io", endpoints.get("contentDelivery"));
+ assertEquals("https://api.contentstack.io", endpoints.get("contentManagement"));
+ assertEquals("https://auth-api.contentstack.com", endpoints.get("auth"));
+ }
+
+ @Test
+ public void testGetAllEndpointsForEu() {
+ Map endpoints = Endpoint.getContentstackEndpoint("eu");
+ assertNotNull(endpoints);
+ assertEquals("https://eu-cdn.contentstack.com", endpoints.get("contentDelivery"));
+ assertEquals("https://eu-api.contentstack.com", endpoints.get("contentManagement"));
+ }
+
+ @Test
+ public void testGetAllEndpointsOmitHttps() {
+ Map endpoints = Endpoint.getContentstackEndpoint("eu", true);
+ assertEquals("eu-cdn.contentstack.com", endpoints.get("contentDelivery"));
+ assertEquals("eu-api.contentstack.com", endpoints.get("contentManagement"));
+ for (String value : endpoints.values()) {
+ assertFalse("No URL should start with https://", value.startsWith("https://"));
+ }
+ }
+
+ @Test
+ public void testGetAllEndpointsWithHttps() {
+ Map endpoints = Endpoint.getContentstackEndpoint("na", false);
+ for (String value : endpoints.values()) {
+ assertTrue("All URLs should start with https://", value.startsWith("https://"));
+ }
+ }
+
+ // ── Utils proxy methods ────────────────────────────────────────────────
+
+ @Test
+ public void testUtilsProxyGetServiceUrl() {
+ assertEquals("https://cdn.contentstack.io",
+ Utils.getContentstackEndpoint("na", "contentDelivery"));
+ }
+
+ @Test
+ public void testUtilsProxyGetServiceUrlOmitHttps() {
+ assertEquals("cdn.contentstack.io",
+ Utils.getContentstackEndpoint("na", "contentDelivery", true));
+ }
+
+ @Test
+ public void testUtilsProxyGetAllEndpoints() {
+ Map endpoints = Utils.getContentstackEndpoint("eu");
+ assertNotNull(endpoints);
+ assertEquals("https://eu-cdn.contentstack.com", endpoints.get("contentDelivery"));
+ }
+
+ @Test
+ public void testUtilsProxyGetAllEndpointsOmitHttps() {
+ Map endpoints = Utils.getContentstackEndpoint("eu", true);
+ assertEquals("eu-cdn.contentstack.com", endpoints.get("contentDelivery"));
+ }
+
+ // ── error cases ────────────────────────────────────────────────────────
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEmptyRegionThrows() {
+ Endpoint.getContentstackEndpoint("", "contentDelivery");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullRegionThrows() {
+ Endpoint.getContentstackEndpoint(null, "contentDelivery");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownRegionThrows() {
+ Endpoint.getContentstackEndpoint("asia-pacific", "contentDelivery");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownServiceThrows() {
+ Endpoint.getContentstackEndpoint("na", "cms");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEmptyServiceThrows() {
+ Endpoint.getContentstackEndpoint("na", "", false);
+ }
+
+ @Test
+ public void testUnknownRegionErrorMessage() {
+ try {
+ Endpoint.getContentstackEndpoint("invalid-region", "contentDelivery");
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("Invalid region"));
+ assertTrue(e.getMessage().contains("invalid-region"));
+ }
+ }
+
+ @Test
+ public void testUnknownServiceErrorMessage() {
+ try {
+ Endpoint.getContentstackEndpoint("na", "unknownService");
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("unknownService"));
+ }
+ }
+
+ @Test
+ public void testEmptyRegionErrorMessage() {
+ try {
+ Endpoint.getContentstackEndpoint("", "contentDelivery");
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().toLowerCase().contains("empty region"));
+ }
+ }
+
+ // ── caching ────────────────────────────────────────────────────────────
+
+ @Test
+ public void testCacheIsUsedOnSubsequentCalls() {
+ // First call loads and caches
+ String first = Endpoint.getContentstackEndpoint("na", "contentDelivery");
+ // Second call uses cache — result must be identical
+ String second = Endpoint.getContentstackEndpoint("na", "contentDelivery");
+ assertEquals(first, second);
+ }
+}
From c6793e3eda214e9009c8a4b7715106a67cac9ff6 Mon Sep 17 00:00:00 2001
From: reeshika-h
Date: Thu, 4 Jun 2026 14:41:43 +0530
Subject: [PATCH 3/6] chore: update version to 1.6.0 and add changelog entries
for dynamic endpoint resolution features
---
CHANGELOG.md | 8 ++++++++
pom.xml | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 163bdc8..de32ce9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
A brief description of what changes project contains
+## Jun 15, 2026
+
+#### v1.6.0
+
+- Feature: Dynamic endpoint resolution via `Endpoint.getContentstackEndpoint()` backed by the Contentstack Regions Registry
+- Feature: `Utils.getContentstackEndpoint()` proxy for backward-compatible access
+- Feature: `regions.json` auto-downloaded at build time via `scripts/download-regions.sh` with runtime fallback
+
## Apr 30, 2026
#### v1.5.1
diff --git a/pom.xml b/pom.xml
index 602e1d2..fc9a74d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.contentstack.sdk
utils
- 1.5.1
+ 1.6.0
jar
Contentstack-utils
Java Utils SDK for Contentstack Content Delivery API, Contentstack is a headless CMS
From b3a69cb3a9b0b4f3755f2e901aee77a4e7a33a89 Mon Sep 17 00:00:00 2001
From: reeshika-h
Date: Thu, 4 Jun 2026 15:20:49 +0530
Subject: [PATCH 4/6] fix: enhance region loading with live download fallback
and improved error handling
---
docs/endpoint-resolution.md | 375 ------------------
.../java/com/contentstack/utils/Endpoint.java | 28 +-
2 files changed, 15 insertions(+), 388 deletions(-)
delete mode 100644 docs/endpoint-resolution.md
diff --git a/docs/endpoint-resolution.md b/docs/endpoint-resolution.md
deleted file mode 100644
index f774dcc..0000000
--- a/docs/endpoint-resolution.md
+++ /dev/null
@@ -1,375 +0,0 @@
-# Region Endpoint Integration Specification
-
-## Overview
-
-Contentstack services are deployed across multiple cloud providers and geographic regions. SDKs must resolve service endpoints dynamically using the Contentstack Regions Registry rather than relying on hardcoded URLs.
-
-This ensures:
-
-- Consistent endpoint resolution across all SDKs
-- Automatic support for newly introduced regions
-- Automatic support for newly introduced services
-- Single source of truth for endpoint configuration
-- Elimination of region-specific host logic inside SDKs
-
----
-
-## Regions Registry
-
-All endpoint information is maintained in the Contentstack Regions Registry.
-
-### Registry URL
-
-```text
-https://artifacts.contentstack.com/regions.json
-```
-
-The registry contains:
-
-- Region identifiers
-- Region aliases
-- Default region information
-- Service endpoint mappings
-
-### Example
-
-```json
-{
- "regions": [
- {
- "id": "na",
- "alias": ["us", "aws-na"],
- "isDefault": true,
- "endpoints": {
- "contentDelivery": "https://cdn.contentstack.io",
- "contentManagement": "https://api.contentstack.io"
- }
- }
- ]
-}
-```
-
----
-
-## Endpoint Resolution Contract
-
-All SDKs must expose a public endpoint resolution API.
-
-```text
-getContentstackEndpoint(
- region,
- service,
- omitProtocol = false
-)
-```
-
-### Parameters
-
-| Parameter | Description |
-|-----------|-------------|
-| `region` | Region identifier or alias |
-| `service` | Service name |
-| `omitProtocol` | Removes protocol prefix from returned URL |
-
-### Returns
-
-- Service URL when a service is specified
-- Complete endpoint map when service is omitted
-
----
-
-## Region Resolution Rules
-
-Region matching must:
-
-- Ignore case
-- Trim whitespace
-- Support aliases
-- Support both dash (`-`) and underscore (`_`) variants where defined
-
-### Examples
-
-| Input | Resolved Region |
-|--------|----------------|
-| `na` | `na` |
-| `us` | `na` |
-| `aws-na` | `na` |
-| `AWS_NA` | `na` |
-| `eu` | `eu` |
-| `azure-na` | `azure-na` |
-| `gcp-eu` | `gcp-eu` |
-
-If no region is found:
-
-```text
-Invalid region
-```
-
----
-
-## Service Resolution Rules
-
-SDKs must:
-
-1. Locate the resolved region.
-2. Locate the service name within the region endpoints.
-3. Return the endpoint URL.
-
-### Example
-
-```text
-Region: eu
-Service: contentDelivery
-
-Result:
-https://eu-cdn.contentstack.com
-```
-
-If the service is unavailable:
-
-```text
-Service not found
-```
-
----
-
-## Supported Service Names
-
-- `contentDelivery`
-- `contentManagement`
-- `graphqlDelivery`
-- `graphqlPreview`
-- `preview`
-- `auth`
-- `application`
-- `images`
-- `assets`
-- `automate`
-- `launch`
-- `developerHub`
-- `brandKit`
-- `genAI`
-- `personalizeManagement`
-- `personalizeEdge`
-- `composableStudio`
-- `assetManagement`
-
-SDKs must not hardcode this list. The registry remains the source of truth.
-
----
-
-## Registry Loading Requirements
-
-Recommended priority:
-
-1. In-memory cache
-2. Local registry file
-3. Registry download fallback
-
-Examples:
-
-- JavaScript SDK: Build-time download
-- PHP SDK: Install-time download with runtime fallback
-- Java SDK: Build-time download via Maven (`generate-resources` phase) with runtime fallback
-
----
-
-## SDK Integration Requirements
-
-```text
-Resolve Region
- ↓
-Resolve contentDelivery Endpoint
- ↓
-Configure SDK Host
- ↓
-Execute API Requests
-```
-
-The SDK host must be configured using the resolved endpoint rather than a hardcoded hostname.
-
----
-
-## Error Handling
-
-| Scenario | Error |
-|-----------|--------|
-| Empty Region | Empty region provided |
-| Invalid Region | Invalid region |
-| Invalid Service | Service not found |
-| Registry Unavailable | Unable to load regions registry |
-
----
-
-## Caching Requirements
-
-Goals:
-
-- Avoid repeated disk reads
-- Avoid repeated network requests
-- Improve endpoint lookup performance
-
-Cache implementation is SDK-specific.
-
----
-
-## Future Compatibility
-
-SDK implementations must not:
-
-- Hardcode endpoint URLs
-- Hardcode region mappings
-- Hardcode service name mappings
-
-All endpoint information must originate from the Regions Registry.
-
----
-
-## SDK Examples
-
-### Java
-
-```java
-import com.contentstack.utils.Endpoint;
-
-// Get a specific service URL
-String cdaUrl = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
-// → "https://eu-cdn.contentstack.com"
-
-// Get the host without the https:// scheme
-String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
-// → "eu-cdn.contentstack.com"
-
-// Get all endpoints for a region
-Map all = Endpoint.getContentstackEndpoint("eu");
-// → { "contentDelivery": "https://eu-cdn.contentstack.com", ... }
-
-// Get all endpoints without the scheme
-Map hosts = Endpoint.getContentstackEndpoint("eu", true);
-```
-
-#### Region aliases
-
-```java
-// All of the following resolve to the same NA region
-Endpoint.getContentstackEndpoint("na", "contentDelivery"); // → https://cdn.contentstack.io
-Endpoint.getContentstackEndpoint("us", "contentDelivery"); // → https://cdn.contentstack.io
-Endpoint.getContentstackEndpoint("aws-na", "contentDelivery"); // → https://cdn.contentstack.io
-Endpoint.getContentstackEndpoint("AWS_NA", "contentDelivery"); // → https://cdn.contentstack.io
-```
-
-#### Available via `Utils` (proxy)
-
-```java
-import com.contentstack.utils.Utils;
-
-// Identical result to Endpoint.getContentstackEndpoint()
-String url = Utils.getContentstackEndpoint("eu", "contentDelivery");
-String host = Utils.getContentstackEndpoint("eu", "contentDelivery", true);
-Map all = Utils.getContentstackEndpoint("eu");
-```
-
-#### Error handling
-
-```java
-try {
- Endpoint.getContentstackEndpoint("", "contentDelivery");
-} catch (IllegalArgumentException e) {
- // "Empty region provided. Please provide a valid region."
-}
-
-try {
- Endpoint.getContentstackEndpoint("invalid", "contentDelivery");
-} catch (IllegalArgumentException e) {
- // "Invalid region: invalid"
-}
-
-try {
- Endpoint.getContentstackEndpoint("na", "unknownService");
-} catch (IllegalArgumentException e) {
- // "Service \"unknownService\" not found for region \"na\""
-}
-```
-
-#### Integration with Delivery SDK
-
-```java
-import com.contentstack.sdk.Config;
-import com.contentstack.sdk.Contentstack;
-import com.contentstack.sdk.Query;
-import com.contentstack.sdk.QueryResult;
-import com.contentstack.sdk.QueryResultsCallBack;
-import com.contentstack.sdk.ResponseType;
-import com.contentstack.sdk.Stack;
-import com.contentstack.utils.Endpoint;
-
-// 1. Resolve the host for the chosen region (omit https:// for setHost)
-String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
-// → "eu-cdn.contentstack.com"
-
-// 2. Wire it into a Config and create the Stack
-Config config = new Config();
-config.setHost(host);
-
-Stack stack = Contentstack.stack("", "", "", config);
-
-// 3. Fetch entries — all requests now go to the EU CDN
-Query query = stack.contentType("blog").query();
-query.find(new QueryResultsCallBack() {
- @Override
- public void onCompletion(ResponseType responseType, QueryResult queryResult, Error error) {
- if (error != null) {
- System.err.println(error.getErrorMessage());
- return;
- }
- queryResult.getResultObjects().forEach(entry ->
- System.out.println(entry.getTitle()));
- }
-});
-```
-
-Change one string to switch regions — everything else stays the same:
-
-```java
-// NA
-String host = Endpoint.getContentstackEndpoint("na", "contentDelivery", true);
-// → "cdn.contentstack.io"
-
-// EU
-String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
-// → "eu-cdn.contentstack.com"
-
-// Azure NA
-String host = Endpoint.getContentstackEndpoint("azure-na", "contentDelivery", true);
-// → "azure-na-cdn.contentstack.com"
-
-// GCP EU
-String host = Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery", true);
-// → "gcp-eu-cdn.contentstack.com"
-```
-
-Read region from environment variable (recommended for production):
-
-```java
-String region = System.getenv().getOrDefault("CONTENTSTACK_REGION", "na");
-
-Config config = new Config();
-config.setHost(Endpoint.getContentstackEndpoint(region, "contentDelivery", true));
-
-Stack stack = Contentstack.stack(
- System.getenv("CONTENTSTACK_API_KEY"),
- System.getenv("CONTENTSTACK_DELIVERY_TOKEN"),
- System.getenv("CONTENTSTACK_ENVIRONMENT"),
- config
-);
-```
-
-#### Refreshing `regions.json`
-
-```bash
-# Runs automatically on every Maven build (generate-resources phase)
-mvn generate-resources
-
-# Or refresh manually
-bash scripts/download-regions.sh
-```
diff --git a/src/main/java/com/contentstack/utils/Endpoint.java b/src/main/java/com/contentstack/utils/Endpoint.java
index d0085ad..fcde112 100644
--- a/src/main/java/com/contentstack/utils/Endpoint.java
+++ b/src/main/java/com/contentstack/utils/Endpoint.java
@@ -1,6 +1,7 @@
package com.contentstack.utils;
import org.json.JSONArray;
+import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
@@ -151,28 +152,29 @@ private static synchronized JSONArray loadRegions() {
return regionsData;
}
- // Try bundled classpath resource first
+ // Try live download first so users always get the latest regions
+ try {
+ String json = downloadRegions();
+ regionsData = new JSONObject(json).getJSONArray("regions");
+ return regionsData;
+ } catch (IOException | JSONException ignored) {
+ // network unavailable — fall through to bundled fallback
+ }
+
+ // Fallback: bundled regions.json packaged in the JAR (offline safety net)
InputStream is = Endpoint.class.getClassLoader().getResourceAsStream(REGIONS_RESOURCE);
if (is != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String json = reader.lines().collect(Collectors.joining("\n"));
regionsData = new JSONObject(json).getJSONArray("regions");
return regionsData;
- } catch (Exception e) {
- // fall through to live download
+ } catch (IOException | JSONException ignored) {
+ // fall through to error
}
}
- // Fallback: download from Contentstack
- try {
- String json = downloadRegions();
- regionsData = new JSONObject(json).getJSONArray("regions");
- return regionsData;
- } catch (Exception e) {
- throw new RuntimeException(
- "contentstack/utils: regions.json not found and could not be downloaded. " +
- "Ensure the JAR was built correctly or network access is available.", e);
- }
+ throw new RuntimeException(
+ "contentstack/utils: could not load regions — network unavailable and no bundled fallback found.");
}
private static String downloadRegions() throws IOException {
From 065545e324a16e8321b6e1ff803f821c911e5c20 Mon Sep 17 00:00:00 2001
From: reeshika-h
Date: Wed, 10 Jun 2026 12:40:57 +0530
Subject: [PATCH 5/6] snyk fixes
---
CHANGELOG.md | 6 ++++++
pom.xml | 4 ++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 163bdc8..1ca6cab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
A brief description of what changes project contains
+## Jun 10, 2026
+
+#### v1.5.2
+
+- Snyk fixes
+
## Apr 30, 2026
#### v1.5.1
diff --git a/pom.xml b/pom.xml
index aead79d..759c146 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.contentstack.sdk
utils
- 1.5.1
+ 1.5.2
jar
Contentstack-utils
Java Utils SDK for Contentstack Content Delivery API, Contentstack is a headless CMS
@@ -28,7 +28,7 @@
2.5.3
2.0.1.Final
20251224
- 7.0.7
+ 7.0.8
1.15.0
From 15771ccdf4ff1a829bce1147c54480233a8deb7d Mon Sep 17 00:00:00 2001
From: reeshika-h
Date: Fri, 26 Jun 2026 13:17:36 +0530
Subject: [PATCH 6/6] chore: update changelog date to Jun 29, 2026 and bump
jsoup and json versions
---
CHANGELOG.md | 2 +-
pom.xml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9531aee..a024e6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
A brief description of what changes project contains
-## Jun 15, 2026
+## Jun 29, 2026
#### v1.6.0
diff --git a/pom.xml b/pom.xml
index 40822ec..7c818fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,14 +20,14 @@
2.2.1
3.1.1
4.13.2
- 1.22.1
+ 1.22.2
1.1.1
3.3
1.5
0.8.0
2.5.3
2.0.1.Final
- 20251224
+ 20260522
7.0.8
1.15.0