package com.moengage.integration.workflow;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.engine.SlingRequestProcessor;
import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.wcm.api.Page;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.ContentElement;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONObject;
import org.json.JSONArray;
@Component(
service = WorkflowProcess.class,
property = { "process.label=MoEngage Content Sync" }
)
public class MoEngageContentSyncStep implements WorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(MoEngageContentSyncStep.class);
private static final String API_BASE_TEMPLATE = "https://api-%s.moengage.com/v1/external/campaigns/content-blocks";
private static final int BUFFER_DELAY_MS = 500;
@Reference private RequestResponseFactory requestResponseFactory;
@Reference private SlingRequestProcessor requestProcessor;
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
throws WorkflowException {
ResourceResolver resourceResolver = null;
try {
String processArgs = args.get("PROCESS_ARGS", "string");
Map<String, String> config = parseProcessArgs(processArgs);
String apiKey = config.get("moengage.api.key");
String apiSecret = config.get("moengage.api.secret");
String dataCenter = config.get("moengage.datacenter");
String publishUrl = config.get("aem.publish.url");
String targetStatus = config.get("moengage.status");
boolean debugMode = Boolean.parseBoolean(config.get("moengage.debug"));
String debugUrl = config.get("moengage.debug.url");
if (apiKey == null || apiKey.trim().isEmpty()) {
log.warn("MoEngage API key not configured, skipping");
return;
}
if (apiSecret == null) apiSecret = "";
if (dataCenter == null || dataCenter.trim().isEmpty()) {
log.warn("MoEngage datacenter not configured, skipping");
return;
}
if (targetStatus == null) targetStatus = "ACTIVE";
targetStatus = targetStatus.toUpperCase();
if (publishUrl != null && publishUrl.endsWith("/")) {
publishUrl = publishUrl.substring(0, publishUrl.length() - 1);
}
String basicAuthHeader = "Basic " + Base64.getEncoder()
.encodeToString((apiKey.trim() + ":" + apiSecret.trim())
.getBytes(StandardCharsets.UTF_8));
String baseUrl = String.format(API_BASE_TEMPLATE, dataCenter.trim());
String searchUrl = baseUrl + "/search";
resourceResolver = workflowSession.adaptTo(ResourceResolver.class);
if (resourceResolver == null) {
log.error("Could not obtain ResourceResolver");
return;
}
String payloadPath = getCleanPath(workItem.getWorkflowData()
.getPayload().toString());
Resource payloadResource = resourceResolver.getResource(payloadPath);
if (payloadResource == null) {
log.warn("Resource not found: {}", payloadPath);
return;
}
String initiator = workItem.getWorkflow().getInitiator();
if (payloadPath.contains("/experience-fragments/")) {
processExperienceFragments(payloadResource, resourceResolver, publishUrl,
baseUrl, searchUrl, apiKey.trim(), basicAuthHeader, targetStatus,
initiator, debugMode, debugUrl);
} else if (payloadPath.contains("/content/dam/")) {
processDAMContent(payloadResource, resourceResolver, publishUrl,
baseUrl, searchUrl, apiKey.trim(), basicAuthHeader, targetStatus,
initiator, debugMode, debugUrl);
} else {
log.info("Unsupported content type at path: {}", payloadPath);
}
} catch (Exception e) {
log.error("MoEngage Sync Failed", e);
}
}
private void processExperienceFragments(Resource resource, ResourceResolver resolver,
String publishUrl, String baseUrl, String searchUrl, String apiKey,
String basicAuthHeader, String targetStatus, String initiator,
boolean debugMode, String debugUrl) throws Exception {
List<XFItem> items = collectExperienceFragments(resource, resolver);
if (items.isEmpty()) {
log.info("No experience fragments found");
return;
}
for (int i = 0; i < items.size(); i++) {
XFItem item = items.get(i);
try {
processExperienceFragment(item, resolver, publishUrl, baseUrl,
searchUrl, apiKey, basicAuthHeader, targetStatus, initiator,
debugMode, debugUrl);
if (i < items.size() - 1) {
Thread.sleep(BUFFER_DELAY_MS);
}
} catch (Exception e) {
log.error("Failed to process XF: {}", item.path, e);
}
}
}
private List<XFItem> collectExperienceFragments(Resource resource,
ResourceResolver resolver) {
List<XFItem> items = new ArrayList<>();
if (resource == null) return items;
Page page = resource.adaptTo(Page.class);
if (page != null) {
Iterator<Page> children = page.listChildren();
boolean hasChildren = children.hasNext();
if (hasChildren) {
children = page.listChildren();
while (children.hasNext()) {
Page variation = children.next();
items.add(new XFItem(variation.getPath(), page, variation));
}
} else {
Page parent = page.getParent();
if (parent != null && parent.listChildren().hasNext()) {
items.add(new XFItem(page.getPath(), parent, page));
}
}
} else {
Iterator<Resource> children = resource.listChildren();
while (children.hasNext()) {
items.addAll(collectExperienceFragments(children.next(), resolver));
}
}
return items;
}
private void processExperienceFragment(XFItem item, ResourceResolver resolver,
String publishUrl, String baseUrl, String searchUrl, String apiKey,
String basicAuthHeader, String targetStatus, String initiator,
boolean debugMode, String debugUrl) throws IOException {
String itemName = generatePathBasedName(item.path);
String rawContent = renderPageHtml(item.variation.getPath(), resolver);
rawContent = cleanContent(rawContent);
if (publishUrl != null && !publishUrl.isEmpty()) {
rawContent = externalizeLinks(rawContent, publishUrl);
}
if (rawContent.isEmpty()) {
log.info("Empty content for XF: {}, skipping", item.path);
return;
}
itemName = sanitizeName(itemName);
log.info("Processing XF: {} -> {}", item.path, itemName);
String existingId = searchContentBlockId(searchUrl, apiKey, basicAuthHeader,
itemName, debugMode, debugUrl);
JSONObject payload = new JSONObject();
if (existingId != null) {
payload.put("id", existingId);
payload.put("raw_content", rawContent);
payload.put("updated_by", initiator);
payload.put("status", targetStatus);
payload.put("description", "Sync from AEM: " + item.path);
sendRequest(baseUrl, "PUT", apiKey, basicAuthHeader,
payload.toString(), debugMode, debugUrl);
log.info("Updated content block: {}", itemName);
} else {
payload.put("name", itemName);
payload.put("label", itemName);
payload.put("content_type", "HTML");
payload.put("raw_content", rawContent);
payload.put("status", targetStatus);
payload.put("created_by", initiator);
payload.put("images_used", new JSONArray());
payload.put("content_block_used", new JSONArray());
payload.put("tag_ids", new JSONArray());
sendRequest(baseUrl, "POST", apiKey, basicAuthHeader,
payload.toString(), debugMode, debugUrl);
log.info("Created content block: {}", itemName);
}
}
private void processDAMContent(Resource resource, ResourceResolver resolver,
String publishUrl, String baseUrl, String searchUrl, String apiKey,
String basicAuthHeader, String targetStatus, String initiator,
boolean debugMode, String debugUrl) throws IOException {
String itemName = generatePathBasedName(resource.getPath());
String rawContent = "";
String contentType = "HTML";
ContentFragment cf = resource.adaptTo(ContentFragment.class);
if (cf != null) {
itemName = cf.getName();
rawContent = extractContentFromFragment(cf);
} else {
Asset asset = resource.adaptTo(Asset.class);
if (asset != null) {
itemName = asset.getName();
String mime = asset.getMimeType();
contentType = (mime != null && mime.contains("text/plain"))
? "Plain Text" : "HTML";
Rendition original = asset.getOriginal();
if (original != null) {
try (InputStream is = original.getStream()) {
rawContent = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
}
}
}
}
if (rawContent.isEmpty()) {
log.info("Empty content, skipping: {}", resource.getPath());
return;
}
itemName = sanitizeName(itemName);
log.info("Processing DAM content: {} -> {}", resource.getPath(), itemName);
String existingId = searchContentBlockId(searchUrl, apiKey, basicAuthHeader,
itemName, debugMode, debugUrl);
JSONObject payload = new JSONObject();
if (existingId != null) {
payload.put("id", existingId);
payload.put("raw_content", rawContent);
payload.put("updated_by", initiator);
payload.put("status", targetStatus);
sendRequest(baseUrl, "PUT", apiKey, basicAuthHeader,
payload.toString(), debugMode, debugUrl);
log.info("Updated content block: {}", itemName);
} else {
payload.put("name", itemName);
payload.put("label", itemName);
payload.put("content_type", contentType);
payload.put("raw_content", rawContent);
payload.put("status", targetStatus);
payload.put("created_by", initiator);
payload.put("images_used", new JSONArray());
payload.put("content_block_used", new JSONArray());
payload.put("tag_ids", new JSONArray());
sendRequest(baseUrl, "POST", apiKey, basicAuthHeader,
payload.toString(), debugMode, debugUrl);
log.info("Created content block: {}", itemName);
}
}
private String extractContentFromFragment(ContentFragment cf) {
if (cf.hasElement("htmlContent")) {
return cf.getElement("htmlContent").getContent();
}
if (cf.hasElement("master")) {
return cf.getElement("master").getContent();
}
if (cf.hasElement("body")) {
return cf.getElement("body").getContent();
}
StringBuilder sb = new StringBuilder();
Iterator<ContentElement> elements = cf.getElements();
while (elements.hasNext()) {
ContentElement el = elements.next();
if (!isMetadataField(el.getName())) {
sb.append(el.getContent()).append("\n");
}
}
return sb.toString().trim();
}
private boolean isMetadataField(String name) {
return name.matches("(?i).*(metadata|title|segment|offerCode).*");
}
private String generatePathBasedName(String path) {
if (path == null || path.isEmpty()) return "content_block";
String temp = path
.replace("/content/experience-fragments/", "")
.replace("/content/dam/", "")
.replace("/content/", "");
if (temp.contains("/jcr:content")) {
temp = temp.substring(0, temp.indexOf("/jcr:content"));
}
return temp.replaceAll("/", "_")
.replaceAll("-", "_")
.replaceAll("[^a-zA-Z0-9_]", "")
.replaceAll("_+", "_")
.replaceAll("^_|_$", "")
.toLowerCase();
}
private String sanitizeName(String name) {
if (name == null || name.isEmpty()) return "content_block";
return name.replaceAll("[^a-zA-Z0-9_]", "_")
.replaceAll("_+", "_")
.replaceAll("^_|_$", "")
.toLowerCase();
}
private String cleanContent(String html) {
if (html == null || html.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
Pattern linkPattern = Pattern.compile("<link[^>]*rel=[\"']stylesheet[\"'][^>]*>", Pattern.CASE_INSENSITIVE);
Matcher linkMatcher = linkPattern.matcher(html);
while (linkMatcher.find()) {
result.append(linkMatcher.group()).append("\n");
}
Pattern stylePattern = Pattern.compile("<style[^>]*>.*?</style>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher styleMatcher = stylePattern.matcher(html);
while (styleMatcher.find()) {
result.append(styleMatcher.group()).append("\n");
}
String bodyContent = html;
Pattern bodyPattern = Pattern.compile("<body[^>]*>(.*?)</body>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher bodyMatcher = bodyPattern.matcher(html);
if (bodyMatcher.find()) {
bodyContent = bodyMatcher.group(1);
} else {
bodyContent = html
.replaceAll("(?is)<!DOCTYPE[^>]*>", "")
.replaceAll("(?is)<html[^>]*>", "")
.replaceAll("(?is)</html>", "")
.replaceAll("(?is)<head[^>]*>.*?</head>", "")
.replaceAll("(?is)<body[^>]*>", "")
.replaceAll("(?is)</body>", "");
}
bodyContent = cleanAEMArtifacts(bodyContent);
result.append(bodyContent);
return result.toString().trim();
}
private String cleanAEMArtifacts(String content) {
if (content == null) return "";
return content
.replaceAll("(?is)<script[^>]*src=\"/etc\\.clientlibs/[^\"]*\"[^>]*></script>", "")
.replaceAll("\\s*data-cmp-data-layer=\"[^\"]*\"", "")
.replaceAll("\\s*data-cmp-clickable", "")
.replaceAll("(?is)<div[^>]*>\\s*</div>", "")
.replaceAll("\\n\\s*\\n\\s*\\n", "\n\n")
.trim();
}
private String renderPageHtml(String path, ResourceResolver resolver) {
try {
if (requestProcessor != null && requestResponseFactory != null) {
HttpServletRequest req = requestResponseFactory
.createRequest("GET", path + ".html");
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletResponse resp = requestResponseFactory.createResponse(out);
requestProcessor.processRequest(req, resp, resolver);
return out.toString(StandardCharsets.UTF_8.name());
}
} catch (Exception e) {
log.error("Failed to render: {}", path, e);
}
return "";
}
private String externalizeLinks(String content, String domain) {
if (content == null || domain == null) return content;
return content
.replaceAll("(src|href)=\"(/content/[^\"]+)\"", "$1=\"" + domain + "$2\"")
.replaceAll("(src|href)=\"(/etc\\.clientlibs/[^\"]+)\"",
"$1=\"" + domain + "$2\"");
}
private String searchContentBlockId(String url, String apiKey, String auth,
String name, boolean dbg, String dbgUrl) {
try {
JSONObject body = new JSONObject();
JSONObject filter = new JSONObject();
filter.put("search_text", name);
body.put("filters", filter);
APIResponse resp = sendRequest(url, "POST", apiKey, auth,
body.toString(), dbg, dbgUrl);
if (resp.statusCode >= 200 && resp.statusCode < 300) {
JSONObject json = new JSONObject(resp.body);
if (json.optInt("count") > 0) {
JSONArray data = json.getJSONArray("data");
for (int i = 0; i < data.length(); i++) {
JSONObject item = data.getJSONObject(i);
if (item.getString("name").equals(name)) {
return item.getString("id");
}
}
}
}
} catch (Exception e) {
log.debug("Search failed: {}", name, e);
}
return null;
}
private APIResponse sendRequest(String endpoint, String method, String apiKey,
String auth, String jsonPayload, boolean dbg, String dbgUrl)
throws IOException {
if (dbg && dbgUrl != null && !dbgUrl.isEmpty()) {
try {
sendDebug(jsonPayload, method, dbgUrl);
} catch (Exception e) {
log.debug("Debug send failed", e);
}
}
URL url = new URL(endpoint.trim());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("MOE-APPKEY", apiKey);
conn.setRequestProperty("Authorization", auth);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
}
int code = conn.getResponseCode();
String body = readResponse(conn);
return new APIResponse(code, body);
}
private void sendDebug(String payload, String method, String debugUrl) {
try {
URL url = new URL(debugUrl + "?method=" + method);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(payload.getBytes(StandardCharsets.UTF_8));
}
conn.getResponseCode();
} catch (Exception e) {
// Non-critical
}
}
private String readResponse(HttpURLConnection conn) {
try {
InputStream is = conn.getResponseCode() >= 400 ?
conn.getErrorStream() : conn.getInputStream();
if (is == null) return "";
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
return reader.lines().collect(Collectors.joining("\n"));
}
} catch (Exception e) {
return "";
}
}
private String getCleanPath(String path) {
if (path == null) return "";
if (path.endsWith("/jcr:content")) {
return path.substring(0, path.indexOf("/jcr:content"));
}
if (path.endsWith("/jcr:content/metadata")) {
return path.substring(0, path.indexOf("/jcr:content/metadata"));
}
return path;
}
private Map<String, String> parseProcessArgs(String args) {
Map<String, String> map = new HashMap<>();
if (args != null && !args.isEmpty()) {
for (String pair : args.split(",")) {
String[] parts = pair.split("=", 2);
if (parts.length == 2) {
map.put(parts[0].trim(), parts[1].trim());
}
}
}
return map;
}
private static class XFItem {
final String path;
final Page xfRoot;
final Page variation;
XFItem(String path, Page xfRoot, Page variation) {
this.path = path;
this.xfRoot = xfRoot;
this.variation = variation;
}
}
private static class APIResponse {
final int statusCode;
final String body;
APIResponse(int statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
}
}