diff --git a/components/org.wso2.micro.integrator.initializer/pom.xml b/components/org.wso2.micro.integrator.initializer/pom.xml
index 4c1059f4d6..662dd896d7 100755
--- a/components/org.wso2.micro.integrator.initializer/pom.xml
+++ b/components/org.wso2.micro.integrator.initializer/pom.xml
@@ -117,6 +117,10 @@
org.wso2.integration.transaction.counter
transaction-count-handler
+
+ org.wso2.orbit.com.nimbusds
+ nimbus-jose-jwt
+
@@ -138,6 +142,10 @@
org.wso2.micro.integrator.initializer.*;version="${project.version}"
+ com.nimbusds.jose;version="${nimbus-jose.orbit.imp.pkg.version}",
+ com.nimbusds.jose.jwk;version="${nimbus-jose.orbit.imp.pkg.version}",
+ com.nimbusds.jose.crypto;version="${nimbus-jose.orbit.imp.pkg.version}",
+ com.nimbusds.jwt;version="${nimbus-jose.orbit.imp.pkg.version}",
org.wso2.micro.integrator.core.*;version="${project.version}",
*;resolution:=optional
diff --git a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/Constants.java b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/Constants.java
index b0ee028397..c43caec69c 100644
--- a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/Constants.java
+++ b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/Constants.java
@@ -15,6 +15,35 @@ private Constants() {
public static final String DASHBOARD_CONFIG_MANAGEMENT_HOSTNAME = "dashboard_config.management_hostname";
public static final String DASHBOARD_CONFIG_MANAGEMENT_PORT = "dashboard_config.management_port";
+ // New ICP Configuration
+ public static final String ICP_CONFIG_URL = "icp_config.icp_url";
+ public static final String ICP_CONFIG_ENVIRONMENT = "icp_config.environment";
+ public static final String ICP_CONFIG_PROJECT = "icp_config.project";
+ public static final String ICP_CONFIG_COMPONENT = "icp_config.integration";
+ public static final String ICP_CONFIG_ENABLED = "icp_config.enabled";
+ public static final String ICP_CONFIG_HEARTBEAT_INTERVAL = "icp_config.heartbeat_interval";
+
+ // JWT Configuration
+ public static final String ICP_JWT_ISSUER = "icp_config.jwt_issuer";
+ public static final String ICP_JWT_AUDIENCE = "icp_config.jwt_audience";
+ public static final String ICP_JWT_SCOPE = "icp_config.jwt_scope";
+ public static final String ICP_JWT_EXPIRY_SECONDS = "icp_config.jwt_expiry_seconds";
+ public static final String ICP_JWT_HMAC_SECRET = "icp_config.jwt_hmac_secret";
+
+ // Default ICP Configuration
+ public static final String DEFAULT_ENVIRONMENT = "production";
+ public static final String DEFAULT_PROJECT = "default";
+ public static final String DEFAULT_COMPONENT = "default";
+ public static final String DEFAULT_ICP_URL = "https://localhost:9445";
+
+ public static final String DEFAULT_JWT_ISSUER = "icp-runtime-jwt-issuer";
+ public static final String DEFAULT_JWT_AUDIENCE = "icp-server";
+ public static final String DEFAULT_JWT_SCOPE = "runtime_agent";
+ public static final long DEFAULT_JWT_EXPIRY_SECONDS = 3600;
+ public static final String DEFAULT_JWT_HMAC_SECRET = "default-secret-key-at-least-32-characters-long-for-hs256";
+ public static final String RUNTIME_TYPE_MI = "MI";
+ public static final String RUNTIME_STATUS_RUNNING = "RUNNING";
+
public static final String DEFAULT_GROUP_ID = "default";
public static final long DEFAULT_HEARTBEAT_INTERVAL = 5;
@@ -23,4 +52,8 @@ private Constants() {
public static final String COLON = ":";
public static final String HTTPS_PREFIX = "https://";
public static final String MANAGEMENT = "management";
+
+ // ICP Endpoints
+ public static final String ICP_HEARTBEAT_ENDPOINT = "/icp/heartbeat";
+ public static final String ICP_DELTA_HEARTBEAT_ENDPOINT = "/icp/deltaHeartbeat";
}
diff --git a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HMACJWTTokenGenerator.java b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HMACJWTTokenGenerator.java
new file mode 100644
index 0000000000..308bd281d8
--- /dev/null
+++ b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HMACJWTTokenGenerator.java
@@ -0,0 +1,56 @@
+package org.wso2.micro.integrator.initializer.dashboard;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class HMACJWTTokenGenerator {
+
+ private final String hmacSecret;
+
+ public HMACJWTTokenGenerator(String hmacSecret) {
+ if (hmacSecret == null || hmacSecret.getBytes(StandardCharsets.UTF_8).length < 32) {
+ throw new IllegalArgumentException("HMAC secret must be at least 256 bits (32 bytes)");
+ }
+ this.hmacSecret = hmacSecret;
+ }
+
+ /**
+ * Generate JWT Token with HMAC SHA256
+ */
+ public String generateToken(String issuer, String audience, String scope, long expiryTimeSeconds)
+ throws JOSEException {
+
+ // Calculate expiry
+ long expiryMillis = System.currentTimeMillis() + (expiryTimeSeconds * 1000);
+
+ // Build claims
+ JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .issuer(issuer)
+ .audience(audience)
+ .expirationTime(new Date(expiryMillis))
+ .issueTime(new Date())
+ .claim("scope", scope)
+ .build();
+
+ // Create HMAC signer
+ JWSSigner signer = new MACSigner(hmacSecret.getBytes(StandardCharsets.UTF_8));
+
+ // Create and sign JWT
+ SignedJWT signedJWT = new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.HS256).build(),
+ claimsSet
+ );
+
+ signedJWT.sign(signer);
+ return signedJWT.serialize();
+ }
+
+}
diff --git a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HeartBeatComponent.java b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HeartBeatComponent.java
index 9f5b7d031a..769c776e31 100644
--- a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HeartBeatComponent.java
+++ b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/HeartBeatComponent.java
@@ -17,9 +17,9 @@
*/
package org.wso2.micro.integrator.initializer.dashboard;
+import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
@@ -71,7 +71,13 @@ private HeartBeatComponent(){
private static final Map configs = ConfigParser.getParsedConfigs();
public static void invokeHeartbeatExecutorService() {
-
+ // Check if new ICP is configured
+ if (ICPHeartBeatComponent.isICPConfigured()) {
+ log.info("New ICP configuration detected. Starting ICP heartbeat service.");
+ ICPHeartBeatComponent.invokeICPHeartbeatExecutorService();
+ return;
+ }
+ // Fall back to old dashboard heartbeat
String heartbeatApiUrl = configs.get(DASHBOARD_CONFIG_URL) + "/heartbeat";
String groupId = getGroupId();
String nodeId = getNodeId();
@@ -174,14 +180,16 @@ private static String generateRandomId() {
}
public static boolean isDashboardConfigured() {
- return configs.get(DASHBOARD_CONFIG_URL) != null;
+ // Check for either old dashboard config or new ICP config
+ return configs.get(DASHBOARD_CONFIG_URL) != null || ICPHeartBeatComponent.isICPConfigured();
}
public static JsonObject getJsonResponse(CloseableHttpResponse response) {
String stringResponse = getStringResponse(response);
JsonObject responseObject = null;
try {
- responseObject = new JsonParser().parse(stringResponse).getAsJsonObject();
+ Gson gson = new Gson();
+ responseObject = gson.fromJson(stringResponse, JsonObject.class);
} catch (JsonParseException e) {
log.debug("Error occurred while parsing the heartbeat response.", e);
}
diff --git a/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/ICPHeartBeatComponent.java b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/ICPHeartBeatComponent.java
new file mode 100644
index 0000000000..ad96924cde
--- /dev/null
+++ b/components/org.wso2.micro.integrator.initializer/src/main/java/org/wso2/micro/integrator/initializer/dashboard/ICPHeartBeatComponent.java
@@ -0,0 +1,1426 @@
+/*
+ * Copyright (c) 2025, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
+ *
+ * WSO2 Inc. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * you may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.wso2.micro.integrator.initializer.dashboard;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
+import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContexts;
+import org.apache.http.util.EntityUtils;
+import org.apache.synapse.api.API;
+import org.apache.synapse.config.SynapseConfiguration;
+import org.apache.synapse.core.axis2.ProxyService;
+import org.apache.axis2.description.AxisService;
+import org.apache.axis2.description.Parameter;
+import org.apache.axis2.engine.AxisConfiguration;
+import org.wso2.carbon.inbound.endpoint.internal.http.api.ConfigurationLoader;
+import org.wso2.config.mapper.ConfigParser;
+import org.wso2.micro.application.deployer.CarbonApplication;
+import org.wso2.micro.application.deployer.config.Artifact;
+import org.wso2.micro.core.util.StringUtils;
+import org.wso2.micro.integrator.core.util.MicroIntegratorBaseUtils;
+import org.wso2.micro.integrator.initializer.deployment.application.deployer.CappDeployer;
+import org.wso2.micro.integrator.registry.MicroIntegratorRegistry;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.wso2.micro.integrator.initializer.dashboard.Constants.*;
+
+/**
+ * Manages heartbeats from micro integrator to new ICP with JWT authentication,
+ * delta heartbeat optimization, and comprehensive artifact metadata.
+ */
+public class ICPHeartBeatComponent {
+
+ private ICPHeartBeatComponent() {
+ }
+
+ private static final Log log = LogFactory.getLog(ICPHeartBeatComponent.class);
+ private static final Map configs = ConfigParser.getParsedConfigs();
+ private static String lastRuntimeHash = null;
+ private static String cachedJwtToken = null;
+ private static long jwtTokenExpiry = 0;
+ private static String runtimeIdFile = ".mi_runtime_id";
+ private static String runtimeId = null;
+
+ /**
+ * Lazily initializes and returns the runtime ID.
+ * Only initializes when ICP is configured.
+ *
+ * @return the runtime ID
+ * @throws IOException if there's an error reading or writing the runtime ID file
+ */
+ private static synchronized String getRuntimeId() throws IOException {
+ if (runtimeId == null) {
+ runtimeId = initRuntimeId();
+ }
+ return runtimeId;
+ }
+
+ /**
+ * Initializes the runtime ID from file or generates a new one.
+ *
+ * @return the runtime ID
+ * @throws IOException if there's an error reading or writing the runtime ID file
+ */
+ private static String initRuntimeId() throws IOException {
+ // Use current working directory for the runtime ID file
+ Path runtimeIdPath = Paths.get(runtimeIdFile);
+
+ // Check if file exists and read the UUID
+ if (Files.exists(runtimeIdPath)) {
+ String existingId = Files.readString(runtimeIdPath);
+ // Validate it's not empty and trim whitespace
+ String trimmedId = existingId.trim();
+ if (!trimmedId.isEmpty()) {
+ return trimmedId;
+ }
+ }
+
+ // Generate new UUID if file doesn't exist or is empty
+ String newRuntimeId = UUID.randomUUID().toString();
+ Files.writeString(runtimeIdPath, newRuntimeId);
+ return newRuntimeId;
+ }
+
+ /**
+ * Starts the ICP heartbeat executor service that sends periodic delta heartbeats
+ * and full heartbeats when requested by the ICP.
+ */
+ public static void invokeICPHeartbeatExecutorService() {
+ String icpUrl = getConfigValue(ICP_CONFIG_URL, DEFAULT_ICP_URL);
+ if (icpUrl == null) {
+ log.warn("ICP URL not configured. ICP heartbeat will not be started.");
+ return;
+ }
+ long interval = getInterval();
+ log.info("Starting ICP heartbeat service. Interval: " + interval + "s");
+
+ ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+ Runnable runnableTask = () -> {
+ try {
+ sendDeltaHeartbeat(icpUrl);
+ } catch (Exception e) {
+ log.error("Error occurred while sending delta heartbeat to ICP.", e);
+ }
+ };
+
+ // Initial delay of 5 seconds, then send at configured interval
+ scheduledExecutorService.scheduleAtFixedRate(runnableTask, 5, interval, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Sends a delta heartbeat to ICP with only runtime hash.
+ * If ICP detects a hash mismatch, it will respond with fullHeartbeatRequired=true.
+ */
+ private static void sendDeltaHeartbeat(String icpUrl) {
+ try {
+ // Build full payload to calculate hash
+ JsonObject fullPayload = buildFullHeartbeatPayload(false);
+ String currentHash = fullPayload.get("runtimeHash").getAsString();
+
+ // Build delta payload
+ JsonObject deltaPayload = new JsonObject();
+ deltaPayload.addProperty("runtime", getRuntimeId());
+ deltaPayload.addProperty("runtimeHash", currentHash);
+
+ // Create timestamp in Ballerina time:Utc format [seconds, nanoseconds_fraction]
+ deltaPayload.add("timestamp", createBallerinaTimestamp());
+
+ String deltaEndpoint = icpUrl + ICP_DELTA_HEARTBEAT_ENDPOINT;
+ JsonObject response = sendHeartbeatRequest(deltaEndpoint, deltaPayload);
+
+ if (response != null && response.has("fullHeartbeatRequired")
+ && response.get("fullHeartbeatRequired").getAsBoolean()) {
+ log.info("ICP requested full heartbeat. Sending full heartbeat with all artifacts.");
+ sendFullHeartbeat(icpUrl);
+ lastRuntimeHash = currentHash;
+ } else if (response != null && response.has("acknowledged")
+ && response.get("acknowledged").getAsBoolean()) {
+ log.debug("Delta heartbeat acknowledged by ICP.");
+ lastRuntimeHash = currentHash;
+ } else {
+ log.debug("Unexpected response from ICP delta heartbeat.");
+ }
+ } catch (Exception e) {
+ log.error("Error sending delta heartbeat to ICP.", e);
+ }
+ }
+
+ /**
+ * Sends a full heartbeat to ICP with all artifact metadata.
+ */
+ private static void sendFullHeartbeat(String icpUrl) {
+ try {
+ JsonObject fullPayload = buildFullHeartbeatPayload(true);
+ String fullEndpoint = icpUrl + ICP_HEARTBEAT_ENDPOINT;
+
+ JsonObject response = sendHeartbeatRequest(fullEndpoint, fullPayload);
+
+ if (response != null && response.has("acknowledged")
+ && response.get("acknowledged").getAsBoolean()) {
+ log.info("Full heartbeat acknowledged by ICP.");
+ } else {
+ log.error("Unexpected response from ICP full heartbeat." + response.toString());
+ }
+ } catch (Exception e) {
+ log.error("Error sending full heartbeat to ICP.", e);
+ }
+ }
+
+ /**
+ * Sends HTTP request to ICP with JWT authentication.
+ */
+ private static JsonObject sendHeartbeatRequest(String endpoint, JsonObject payload) {
+ try (CloseableHttpClient client = createHttpClient()) {
+ HttpPost httpPost = new HttpPost(endpoint);
+
+ // Add JWT token to Authorization header
+ String jwtToken = "";
+ try {
+ jwtToken = generateOrGetCachedJwtToken();
+ } catch (Exception e) {
+ log.error("Error while jwtToken creation ", e);
+ }
+ httpPost.setHeader("Authorization", "Bearer " + jwtToken);
+ httpPost.setHeader("Accept", HEADER_VALUE_APPLICATION_JSON);
+ httpPost.setHeader("Content-type", HEADER_VALUE_APPLICATION_JSON);
+
+ StringEntity entity = new StringEntity(payload.toString(), "UTF-8");
+ httpPost.setEntity(entity);
+
+ CloseableHttpResponse response = client.execute(httpPost);
+ return getJsonResponse(response);
+ } catch (Exception e) {
+ log.error("Error sending heartbeat request to ICP at: " + endpoint, e);
+ return null;
+ }
+ }
+
+ /**
+ * Builds the full heartbeat payload with all artifact metadata.
+ */
+ private static JsonObject buildFullHeartbeatPayload(boolean includeTimestamp) throws IOException {
+ JsonObject payload = new JsonObject();
+ payload.addProperty("runtime", getRuntimeId());
+ payload.addProperty("runtimeType", RUNTIME_TYPE_MI);
+ payload.addProperty("status", RUNTIME_STATUS_RUNNING);
+ payload.addProperty("environment", getEnvironment());
+ payload.addProperty("project", getProject());
+ payload.addProperty("component", getComponent());
+ payload.addProperty("version", getMicroIntegratorVersion());
+
+ // Node information
+ JsonObject nodeInfo = new JsonObject();
+ nodeInfo.addProperty("platformName", "WSO2 Micro Integrator");
+ nodeInfo.addProperty("platformVersion", getMicroIntegratorVersion());
+ nodeInfo.addProperty("platformHome", System.getProperty("carbon.home"));
+ nodeInfo.addProperty("osName", System.getProperty("os.name"));
+ nodeInfo.addProperty("osVersion", System.getProperty("os.version"));
+ nodeInfo.addProperty("osArch", System.getProperty("os.arch"));
+ nodeInfo.addProperty("javaVersion", System.getProperty("java.version"));
+ nodeInfo.addProperty("carbonHome", System.getProperty("carbon.home"));
+ nodeInfo.addProperty("javaVendor", System.getProperty("java.vendor"));
+
+ Runtime runtime = Runtime.getRuntime();
+ nodeInfo.addProperty("totalMemory", runtime.totalMemory());
+ nodeInfo.addProperty("freeMemory", runtime.freeMemory());
+ nodeInfo.addProperty("maxMemory", runtime.maxMemory());
+ nodeInfo.addProperty("usedMemory", runtime.totalMemory() - runtime.freeMemory());
+ payload.add("nodeInfo", nodeInfo);
+ // Artifacts
+ JsonObject artifacts = collectArtifacts();
+ payload.add("artifacts", artifacts);
+
+ // Hash (exclude timestamp for hash calculation)
+ String hash = calculateHash(payload);
+ payload.addProperty("runtimeHash", hash);
+
+ // Add timestamp if requested
+ if (includeTimestamp) {
+ payload.add("timestamp", createBallerinaTimestamp());
+ }
+
+ // Validate payload structure for ICP compatibility
+ payload = validateHeartbeatPayload(payload);
+
+ log.info("Full heartbeat payload: " + payload.toString());
+ return payload;
+ }
+
+ /**
+ * Collects comprehensive artifact metadata from all MI Management API resources
+ */
+ private static JsonObject collectArtifacts() {
+ JsonObject artifacts = new JsonObject();
+
+ try {
+ // Check if Synapse environment is available
+ if (MicroIntegratorBaseUtils.getSynapseEnvironment() == null) {
+ log.warn("Synapse environment is not available yet, returning empty artifacts");
+ return createEmptyArtifactsStructure();
+ }
+
+ SynapseConfiguration synapseConfig = MicroIntegratorBaseUtils.getSynapseEnvironment()
+ .getSynapseConfiguration();
+
+ if (synapseConfig == null) {
+ log.warn("Synapse configuration is not available yet, returning empty artifacts");
+ return createEmptyArtifactsStructure();
+ }
+
+ // Collect all artifact types as available in Management API
+
+ // 1. REST APIs
+ artifacts.add("apis", collectRestApis(synapseConfig));
+
+ // 2. Proxy Services
+ artifacts.add("proxyServices", collectProxyServices(synapseConfig));
+
+ // 3. Endpoints
+ artifacts.add("endpoints", collectEndpoints(synapseConfig));
+
+ // 4. Inbound Endpoints
+ artifacts.add("inboundEndpoints", collectInboundEndpoints(synapseConfig));
+
+ // 5. Sequences
+ artifacts.add("sequences", collectSequences(synapseConfig));
+
+ // 6. Tasks
+ artifacts.add("tasks", collectTasks(synapseConfig));
+
+ // 7. Templates
+ artifacts.add("templates", collectTemplates(synapseConfig));
+
+ // 8. Message Stores
+ artifacts.add("messageStores", collectMessageStores(synapseConfig));
+
+ // 9. Message Processors
+ artifacts.add("messageProcessors", collectMessageProcessors(synapseConfig));
+
+ // 10. Local Entries
+ artifacts.add("localEntries", collectLocalEntries(synapseConfig));
+
+ // 11. Data Services (requires separate access)
+ artifacts.add("dataServices", collectDataServices(synapseConfig));
+
+ // 12. Carbon Applications (requires separate access)
+ artifacts.add("carbonApps", collectCarbonApps());
+
+ // 13. Data Sources (requires separate access)
+ artifacts.add("dataSources", collectDataSources());
+
+ // 14. Connectors (requires separate access)
+ artifacts.add("connectors", collectConnectors(synapseConfig));
+
+ // 15. Registry Resources (requires separate access)
+ artifacts.add("registryResources", collectRegistryResources(synapseConfig));
+
+ // 16. Listeners (HTTP/HTTPS ports)
+ artifacts.add("listeners", collectListeners());
+
+ } catch (Exception e) {
+ log.error("Error collecting artifacts from MI configuration.", e);
+ return createEmptyArtifactsStructure();
+ }
+
+ return artifacts;
+ }
+
+ private static JsonObject createEmptyArtifactsStructure() {
+ JsonObject artifacts = new JsonObject();
+ artifacts.add("apis", new JsonArray());
+ artifacts.add("proxyServices", new JsonArray());
+ artifacts.add("endpoints", new JsonArray());
+ artifacts.add("inboundEndpoints", new JsonArray());
+ artifacts.add("sequences", new JsonArray());
+ artifacts.add("tasks", new JsonArray());
+ artifacts.add("templates", new JsonArray());
+ artifacts.add("messageStores", new JsonArray());
+ artifacts.add("messageProcessors", new JsonArray());
+ artifacts.add("localEntries", new JsonArray());
+ artifacts.add("dataServices", new JsonArray());
+ artifacts.add("carbonApps", new JsonArray());
+ artifacts.add("dataSources", new JsonArray());
+ artifacts.add("connectors", new JsonArray());
+ artifacts.add("registryResources", new JsonArray());
+ artifacts.add("listeners", new JsonArray());
+ artifacts.add("systemInfo", new JsonObject());
+ return artifacts;
+ }
+
+ /**
+ * Validates and sanitizes artifacts structure to ensure compatibility with ICP GraphQL API
+ */
+ private static JsonObject validateAndSanitizeArtifacts(JsonObject artifacts) {
+ try {
+ log.debug("Validating artifacts structure for ICP compatibility");
+
+ // Ensure all expected artifact arrays exist and are not null
+ String[] requiredArrays = {
+ "apis", "proxyServices", "endpoints", "inboundEndpoints", "sequences",
+ "tasks", "templates", "messageStores", "messageProcessors", "localEntries",
+ "dataServices", "carbonApps", "dataSources", "connectors", "registryResources", "listeners"
+ };
+
+ for (String arrayName : requiredArrays) {
+ if (!artifacts.has(arrayName) || artifacts.get(arrayName).isJsonNull()) {
+ log.warn("Missing or null artifact array: " + arrayName + ", adding empty array");
+ artifacts.add(arrayName, new JsonArray());
+ } else if (!artifacts.get(arrayName).isJsonArray()) {
+ log.warn("Invalid artifact array type for: " + arrayName + ", replacing with empty array");
+ artifacts.add(arrayName, new JsonArray());
+ }
+ }
+
+ // Ensure systemInfo exists and is a valid object
+ if (!artifacts.has("systemInfo") || artifacts.get("systemInfo").isJsonNull()) {
+ log.warn("Missing or null systemInfo, adding empty object");
+ artifacts.add("systemInfo", new JsonObject());
+ } else if (!artifacts.get("systemInfo").isJsonObject()) {
+ log.warn("Invalid systemInfo type, replacing with empty object");
+ artifacts.add("systemInfo", new JsonObject());
+ }
+
+ // Validate and sanitize individual artifact arrays
+ for (String arrayName : requiredArrays) {
+ JsonArray artifactArray = artifacts.getAsJsonArray(arrayName);
+ JsonArray sanitizedArray = new JsonArray();
+
+ for (int i = 0; i < artifactArray.size(); i++) {
+ try {
+ com.google.gson.JsonElement element = artifactArray.get(i);
+ if (element != null && element.isJsonObject()) {
+ JsonObject artifactObj = element.getAsJsonObject();
+
+ // Ensure each artifact has at least a name and type
+ if (!artifactObj.has("name") || artifactObj.get("name").isJsonNull()) {
+ artifactObj.addProperty("name", "UNKNOWN_" + arrayName.toUpperCase() + "_" + i);
+ }
+ if (!artifactObj.has("type") || artifactObj.get("type").isJsonNull()) {
+ // Create proper type name by removing trailing 's' and capitalizing
+ String typeName = arrayName.replaceAll("s$", "");
+ typeName = typeName.substring(0, 1).toUpperCase() + typeName.substring(1);
+ artifactObj.addProperty("type", typeName);
+ }
+
+ sanitizedArray.add(artifactObj);
+ } else {
+ log.warn("Invalid artifact object in " + arrayName + " at index " + i + ", skipping");
+ }
+ } catch (Exception e) {
+ log.warn("Error validating artifact in " + arrayName + " at index " + i + ", skipping", e);
+ }
+ }
+
+ artifacts.add(arrayName, sanitizedArray);
+ }
+
+ log.debug("Artifacts validation completed successfully");
+ return artifacts;
+
+ } catch (Exception e) {
+ log.error("Error validating artifacts structure, returning empty structure", e);
+ return createEmptyArtifactsStructure();
+ }
+ }
+
+ /**
+ * Validates the entire heartbeat payload structure for Ballerina GraphQL API compatibility
+ */
+ private static JsonObject validateHeartbeatPayload(JsonObject payload) {
+ try {
+ log.debug("Validating heartbeat payload structure for ICP GraphQL API compatibility");
+
+ // Ensure all required root-level properties exist and have correct types
+ if (!payload.has("runtime") || payload.get("runtime").isJsonNull()) {
+ payload.addProperty("runtime", UUID.randomUUID().toString());
+ log.warn("Missing runtime, added default UUID");
+ }
+
+ if (!payload.has("runtimeType") || payload.get("runtimeType").isJsonNull()) {
+ payload.addProperty("runtimeType", "MI");
+ log.warn("Missing runtimeType, added default 'MI'");
+ }
+
+ if (!payload.has("status") || payload.get("status").isJsonNull()) {
+ payload.addProperty("status", "RUNNING");
+ log.warn("Missing status, added default 'RUNNING'");
+ }
+
+ if (!payload.has("environment") || payload.get("environment").isJsonNull()) {
+ payload.addProperty("environment", "dev");
+ log.warn("Missing environment, added default 'dev'");
+ }
+
+ if (!payload.has("project") || payload.get("project").isJsonNull()) {
+ payload.addProperty("project", "default");
+ log.warn("Missing project, added default 'default'");
+ }
+
+ if (!payload.has("component") || payload.get("component").isJsonNull()) {
+ payload.addProperty("component", "micro-integrator");
+ log.warn("Missing component, added default 'micro-integrator'");
+ }
+
+ if (!payload.has("version") || payload.get("version").isJsonNull()) {
+ payload.addProperty("version", "4.4.0");
+ log.warn("Missing version, added default '4.4.0'");
+ }
+
+ // Validate nodeInfo structure
+ if (!payload.has("nodeInfo") || payload.get("nodeInfo").isJsonNull() || !payload.get("nodeInfo").isJsonObject()) {
+ JsonObject nodeInfo = new JsonObject();
+ nodeInfo.addProperty("platformName", "wso2-mi");
+ nodeInfo.addProperty("platformVersion", "4.4.0");
+ nodeInfo.addProperty("platformHome", System.getProperty("carbon.home", "/opt/wso2mi"));
+ nodeInfo.addProperty("osName", System.getProperty("os.name", "unknown"));
+ nodeInfo.addProperty("osVersion", System.getProperty("os.version", "unknown"));
+ nodeInfo.addProperty("javaVersion", System.getProperty("java.version", "unknown"));
+ payload.add("nodeInfo", nodeInfo);
+ log.warn("Missing or invalid nodeInfo, added default nodeInfo structure");
+ }
+
+ // Validate artifacts structure
+ if (!payload.has("artifacts") || payload.get("artifacts").isJsonNull() || !payload.get("artifacts").isJsonObject()) {
+ payload.add("artifacts", createEmptyArtifactsStructure());
+ log.warn("Missing or invalid artifacts, added empty artifacts structure");
+ }
+
+ // Ensure runtimeHash exists
+ if (!payload.has("runtimeHash") || payload.get("runtimeHash").isJsonNull()) {
+ payload.addProperty("runtimeHash", "");
+ log.warn("Missing runtimeHash, added empty string");
+ }
+
+ // Validate timestamp structure if present
+ if (payload.has("timestamp") && !payload.get("timestamp").isJsonNull()) {
+ if (!payload.get("timestamp").isJsonArray()) {
+ payload.add("timestamp", createBallerinaTimestamp());
+ log.warn("Invalid timestamp format, replaced with valid Ballerina timestamp");
+ } else {
+ JsonArray timestamp = payload.getAsJsonArray("timestamp");
+ if (timestamp.size() != 2) {
+ payload.add("timestamp", createBallerinaTimestamp());
+ log.warn("Invalid timestamp array size, replaced with valid Ballerina timestamp");
+ }
+ }
+ }
+
+ log.debug("Heartbeat payload validation completed successfully");
+ return payload;
+
+ } catch (Exception e) {
+ log.error("Error validating heartbeat payload structure, returning minimal payload", e);
+
+ // Create minimal valid payload
+ JsonObject minimalPayload = new JsonObject();
+ minimalPayload.addProperty("runtime", UUID.randomUUID().toString());
+ minimalPayload.addProperty("runtimeType", "MI");
+ minimalPayload.addProperty("status", "RUNNING");
+ minimalPayload.addProperty("environment", "dev");
+ minimalPayload.addProperty("project", "default");
+ minimalPayload.addProperty("component", "micro-integrator");
+ minimalPayload.addProperty("version", "4.4.0");
+ minimalPayload.addProperty("runtimeHash", "");
+
+ JsonObject nodeInfo = new JsonObject();
+ nodeInfo.addProperty("platformName", "wso2-mi");
+ nodeInfo.addProperty("platformVersion", "4.4.0");
+ minimalPayload.add("nodeInfo", nodeInfo);
+
+ minimalPayload.add("artifacts", createEmptyArtifactsStructure());
+
+ return minimalPayload;
+ }
+ }
+
+ /**
+ * Creates a Ballerina time:Utc compatible timestamp array [seconds, nanoseconds_fraction].
+ */
+ private static JsonArray createBallerinaTimestamp() {
+ Instant now = Instant.now();
+ JsonArray timestampArray = new JsonArray();
+ timestampArray.add(now.getEpochSecond()); // seconds since epoch as int
+ timestampArray.add(now.getNano() / 1_000_000_000.0); // nanoseconds as decimal fraction
+ return timestampArray;
+ }
+
+ /**
+ * Calculates MD5 hash of the payload (excluding timestamp and dynamic memory values).
+ */
+ private static String calculateHash(JsonObject payload) {
+ try {
+ // Create a copy and remove timestamp for consistent hashing
+ JsonObject payloadCopy = payload.deepCopy();
+ payloadCopy.remove("timestamp");
+ if (payloadCopy.has("nodeInfo") && payloadCopy.get("nodeInfo").isJsonObject()) {
+ JsonObject nodeInfo = payloadCopy.getAsJsonObject("nodeInfo");
+ nodeInfo.remove("freeMemory");
+ nodeInfo.remove("usedMemory");
+ nodeInfo.remove("maxMemory");
+ nodeInfo.remove("totalMemory");
+ }
+
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] hashBytes = md.digest(payloadCopy.toString().getBytes("UTF-8"));
+ return Base64.getEncoder().encodeToString(hashBytes);
+ } catch (Exception e) {
+ log.error("Error calculating hash for heartbeat payload.", e);
+ return "";
+ }
+ }
+
+ /**
+ * Generates a new JWT token or returns cached token if still valid.
+ */
+ private static String generateOrGetCachedJwtToken() throws Exception {
+ long currentTime = System.currentTimeMillis();
+ // Return cached token if it's still valid (with 5 minute buffer)
+ if (cachedJwtToken != null && currentTime < (jwtTokenExpiry - 300000)) {
+ return cachedJwtToken;
+ }
+ String jwtHmacSecret = getConfigValue(ICP_JWT_HMAC_SECRET, DEFAULT_JWT_HMAC_SECRET);
+ HMACJWTTokenGenerator hmacJWTTokenGenerator = new HMACJWTTokenGenerator(jwtHmacSecret);
+ String issuer = getConfigValue(ICP_JWT_ISSUER, DEFAULT_JWT_ISSUER);
+ String audience = getConfigValue(ICP_JWT_AUDIENCE, DEFAULT_JWT_AUDIENCE);
+ String scope = getConfigValue(ICP_JWT_SCOPE, DEFAULT_JWT_SCOPE);
+ long expirySeconds = getJwtExpirySeconds();
+ // Generate new token
+ cachedJwtToken = hmacJWTTokenGenerator.generateToken(issuer, audience, scope, expirySeconds);
+ jwtTokenExpiry = currentTime + (expirySeconds * 1000);
+ return cachedJwtToken;
+ }
+
+ /**
+ * Creates an HTTP client with SSL support.
+ */
+ private static CloseableHttpClient createHttpClient() throws Exception {
+ return HttpClients.custom()
+ .setSSLSocketFactory(new SSLConnectionSocketFactory(
+ SSLContexts.custom()
+ .loadTrustMaterial(null, (TrustStrategy) new TrustSelfSignedStrategy())
+ .build(),
+ NoopHostnameVerifier.INSTANCE))
+ .build();
+ }
+
+ /**
+ * Gets the environment name.
+ */
+ private static String getEnvironment() {
+ return getConfigValue(ICP_CONFIG_ENVIRONMENT, DEFAULT_ENVIRONMENT);
+ }
+
+ /**
+ * Gets the project name.
+ */
+ private static String getProject() {
+ return getConfigValue(ICP_CONFIG_PROJECT, DEFAULT_PROJECT);
+ }
+
+ /**
+ * Gets the component name.
+ */
+ private static String getComponent() {
+ return getConfigValue(ICP_CONFIG_COMPONENT, DEFAULT_COMPONENT);
+ }
+
+ /**
+ * Gets the heartbeat interval in seconds.
+ */
+ private static long getInterval() {
+ long interval = DEFAULT_HEARTBEAT_INTERVAL;
+ Object configuredInterval = configs.get(ICP_CONFIG_HEARTBEAT_INTERVAL);
+ if (configuredInterval != null) {
+ interval = Integer.parseInt(configuredInterval.toString());
+ }
+ return interval;
+ }
+
+ /**
+ * Gets the JWT token expiry time in seconds.
+ */
+ private static long getJwtExpirySeconds() {
+ Object expiry = configs.get(ICP_JWT_EXPIRY_SECONDS);
+ if (expiry != null) {
+ return Long.parseLong(expiry.toString());
+ }
+ return DEFAULT_JWT_EXPIRY_SECONDS;
+ }
+
+ /**
+ * Gets the MI version.
+ */
+ private static String getMicroIntegratorVersion() {
+ return System.getProperty("product.version", "4.4.0");
+ }
+
+ /**
+ * Helper method to get configuration value with fallback.
+ */
+ private static String getConfigValue(String key, String defaultValue) {
+ Object value = configs.get(key);
+ return (value != null) ? value.toString() : defaultValue;
+ }
+
+ /**
+ * Checks if ICP is configured and enabled.
+ * Returns true only if explicitly enabled via configuration.
+ *
+ * @return true if ICP is enabled, false otherwise
+ */
+ public static boolean isICPConfigured() {
+ Object icpEnabled = configs.get(ICP_CONFIG_ENABLED);
+ return icpEnabled != null && "true".equalsIgnoreCase(icpEnabled.toString());
+ }
+
+ /**
+ * Parses JSON response from HTTP response.
+ */
+ private static JsonObject getJsonResponse(CloseableHttpResponse response) {
+ try {
+ HttpEntity entity = response.getEntity();
+ String stringResponse = EntityUtils.toString(entity, "UTF-8");
+ Gson gson = new Gson();
+ return gson.fromJson(stringResponse, JsonObject.class);
+ } catch (Exception e) {
+ log.debug("Error parsing JSON response from ICP.", e);
+ return null;
+ }
+ }
+
+ // ===== ARTIFACT COLLECTION METHODS =====
+
+ /**
+ * Collects REST API information from Synapse Configuration
+ */
+ private static JsonArray collectRestApis(SynapseConfiguration synapseConfig) {
+ JsonArray apis = new JsonArray();
+ try {
+ Collection apiCollection = synapseConfig.getAPIs();
+ for (API api : apiCollection) {
+ JsonObject apiObj = new JsonObject();
+ apiObj.addProperty("name", api.getName());
+ apiObj.addProperty("context", api.getContext());
+ apiObj.addProperty("version", api.getVersion());
+ apiObj.addProperty("host", api.getHost());
+ apiObj.addProperty("port", api.getPort());
+ apiObj.addProperty("type", "API");
+ apiObj.addProperty("state", "ENABLED");
+
+ // Collect API resources
+ JsonArray resources = new JsonArray();
+ if (api.getResources() != null) {
+ for (org.apache.synapse.api.Resource resource : api.getResources()) {
+ JsonObject resourceObj = new JsonObject();
+
+ String resourcePath = "";
+ try {
+ if (resource.getDispatcherHelper() != null) {
+ resourcePath = resource.getDispatcherHelper().getString();
+ }
+ if (resourcePath == null || resourcePath.isEmpty()) {
+ resourcePath = "/*";
+ }
+ } catch (Exception ex) {
+ resourcePath = "/*";
+ }
+ resourceObj.addProperty("path", resourcePath);
+
+ if (resource.getMethods() != null && resource.getMethods().length > 0) {
+ resourceObj.addProperty("methods", String.join(",", resource.getMethods()));
+ }
+ resources.add(resourceObj);
+ }
+ }
+ apiObj.add("resources", resources);
+ apis.add(apiObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting REST APIs", e);
+ }
+ return apis;
+ }
+
+ /**
+ * Collects Proxy Service information from Synapse Configuration
+ */
+ private static JsonArray collectProxyServices(SynapseConfiguration synapseConfig) {
+ JsonArray proxies = new JsonArray();
+ try {
+ Collection proxyCollection = synapseConfig.getProxyServices();
+ for (ProxyService proxy : proxyCollection) {
+ JsonObject proxyObj = new JsonObject();
+ proxyObj.addProperty("name", proxy.getName());
+ proxyObj.addProperty("type", "ProxyService");
+ proxyObj.addProperty("state", proxy.isRunning() ? "ENABLED" : "DISABLED");
+
+ // Transport information
+ if (proxy.getTransports() != null) {
+ JsonArray transports = new JsonArray();
+ for (Object transport : proxy.getTransports()) {
+ transports.add(transport.toString());
+ }
+ proxyObj.add("transports", transports);
+ }
+
+ proxies.add(proxyObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Proxy Services", e);
+ }
+ return proxies;
+ }
+
+ /**
+ * Collects Endpoint information from Synapse Configuration
+ */
+ private static JsonArray collectEndpoints(SynapseConfiguration synapseConfig) {
+ JsonArray endpoints = new JsonArray();
+ try {
+ Map endpointMap = synapseConfig.getDefinedEndpoints();
+ for (Map.Entry entry : endpointMap.entrySet()) {
+ JsonObject endpointObj = new JsonObject();
+ org.apache.synapse.endpoints.Endpoint endpoint = entry.getValue();
+ endpointObj.addProperty("name", entry.getKey());
+ endpointObj.addProperty("type", endpoint.getClass().getSimpleName());
+ endpoints.add(endpointObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Endpoints", e);
+ }
+ return endpoints;
+ }
+
+ /**
+ * Collects Inbound Endpoint information
+ */
+ private static JsonArray collectInboundEndpoints(SynapseConfiguration synapseConfig) {
+ JsonArray inboundEndpoints = new JsonArray();
+ try {
+ Collection inboundCollection =
+ synapseConfig.getInboundEndpoints();
+ for (org.apache.synapse.inbound.InboundEndpoint inbound : inboundCollection) {
+ JsonObject inboundObj = new JsonObject();
+ inboundObj.addProperty("name", inbound.getName());
+ inboundObj.addProperty("protocol", inbound.getProtocol());
+ inboundObj.addProperty("state", "ENABLED");
+ inboundEndpoints.add(inboundObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Inbound Endpoints", e);
+ }
+ return inboundEndpoints;
+ }
+
+ /**
+ * Collects Sequence information from Synapse Configuration
+ */
+ private static JsonArray collectSequences(SynapseConfiguration synapseConfig) {
+ JsonArray sequences = new JsonArray();
+ try {
+ Map seqMap =
+ synapseConfig.getDefinedSequences();
+ for (Map.Entry entry : seqMap.entrySet()) {
+ JsonObject seqObj = new JsonObject();
+ seqObj.addProperty("name", entry.getKey());
+ seqObj.addProperty("type", "Sequence");
+ sequences.add(seqObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Sequences", e);
+ }
+ return sequences;
+ }
+
+ /**
+ * Collects Task information from Synapse Configuration
+ */
+ private static JsonArray collectTasks(SynapseConfiguration synapseConfig) {
+ JsonArray tasks = new JsonArray();
+ try {
+ // Note: Task collection might require different approach based on MI version
+ // For now, we'll create a placeholder implementation
+ log.debug("Tasks collection placeholder - requires TaskManager integration");
+ } catch (Exception e) {
+ log.error("Error collecting Tasks", e);
+ }
+ return tasks;
+ }
+
+ /**
+ * Collects Template information from Synapse Configuration
+ */
+ private static JsonArray collectTemplates(SynapseConfiguration synapseConfig) {
+ JsonArray templates = new JsonArray();
+ try {
+ // Endpoint Templates
+ Map endpointTemplates =
+ synapseConfig.getEndpointTemplates();
+ for (Map.Entry entry : endpointTemplates.entrySet()) {
+ JsonObject templateObj = new JsonObject();
+ templateObj.addProperty("name", entry.getKey());
+ templateObj.addProperty("type", "EndpointTemplate");
+ templates.add(templateObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Templates", e);
+ }
+ return templates;
+ }
+
+ /**
+ * Collects Message Store information from Synapse Configuration
+ */
+ private static JsonArray collectMessageStores(SynapseConfiguration synapseConfig) {
+ JsonArray messageStores = new JsonArray();
+ try {
+ Map storeMap =
+ synapseConfig.getMessageStores();
+ for (Map.Entry entry : storeMap.entrySet()) {
+ JsonObject storeObj = new JsonObject();
+ org.apache.synapse.message.store.MessageStore store = entry.getValue();
+ storeObj.addProperty("name", entry.getKey());
+ storeObj.addProperty("type", store.getClass().getSimpleName());
+ storeObj.addProperty("size", store.size());
+ messageStores.add(storeObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Message Stores", e);
+ }
+ return messageStores;
+ }
+
+ /**
+ * Collects Message Processor information from Synapse Configuration
+ */
+ private static JsonArray collectMessageProcessors(SynapseConfiguration synapseConfig) {
+ JsonArray messageProcessors = new JsonArray();
+ try {
+ Map processorMap =
+ synapseConfig.getMessageProcessors();
+ for (Map.Entry entry : processorMap.entrySet()) {
+ JsonObject processorObj = new JsonObject();
+ org.apache.synapse.message.processor.MessageProcessor processor = entry.getValue();
+ processorObj.addProperty("name", entry.getKey());
+ processorObj.addProperty("type", processor.getClass().getSimpleName());
+ processorObj.addProperty("state", "ENABLED"); // Default state
+ messageProcessors.add(processorObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Message Processors", e);
+ }
+ return messageProcessors;
+ }
+
+ /**
+ * Collects Local Entry information from Synapse Configuration
+ */
+ private static JsonArray collectLocalEntries(SynapseConfiguration synapseConfig) {
+ JsonArray localEntries = new JsonArray();
+ try {
+ @SuppressWarnings("unchecked")
+ Map entryMap = synapseConfig.getLocalRegistry();
+ for (Map.Entry entry : entryMap.entrySet()) {
+ JsonObject entryObj = new JsonObject();
+ entryObj.addProperty("name", entry.getKey());
+ entryObj.addProperty("type", "LocalEntry");
+ entryObj.addProperty("valueType", entry.getValue().getClass().getSimpleName());
+ localEntries.add(entryObj);
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Local Entries", e);
+ }
+ return localEntries;
+ }
+
+ /**
+ * Collects Data Service information from Synapse Configuration
+ */
+ private static JsonArray collectDataServices(SynapseConfiguration synapseConfig) {
+ JsonArray dataServices = new JsonArray();
+ try {
+ if (synapseConfig == null || synapseConfig.getAxisConfiguration() == null) {
+ log.debug("Synapse configuration or Axis configuration is not available for data services collection");
+ return dataServices;
+ }
+
+ AxisConfiguration axisConfiguration = synapseConfig.getAxisConfiguration();
+
+ // Get available data service names using DBUtils
+ String[] dataServiceNames = org.wso2.micro.integrator.dataservices.core.DBUtils.getAvailableDS(axisConfiguration);
+
+ for (String serviceName : dataServiceNames) {
+ JsonObject dsObj = new JsonObject();
+ try {
+ dsObj.addProperty("name", serviceName);
+ dsObj.addProperty("type", "DataService");
+
+ // Try to get the DataService object for more details
+ AxisService axisService = axisConfiguration.getServiceForActivation(serviceName);
+ if (axisService != null) {
+ // Get DataService object from axis service parameter
+ Parameter dsParam = axisService.getParameter("DataService");
+ if (dsParam != null && dsParam.getValue() instanceof org.wso2.micro.integrator.dataservices.core.engine.DataService) {
+ org.wso2.micro.integrator.dataservices.core.engine.DataService dataService =
+ (org.wso2.micro.integrator.dataservices.core.engine.DataService) dsParam.getValue();
+
+ // Add service description if available
+ if (dataService.getDescription() != null) {
+ dsObj.addProperty("description", dataService.getDescription());
+ }
+
+ // Add config information
+ if (dataService.getConfigs() != null && !dataService.getConfigs().isEmpty()) {
+ JsonArray configs = new JsonArray();
+ for (String configId : dataService.getConfigs().keySet()) {
+ configs.add(configId);
+ }
+ dsObj.add("configs", configs);
+ }
+
+ // Add operation count
+ if (dataService.getOperationNames() != null) {
+ dsObj.addProperty("operationCount", dataService.getOperationNames().size());
+ }
+
+ // Add query count
+ if (dataService.getQueries() != null) {
+ dsObj.addProperty("queryCount", dataService.getQueries().size());
+ }
+ }
+
+ // Add service status
+ dsObj.addProperty("state", axisService.isActive() ? "ENABLED" : "DISABLED");
+ }
+
+ dataServices.add(dsObj);
+ } catch (Exception e) {
+ log.warn("Error processing data service: " + serviceName, e);
+ // Add basic info even if detailed processing fails
+ JsonObject basicDsObj = new JsonObject();
+ basicDsObj.addProperty("name", serviceName);
+ basicDsObj.addProperty("type", "DataService");
+ basicDsObj.addProperty("state", "UNKNOWN");
+ dataServices.add(basicDsObj);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Error collecting Data Services", e);
+ }
+ return dataServices;
+ }
+
+ /**
+ * Collects Carbon Application information in the required heartbeat format
+ */
+ private static JsonArray collectCarbonApps() {
+ JsonArray carbonApps = new JsonArray();
+ try {
+ // Get active Carbon Applications
+ Collection activeApps = CappDeployer.getCarbonApps();
+ for (CarbonApplication app : activeApps) {
+ JsonObject appObj = convertCarbonAppToHeartbeatFormat(app, "active", getRuntimeId());
+ if (appObj != null) {
+ carbonApps.add(appObj);
+ }
+ }
+
+ // Get faulty Carbon Applications
+ Collection faultyApps = CappDeployer.getFaultyCAppObjects();
+ for (CarbonApplication app : faultyApps) {
+ JsonObject appObj = convertCarbonAppToHeartbeatFormat(app, "faulty", getRuntimeId());
+ if (appObj != null) {
+ carbonApps.add(appObj);
+ }
+ }
+
+ } catch (Exception e) {
+ log.error("Error collecting Carbon Apps", e);
+ }
+ return carbonApps;
+ }
+
+ /**
+ * Converts a CarbonApplication to the required heartbeat format with name, nameIgnoreCase, and nodes structure
+ */
+ private static JsonObject convertCarbonAppToHeartbeatFormat(CarbonApplication carbonApp, String status, String runtimeId) {
+ if (carbonApp == null) {
+ return null;
+ }
+
+ JsonObject appObj = new JsonObject();
+ String appName = carbonApp.getAppName();
+
+ // Main structure
+ appObj.addProperty("name", appName);
+ appObj.addProperty("runtimeId", runtimeId);
+ appObj.addProperty("version", carbonApp.getAppVersion());
+ appObj.addProperty("status", status);
+
+ // Create nodes array with single node (this MI instance)
+ // JsonArray nodes = new JsonArray();
+ // JsonObject nodeObj = new JsonObject();
+
+ // Create details JSON string
+ // JsonObject details = new JsonObject();
+ // details.addProperty("name", appName);
+ // details.addProperty("version", carbonApp.getAppVersion());
+ // details.addProperty("status", status);
+
+ // // Collect artifacts contained in this Carbon App
+ // JsonArray artifacts = new JsonArray();
+ // try {
+ // if (carbonApp.getAppConfig() != null &&
+ // carbonApp.getAppConfig().getApplicationArtifact() != null &&
+ // carbonApp.getAppConfig().getApplicationArtifact().getDependencies() != null) {
+
+ // for (Artifact.Dependency dependency : carbonApp.getAppConfig().getApplicationArtifact().getDependencies()) {
+ // Artifact artifact = dependency.getArtifact();
+
+ // if (artifact != null && artifact.getName() != null) {
+ // JsonObject artifactObj = new JsonObject();
+ // artifactObj.addProperty("name", artifact.getName());
+
+ // // Extract artifact type (remove prefix if present)
+ // String artifactType = artifact.getType();
+ // if (artifactType != null && artifactType.contains("/")) {
+ // artifactType = artifactType.split("/")[1];
+ // }
+ // artifactObj.addProperty("type", artifactType);
+
+ // artifacts.add(artifactObj);
+ // }
+ // }
+ // }
+ // } catch (Exception e) {
+ // log.warn("Error processing artifacts for Carbon App: " + appName, e);
+ // }
+
+ // details.add("artifacts", artifacts);
+
+ // // Convert details to JSON string and add to node
+ // nodeObj.addProperty("details", details.toString());
+ // nodes.add(nodeObj);
+
+ // appObj.add("nodes", nodes);
+
+ return appObj;
+ }
+
+ /**
+ * Collects Data Source information
+ */
+ private static JsonArray collectDataSources() {
+ JsonArray dataSources = new JsonArray();
+ try {
+ // Note: This would require access to the DataSource manager
+ // For now, returning empty array as it requires integration with DS manager
+ log.debug("Data Sources collection not yet implemented - requires DS manager integration");
+ } catch (Exception e) {
+ log.error("Error collecting Data Sources", e);
+ }
+ return dataSources;
+ }
+
+ /**
+ * Collects Connector information from Synapse Configuration
+ */
+ private static JsonArray collectConnectors(SynapseConfiguration synapseConfig) {
+ JsonArray connectors = new JsonArray();
+ try {
+ log.info("Starting connector collection for ICP heartbeat");
+
+ if (synapseConfig == null) {
+ log.warn("SynapseConfiguration is null, returning empty connector list");
+ return connectors;
+ }
+
+ // Get synapse libraries (which include connectors)
+ Map libraryMap = synapseConfig.getSynapseLibraries();
+
+ if (libraryMap == null || libraryMap.isEmpty()) {
+ log.info("No connectors/libraries found in SynapseConfiguration");
+ return connectors;
+ }
+
+ log.info("Found " + libraryMap.size() + " libraries/connectors to process");
+
+ int processedCount = 0;
+ int errorCount = 0;
+
+ for (Map.Entry entry : libraryMap.entrySet()) {
+ try {
+ String qualifiedName = entry.getKey();
+ org.apache.synapse.libraries.model.Library library = entry.getValue();
+
+ if (library instanceof org.apache.synapse.libraries.model.SynapseLibrary) {
+ org.apache.synapse.libraries.model.SynapseLibrary synapseLibrary =
+ (org.apache.synapse.libraries.model.SynapseLibrary) library;
+
+ JsonObject connectorObj = new JsonObject();
+ connectorObj.addProperty("name", synapseLibrary.getName());
+ connectorObj.addProperty("package", synapseLibrary.getPackage());
+ connectorObj.addProperty("qualifiedName", qualifiedName);
+ connectorObj.addProperty("type", "Connector");
+
+ // Add description if available
+ if (synapseLibrary.getDescription() != null) {
+ connectorObj.addProperty("description", synapseLibrary.getDescription());
+ }
+
+ // Add status information
+ Boolean libStatus = synapseLibrary.getLibStatus();
+ String status = (libStatus != null && libStatus) ? "enabled" : "disabled";
+ connectorObj.addProperty("status", status);
+
+ // Add file name if available
+ if (synapseLibrary.getFileName() != null) {
+ connectorObj.addProperty("fileName", synapseLibrary.getFileName());
+ }
+
+ connectors.add(connectorObj);
+ processedCount++;
+
+ if (log.isDebugEnabled()) {
+ log.debug("Processed connector: " + synapseLibrary.getName() +
+ " (package: " + synapseLibrary.getPackage() + ", status: " + status + ")");
+ }
+ } else {
+ log.debug("Skipping non-SynapseLibrary entry: " + qualifiedName +
+ " (type: " + library.getClass().getSimpleName() + ")");
+ }
+ } catch (Exception e) {
+ errorCount++;
+ log.warn("Error processing connector entry: " + entry.getKey(), e);
+
+ // Add basic error entry
+ JsonObject errorConnectorObj = new JsonObject();
+ errorConnectorObj.addProperty("name", "ERROR_" + entry.getKey());
+ errorConnectorObj.addProperty("type", "Connector");
+ errorConnectorObj.addProperty("status", "error");
+ errorConnectorObj.addProperty("error", e.getMessage());
+ connectors.add(errorConnectorObj);
+ }
+ }
+
+ log.info("Connector collection completed. Processed: " + processedCount +
+ ", Errors: " + errorCount + ", Total connectors collected: " + connectors.size());
+
+ } catch (Exception e) {
+ log.error("Error collecting Connectors from SynapseConfiguration", e);
+ }
+ return connectors;
+ }
+
+ /**
+ * Collects Registry Resources from MicroIntegratorRegistry
+ */
+ private static JsonArray collectRegistryResources(SynapseConfiguration synapseConfig) {
+ JsonArray registryResources = new JsonArray();
+ try {
+ log.info("Starting registry resources collection for ICP heartbeat");
+
+ if (synapseConfig == null) {
+ log.warn("SynapseConfiguration is null, returning empty registry resources list");
+ return registryResources;
+ }
+
+ // Get the registry from synapse configuration
+ org.apache.synapse.registry.Registry synapseRegistry = synapseConfig.getRegistry();
+ if (synapseRegistry == null) {
+ log.warn("No registry found in SynapseConfiguration, returning empty registry resources list");
+ return registryResources;
+ }
+
+ if (!(synapseRegistry instanceof org.wso2.micro.integrator.registry.MicroIntegratorRegistry)) {
+ log.warn("Registry is not MicroIntegratorRegistry type, cannot collect resources. Type: " +
+ synapseRegistry.getClass().getSimpleName());
+ return registryResources;
+ }
+
+ org.wso2.micro.integrator.registry.MicroIntegratorRegistry microIntegratorRegistry =
+ (org.wso2.micro.integrator.registry.MicroIntegratorRegistry) synapseRegistry;
+
+ String regRoot = microIntegratorRegistry.getRegRoot();
+ log.info("Registry root path: " + regRoot);
+
+ if (regRoot == null || regRoot.trim().isEmpty()) {
+ log.warn("Registry root path is null or empty, returning empty registry resources list");
+ return registryResources;
+ }
+
+ // Get the root directory contents
+ java.io.File rootDir = new java.io.File(regRoot);
+ if (!rootDir.exists() || !rootDir.isDirectory()) {
+ log.warn("Registry root directory does not exist or is not a directory: " + regRoot);
+ return registryResources;
+ }
+
+ // Get immediate children of the registry root
+ org.json.JSONArray childrenList = microIntegratorRegistry.getChildrenList(regRoot, regRoot);
+
+ if (childrenList == null) {
+ log.warn("Failed to get children list from registry root");
+ return registryResources;
+ }
+
+ log.info("Found " + childrenList.length() + " registry resources to process");
+
+ int processedCount = 0;
+ int errorCount = 0;
+
+ // Convert JSONArray to JsonArray and add metadata
+ for (int i = 0; i < childrenList.length(); i++) {
+ try {
+ org.json.JSONObject child = childrenList.getJSONObject(i);
+
+ JsonObject resourceObj = new JsonObject();
+
+ // Basic properties
+ if (child.has("name")) {
+ resourceObj.addProperty("name", child.getString("name"));
+ }
+ if (child.has("type")) {
+ resourceObj.addProperty("type", child.getString("type"));
+ }
+ if (child.has("path")) {
+ resourceObj.addProperty("path", child.getString("path"));
+ }
+ if (child.has("mediaType")) {
+ resourceObj.addProperty("mediaType", child.getString("mediaType"));
+ }
+ if (child.has("size")) {
+ resourceObj.addProperty("size", child.getString("size"));
+ }
+ if (child.has("lastModified")) {
+ resourceObj.addProperty("lastModified", child.getString("lastModified"));
+ }
+
+ // Properties array if exists
+ if (child.has("properties")) {
+ org.json.JSONArray properties = child.getJSONArray("properties");
+ JsonArray propsArray = new JsonArray();
+ for (int j = 0; j < properties.length(); j++) {
+ org.json.JSONObject prop = properties.getJSONObject(j);
+ JsonObject propObj = new JsonObject();
+ if (prop.has("name")) {
+ propObj.addProperty("name", prop.getString("name"));
+ }
+ if (prop.has("value")) {
+ propObj.addProperty("value", prop.getString("value"));
+ }
+ propsArray.add(propObj);
+ }
+ resourceObj.add("properties", propsArray);
+ }
+
+ registryResources.add(resourceObj);
+ processedCount++;
+
+ if (log.isDebugEnabled()) {
+ String resourceName = child.has("name") ? child.getString("name") : "unknown";
+ String resourceType = child.has("type") ? child.getString("type") : "unknown";
+ log.debug("Processed registry resource: " + resourceName + " (type: " + resourceType + ")");
+ }
+
+ } catch (Exception e) {
+ errorCount++;
+ log.warn("Error processing registry resource at index " + i, e);
+
+ // Add basic error entry
+ JsonObject errorResourceObj = new JsonObject();
+ errorResourceObj.addProperty("name", "ERROR_RESOURCE_" + i);
+ errorResourceObj.addProperty("type", "error");
+ errorResourceObj.addProperty("error", e.getMessage());
+ registryResources.add(errorResourceObj);
+ }
+ }
+
+ log.info("Registry resources collection completed. Processed: " + processedCount +
+ ", Errors: " + errorCount + ", Total registry resources collected: " + registryResources.size());
+
+ // Log the response data for debugging
+ if (log.isDebugEnabled()) {
+ log.debug("Registry resources response data: " + registryResources.toString());
+ } else {
+ log.info("Registry resources response summary - Count: " + registryResources.size() +
+ ", Root directory: " + regRoot);
+ }
+
+ } catch (Exception e) {
+ log.error("Error collecting Registry Resources from SynapseConfiguration", e);
+ }
+ return registryResources;
+ }
+
+ /**
+ * Collects Listener information (HTTP/HTTPS ports)
+ */
+ private static JsonArray collectListeners() {
+ JsonArray listeners = new JsonArray();
+ try {
+ // HTTP Listener
+ JsonObject httpListener = new JsonObject();
+ httpListener.addProperty("protocol", "http");
+ httpListener.addProperty("port", ConfigurationLoader.getInternalInboundHttpPort());
+ httpListener.addProperty("host", "0.0.0.0");
+ listeners.add(httpListener);
+
+ // HTTPS Listener
+ JsonObject httpsListener = new JsonObject();
+ httpsListener.addProperty("protocol", "https");
+ httpsListener.addProperty("port", ConfigurationLoader.getInternalInboundHttpsPort());
+ httpsListener.addProperty("host", "0.0.0.0");
+ listeners.add(httpsListener);
+ } catch (Exception e) {
+ log.error("Error collecting Listeners", e);
+ }
+ return listeners;
+ }
+
+}
diff --git a/deployment-icp-sample.toml b/deployment-icp-sample.toml
new file mode 100644
index 0000000000..7aac0adc87
--- /dev/null
+++ b/deployment-icp-sample.toml
@@ -0,0 +1,62 @@
+# ========================================
+# WSO2 Micro Integrator - ICP Configuration Sample
+# ========================================
+
+# ----------------------------------------
+# New ICP Integration Configuration
+# ----------------------------------------
+[server]
+hostname = "localhost"
+# offset = 10
+
+[user_store]
+type = "read_only_ldap"
+
+[keystore.primary]
+file_name = "repository/resources/security/wso2carbon.jks"
+password = "wso2carbon"
+alias = "wso2carbon"
+key_password = "wso2carbon"
+
+[truststore]
+file_name = "repository/resources/security/client-truststore.jks"
+password = "wso2carbon"
+alias = "symmetric.key.value"
+algorithm = "AES"
+
+# ----------------------------------------
+# ICP Configuration
+# ----------------------------------------
+[icp_config]
+# Enable new ICP integration (set to false to use legacy Dashboard)
+enabled = true
+environment = "prod"
+project = "sample-project"
+integration = "sample-mi-integration"
+# ----------------------------------------
+# Heartbeat Configuration
+# ----------------------------------------
+# Interval between heartbeat transmissions (in seconds)
+# Default: 5 seconds
+# heartbeat_interval = 10
+# ----------------------------------------
+# Advanced - JWT Authentication Configuration
+# ----------------------------------------
+#jwt_issuer = "icp-runtime-jwt-issuer"
+#jwt_audience = "icp-server"
+#jwt_scope = "runtime_agent"
+# Token validity in seconds (default: 3600 = 1 hour)
+#jwt_expiry_seconds = 3600
+#jwt_hmac_secret = "default-secret-key-at-least-32-characters-long-for-hs256"
+
+# ----------------------------------------
+# Legacy Dashboard Configuration (Optional)
+# Keep this for backward compatibility or fallback
+# ----------------------------------------
+#[dashboard_config]
+#dashboard_url = "https://dashboard-host:9743/dashboard/api/"
+#group_id = "default"
+#node_id = "node1"
+#heartbeat_interval = 5
+#management_hostname = "mi-host.example.com"
+#management_port = 9154