Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
Daggolu_PremSai
Participant
1,952

Introduction

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:

  • Static values
  • Dynamic values extracted from XML payloads

 

What are Custom Header Properties in MPL?

Custom Header Properties are key-value pairs explicitly written into the message processing logs using groovy.

Once added:

  • They appear in the MPL Custom Headers section
  • They can be used to search and filter messages
  • They significantly reduce time spent opening and inspecting payloads

Example use cases:

  • Search messages by Purchase Order Number
  • Filter failed messages by business status
  • Trace a message by external reference ID

 

Design Overview

This solution follow a configuration-first design:

  • Developers configure headers or properties with a specific prefix
  • A single groovy script reads these configuration
  • Values are logged into MPL as custom header properties

No payload specific logic is hardcoded in the script.

Naming Convention Used
PrefixPurpose
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
Example Configuration

custH_StaticHeader = HelloWorld
custH_PONumber = Xpath_/PurchaseOrder/Header/DocumentNumber
custH_Status = Xpath_/PurchaseOrder/Status

How this works:
  • static values are logged as-is
  • XPath-based values are evaluated against the XML payload
  • Each extracted values is written into MPL as a custom header property
Dynamic XML Payload Support

This script is designed specifically for Static values and XML Payloads

It supports:

  • Namespace-agnostic XPath (using local-name()) - no need to provide namespace in XPath.
  • Repeating nodes (NodeSet extraction)
  • Attributes and element values
  • Optional and empty elements
  • Real-world SAP XML structures with multiple namespaces

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 ""
    }
}
Test Payload Used

The following XML Payload used to validate the Groovy script across multiple scenarios:

  • Multiple namespaces
  • Repeating elements
  • Attributes and element values
  • Optional and empty nodes
  • Nested structures
<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>
Test Configuration

The following headers/properties were configures in content modifier to test the script:

Header/Property KeyValueCase
custH_StaticHeaderHelloWorldStatic (non-XPath)
custH_MaterialsXpath_/PurchaseOrder/Items/Item/MaterialNode set (multiple elements)
custH_QtyUnitXpath_/PurchaseOrder/Items/Item/Quantity@unitAttribute extraction (all items)
custH_PONumXpath_/PurchaseOrder/Header/DocumentNumberSingle value (element)
custH_StaticPropertyHelloWorldStatic (non-XPath)
custH_ApprovalXpath_/PurchaseOrder/Header/ApprovalStatus@codeAttribute value
custH_PosNumbersXpath_/PurchaseOrder/Items/Item/PosNumberRepeated values
custH_CustRefXpath_/PurchaseOrder/References/Reference@referencetypeAll attributes in reference nodes
custH_ThirdQtyUnitXpath_/PurchaseOrder/Items/Item[3]/Quantity@unitSpecific index value
custH_EmptyNoteXpath_/PurchaseOrder/Header/NoteEmpty element (does not create if value is blank)
custH_StatusXpath_/PurchaseOrder/StatusNamespaced element (content)
custH_StatusAttrXpath_/PurchaseOrder/Status@statusCodeNamespaced attribute
Properties

Daggolu_PremSai_2-1767181407484.png

Headers

 

Daggolu_PremSai_3-1767181454632.png

After execution, the following custom header properties were visible in the Message Processing Log:

Daggolu_PremSai_4-1767181605329.png

The following scenarios were also tested to ensure robustness:

  • Invalid Xpath - Script continues without failure
  • Missing XML Payload - XPath extraction skipped
  • Header/Property without custH_ prefix - Ignored by script
  • Static value without Xpath_ - Logged directly
  • Large Payload - XML parsed only once
  • XXE content - Blocked by secure XML parsing

Note: This groovy script only logs custom header properties for MPL search and passes the input payload unchanged to the output.

Conclusion: 

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!