/********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - Trek Global Corporation * * - Murilo Torino * **********************************************************************/ package com.trekglobal.idempiere.rest.api.webhook; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.compiere.model.PO; import org.compiere.util.CLogger; import org.compiere.util.Util; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** * Resolves a PayloadTemplate JSON string by replacing {@code @ColumnName@} * variables with values from a PO (Persistent Object). * *
* Template: {"order": "@DocumentNo@", "total": @GrandTotal@, "status": "@DocStatus@"}
* Result: {"order": "PO-001", "total": 1500.00, "status": "CO"}
*
*
* Note: For numeric/boolean values that should appear unquoted in JSON, * do NOT wrap them in quotes in the template: * {@code "total": @GrandTotal@} produces {@code "total": 1500.00} * {@code "total": "@GrandTotal@"} produces {@code "total": "1500.00"}
* * @author muriloht Murilo H. Torquato <murilo@muriloht.com> */ public class WebhookPayloadTemplateResolver { private static final CLogger log = CLogger.getCLogger(WebhookPayloadTemplateResolver.class); /** Pattern matching @ColumnName@ — column names are alphanumeric + underscore */ private static final Pattern VARIABLE_PATTERN = Pattern.compile("@([A-Za-z_][A-Za-z0-9_]*)@"); private WebhookPayloadTemplateResolver() { } /** * Resolve a payload template by substituting @ColumnName@ variables * with values from the given PO. * * @param template the JSON template string with @ColumnName@ placeholders * @param po the source persistent object to read values from * @return the resolved JSON string, or null if template is empty */ public static String resolve(String template, PO po) { if (Util.isEmpty(template, true) || po == null) return null; Matcher matcher = VARIABLE_PATTERN.matcher(template); StringBuffer result = new StringBuffer(); while (matcher.find()) { String columnName = matcher.group(1); String replacement = resolveColumn(po, columnName); // Escape backslashes and dollar signs for appendReplacement matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } matcher.appendTail(result); String resolved = result.toString(); // Post-process: remove entries whose value resolved to JSON null. // Follows the same convention as DefaultPOSerializer — null fields are omitted. try { JsonElement parsed = JsonParser.parseString(resolved); if (parsed.isJsonObject()) { removeNullEntries(parsed.getAsJsonObject()); return parsed.toString(); } } catch (Exception e) { log.log(java.util.logging.Level.WARNING, "Failed to parse webhook template JSON, returning resolved string as-is: " + e.getMessage()); } return resolved; } /** * Recursively remove entries whose value is JSON null * (produced by resolveColumn when the PO value is null and the template * placed the variable unquoted). */ private static void removeNullEntries(JsonObject obj) { for (Map.Entry