In SAP Integration Suite, Message Processing Logs (MPL) are a primary tool for monitoring and troubleshooting interfaces.
While CPI provides technical details by default, integration developers often need business relevant information such as document numbers, PO numbers, statuses or reference ID from the payload to quickly search for specific messages in MPL.
To address this, many projects adopt custom header properties that are written into MPL and later used as search criteria in MPL view.
This blog presents a generic and reusable approach to populate such custom header properties, supporting both:
Custom Header Properties are key-value pairs explicitly written into the message processing logs using groovy.
Once added:
Example use cases:
This solution follow a configuration-first design:
No payload specific logic is hardcoded in the script.
| Prefix | Purpose |
| custH_ | Marks a header/property to be logged as a custom header property in MPL |
| Xpath_ | Indicates the value should be extracted dynamically from the XML Payload |
custH_StaticHeader = HelloWorld
custH_PONumber = Xpath_/PurchaseOrder/Header/DocumentNumber
custH_Status = Xpath_/PurchaseOrder/Status
This script is designed specifically for Static values and XML Payloads
It supports:
Scope Note: This script currently supports only static values and XML payloads, It does not process JSON, CSV or other non-XML formats.
The Groovy script was refined in multiple iterations with the help of AI, making it more generic and easier to extend for future enhancements.
// This script supports Groovy Verion 2, please upgrade if needed.
// Import required classes for message handling, XML parsing, and XPath processing
import com.sap.gateway.ip.core.customdev.util.Message
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import javax.xml.xpath.XPathFactory
import javax.xml.xpath.XPathConstants
import org.xml.sax.InputSource
import org.xml.sax.SAXException
import java.io.StringReader
import java.io.IOException
Message processData(Message message) {
def messageLog
// Attempt to obtain messageLog for logging
try {
messageLog = messageLogFactory.getMessageLog(message)
} catch (Exception e) {
return message
}
if (messageLog == null) return message
// Get the message body and check if it's XML
def body = message.getBody(String)
boolean isXML = body?.trim()?.startsWith("<")
def xmlDoc = null
def xpath = null
// Combine headers and properties into a single map for unified processing
def allInputs = [:]
allInputs.putAll(message.getHeaders())
allInputs.putAll(message.getProperties())
// PREPROCESS: Check if any 'custH_' keyed field will require XPath extraction.
// Only parse XML if at least one such case exists (for performance optimization).
boolean needsXmlParse = allInputs.any { k, v ->
k.startsWith("custH_") && v?.toString()?.trim()?.startsWith("Xpath_") && isXML
}
if (needsXmlParse) {
try {
def factory = DocumentBuilderFactory.newInstance()
factory.setNamespaceAware(false)
// --- Security Block: Prevent XXE attacks and other unsafe XML behaviors ---
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
factory.setFeature("http://xml.org/sax/features/external-general-entities", false)
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
factory.setXIncludeAware(false)
factory.setExpandEntityReferences(false)
// -------------------------------------------------------------------------
// Parse the XML document and prepare for XPath usage
xmlDoc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(body)))
xpath = XPathFactory.newInstance().newXPath()
} catch (ParserConfigurationException | SAXException | IOException | IllegalArgumentException e) {
// If secure parsing fails, fall back to not using XPath extraction
xmlDoc = null
xpath = null
}
}
// MAIN LOGIC: Iterate over all combined header/property fields
allInputs.each { key, value ->
if (!key.startsWith("custH_")) return // Only process custom header entries
def headerName = key.substring(6) // Remove prefix to get the real header/property name
def val = value?.toString()?.trim()
if (!val) return
// CASE 1: Static value - always log as custom header property
if (!val.startsWith("Xpath_")) {
messageLog.addCustomHeaderProperty(headerName, val)
return
}
// CASE 2: XPath value - extract from body using constructed XPath, if XML and parser available
if (isXML && xmlDoc && xpath) {
def rawXpath = val.substring(6)
def finalXpath = buildXPath(rawXpath) // Build namespace-agnostic XPath from supplied string
if (finalXpath) {
try {
// Evaluate the XPath and extract all resulting node values
def nodes = xpath.evaluate(finalXpath, xmlDoc, XPathConstants.NODESET)
if (nodes && nodes.getLength() > 0) {
for (int i = 0; i < nodes.getLength(); i++) {
def nodeVal = nodes.item(i)?.getTextContent()?.trim()
if (nodeVal) {
messageLog.addCustomHeaderProperty(headerName, nodeVal)
}
}
}
} catch (Exception ignore) { /* Never fail for XPath extraction errors */ }
}
}
}
// Return message object at end of processing
return message
}
/*
* buildXPath: Build a namespace-agnostic XPath expression from a path string.
* Supports attributes and repeated elements. Used by main function to simplify XPath field extraction even in the presence of XML namespaces.
*/
String buildXPath(String xp) {
try {
// Tokenize and filter out empty tokens
return xp.tokenize('/').findAll { it }.collect { token ->
def element = token, idx = "", attr = ""
// Handle attribute syntax in each token
if (element.contains("@")) {
def parts = element.split("@")
element = parts[0]
attr = parts[1]
}
// Handle index in element (e.g., /Header/Item[3]/Field/@attribute)
if (element == "") {
// In case of /@attr at the end
return attr ? "/@${attr}" : ""
} else {
def matcher = (element =~ /^(.+?)(\[(\d+)\])?$/)
if (matcher.matches()) {
element = matcher[0][1]
idx = matcher[0][2] ?: ""
}
def nodeExpr = "/*[local-name()='${element}']"
nodeExpr += idx
if (attr) {
nodeExpr += "/@${attr}"
}
return nodeExpr
}
}.join('')
} catch (Exception e) {
return ""
}
}The following XML Payload used to validate the Groovy script across multiple scenarios:
<ns0:PurchaseOrder xmlns:ns0="urn:sap-com:document:sap:rfc:functions" xmlns:sf="urn:sap-com:document:sap:soap:functions:mc-style">
<ns0:Header>
<ns0:DocumentNumber>4500001234</ns0:DocumentNumber>
<ns0:CreatedBy>Jane Doe</ns0:CreatedBy>
<ns0:ApprovalStatus code="APPROVED"/>
<ns0:Note/>
</ns0:Header>
<ns0:Items>
<ns0:Item>
<ns0:PosNumber>10</ns0:PosNumber>
<ns0:Material>MAT-ABC</ns0:Material>
<ns0:Quantity unit="EA">5</ns0:Quantity>
</ns0:Item>
<ns0:Item>
<ns0:PosNumber>20</ns0:PosNumber>
<ns0:Material>MAT-XYZ</ns0:Material>
<ns0:Quantity unit="BOX">2</ns0:Quantity>
</ns0:Item>
<ns0:Item>
<ns0:PosNumber>30</ns0:PosNumber>
<ns0:Material>MAT-EMP</ns0:Material>
<ns0:Quantity unit="EA"/>
</ns0:Item>
</ns0:Items>
<sf:Status statusCode="CONFIRMED">Order Confirmed</sf:Status>
<ns0:References>
<ns0:Reference referencetype="Customer">CUST-001</ns0:Reference>
<ns0:Reference referencetype="Supplier">SUP-002</ns0:Reference>
</ns0:References>
</ns0:PurchaseOrder>The following headers/properties were configures in content modifier to test the script:
| Header/Property Key | Value | Case |
| custH_StaticHeader | HelloWorld | Static (non-XPath) |
| custH_Materials | Xpath_/PurchaseOrder/Items/Item/Material | Node set (multiple elements) |
| custH_QtyUnit | Xpath_/PurchaseOrder/Items/Item/Quantity@unit | Attribute extraction (all items) |
| custH_PONum | Xpath_/PurchaseOrder/Header/DocumentNumber | Single value (element) |
| custH_StaticProperty | HelloWorld | Static (non-XPath) |
| custH_Approval | Xpath_/PurchaseOrder/Header/ApprovalStatus@code | Attribute value |
| custH_PosNumbers | Xpath_/PurchaseOrder/Items/Item/PosNumber | Repeated values |
| custH_CustRef | Xpath_/PurchaseOrder/References/Reference@referencetype | All attributes in reference nodes |
| custH_ThirdQtyUnit | Xpath_/PurchaseOrder/Items/Item[3]/Quantity@unit | Specific index value |
| custH_EmptyNote | Xpath_/PurchaseOrder/Header/Note | Empty element (does not create if value is blank) |
| custH_Status | Xpath_/PurchaseOrder/Status | Namespaced element (content) |
| custH_StatusAttr | Xpath_/PurchaseOrder/Status@statusCode | Namespaced attribute |
After execution, the following custom header properties were visible in the Message Processing Log:
The following scenarios were also tested to ensure robustness:
Note: This groovy script only logs custom header properties for MPL search and passes the input payload unchanged to the output.
This generic Groovy Script provides a simple, reusable, and configuration-driven way to log custom header properties for static values and dynamic XML Payloads, improving message processing log search and operation visibility in SAP CPI without impacting the message payload.
If anything is not working as expected or if you see scope for enhancement in the script, feel free to drop a comment.
Happy Integrating!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 26 | |
| 25 | |
| 21 | |
| 20 | |
| 19 | |
| 14 | |
| 14 | |
| 14 | |
| 14 | |
| 10 |