Intro
Routing in CPI can get messy. As more routing branches are added, maintaining them becomes a headache, especially when conditions get complex or frequently change.
Background / Problem
I’ve explored a few ways to reduce this pain.
One is convention-based processDirect routing, e.g. building endpoint dynamically from headers like: /PD_${header.mestyp}_${header.rcvprn}. It’s simple and router-free, but can’t handle complex routing conditions.
Another is using value mapping for routing, lookup a field to get the processDirect path. See link at [Ref 1] & [Ref 2]. But VM has serious limits:
Only supports single field matchingNo clean way to do AND logic without messy field concatenationCan’t return multiple matching rowsNo support for rich conditions like contains, startsWith, greaterThan, etc.
If you try to simulate those rich conditions in VM, it gets hacky, need to embed pseudo-logic in values, then decode with Groovy. That’s fragile and hard to maintain.
You could build your own rule engine, but that becomes a long-term maintenance trap. You’ll keep adding custom operators, fixing your own rule evaluation engine, and it won’t be standard. I wouldn’t go that route.
Inspiration
While revisiting the Dynamic Router pattern from Gregor Hohpe’s EIP book and Apache Camel, I realized: CPI should be able to support dynamic, flexible routing, if the conditions are externalized and evaluated at runtime.
The core idea of a true Dynamic Router:
Rules are externally maintainedEasy to add/remove/updateSupports rich conditions (not just equals)Still simple to implement
Camel is the backbone of CPI, so I turned to Camel’s Simple Expression language.
Research
After revisit a great blog [Ref 3] by Eng Swee, found out able to use evaluate() method to get value using Groovy, however, it doesn’t return single true or false.
Until at [Ref 4] Camel SimpleBuilder JavaDoc, found the method: matches()
Discovery
With SimpleBuilder.matches(), I can evaluate conditions like below with headers/properties in Groovy to Boolean true/false. Please note that for matches() method, Both (“and”, “or”) is not working, have to use actual operator (&&, II). Need double equal sign. Below 2 examples:
${header.type} == ‘SO’ && ${header.class} == ‘A’
${header.partNumber.startsWith(‘FG’)}
This opened up new possibilities and a clean path: I can store rules as just two fields:
endpoint (processDirect path)condition (simple expression)
Evaluate them in Groovy using matches(). If it returns true, route to that endpoint.
How it work: Dynamic Simple Expression matches()
You can build “Simple Expression Tester” to test simple expression from Postman.
Step 1: Build an iFlow like below. Allowed Header(s) must be *
Step 2: Add the script testSimpleExpression.groovy that do evaluate() and matches() on some Simple Expression:
import com.sap.gateway.ip.core.customdev.util.Message
import org.apache.camel.Exchange
import org.apache.camel.builder.SimpleBuilder
def Message processData(Message message) {
def headers = message.getHeaders()
Exchange ex = message.exchange
def evaluateSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).evaluate(ex, String)
}
def matchSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).matches(ex)
}
def condition = evaluateSimple(‘${header.condition}’)
def result = matchSimple(condition)
def output = “condition: ” + condition + “rn” + “result: ” + result
message.setBody(output)
return message
}
Step 3: Positive Test Run. Header class & type matched the condition, result = true
Step 4: Negative Test Run. Changed class from A to B, then result = false
Step 5: Tried with some other method like startsWith() still work. I not tried all possible method, however believe as long as the method work in Camel Simple Expression, will work in this iFlow too.
Dynamic Router with External Rule Set (Simple Expression)
After testing and a few iterations, this worked well. So now ready to build a mini rule engine that is simple, dynamic, and avoids all the earlier pain points.
Step 1: Below will be our rule set in JSON, telling the processDirect endpoint and condition. The name field is just as an identifier for the rule.
[
{
“name”: “ReceiverA”,
“endpoint”: “/PD_SO_A”,
“condition”: “${header.type} == ‘SO’ && ${header.class} == ‘A'”
},
{
“name”: “ReceiverB”,
“endpoint”: “/PD_PO_B”,
“condition”: “${header.type} == ‘PO’ && ${header.class} == ‘B'”
}
]
Step 2: Build below ‘Dynamic Router’ iflow. For demo purpose, the sender is HTTP adapter. In actual you may change to processDirect. Allowed Header(s) must be *
Step 3: That “getPDSimpleRule” script, for now you can just use content modifier set static JSON rule set to property “simpleRule”. We going to change to Partner Directory later.
Step 4: Script “doDynamicRouting”. This script loop each rule, if condition matched, set the endpoint in property, also do logging in custom headers for traceability.
import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonSlurper
import org.apache.camel.Exchange
import org.apache.camel.builder.SimpleBuilder
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
def properties = message.getProperties()
Exchange ex = message.exchange
def matchSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).matches(ex)
}
def simpleRule = message.getProperty(“simpleRule”)
def simpleRuleJSON = new JsonSlurper().parseText(simpleRule)
properties.put(“endpoint”, “/not_found_from_rule”)
for(rule in simpleRuleJSON){
def name = rule.name ?: “”
def endpoint = rule.endpoint ?: “”
def condition = rule.condition ?: “”
def result = matchSimple(condition)
if(result == true){
messageLog.addCustomHeaderProperty(“name”, name)
messageLog.addCustomHeaderProperty(“condition”, condition)
messageLog.addCustomHeaderProperty(“endpoint”, endpoint)
properties.put(“endpoint”, endpoint)
break
}
}
return message
}
Positive Test Run (Go to ReceiverA)
Positive Test Run (Go to ReceiverB)
Negative Test Run (When no rule matches)
Where To Store The Rule Set?
One possible way is to store Json rule set in Partner Directory. SAP CPI do have UI to upload JSON to Partner Directory. Below are steps to upload JSON to Partner Directory:
Step 1: At CPI Monitor, go to ‘Partner Directory’
Step 2: Click ‘+’ to add:
Step 3: Maintain like below, browse to JSON rule set file, then ‘Create’.
Step 4: After maintain, you should see like below:
Step 5: Replace the step ‘getPDSimpleRule’ with below script. This script get simpleRule JSON from Partner Directory.
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.pd.PartnerDirectoryService
import com.sap.it.api.ITApiFactory
import com.sap.it.api.pd.BinaryData
def Message processData(Message message) {
def service = ITApiFactory.getApi(PartnerDirectoryService.class, null);
if(service == null){
throw new IllegalStateException(“Partner Directory Service not found”);
}
def simpleRuleData = service.getParameter(“SimpleRule”, “DynamicRouter” , BinaryData.class)
def simpleRule = new String(simpleRuleData.getData())
message.setProperty(“simpleRule”, simpleRule)
return message
}
Step 6: If future need to maintain the JSON rule set, come here, Edit..
then just Browse and upload updated JSON rule file. Filename doesn’t matter, Partner Directory will auto rename.
Closing Summary
This approach doesn’t aim to replace complex rule engines, but for many real-world routing needs, it strikes the right balance: flexible, simple, and maintainable. If you’ve struggled with hardcoded routers or limited value mappings in CPI, this might be a cleaner path forward.
What do you think about this approach?
If you’ve tackled similar routing challenges in CPI, I’d be curious how you approached it. Feel free to share your thoughts, improvements, or alternative ideas.
Disclaimer: This is a pattern I sparked from EIP pattern(Dynamic Router) and made workable in CPI, but it hasn’t been used in real production environments yet. Use it at your own discretion.
References:
[Ref 1] IDOC Dispatching – Use case for the ProcessDirect Adapter from @ArielBravo
[Ref 2] SAP Cloud Integration IDoc Receiver Handler from @aayushaggarwal
[Ref 3] Using Camel’s Simple in CPI Groovy scripts from @engswee
[Ref 4] Class SimpleBuilder JavaDoc from Apache Camel
[Ref 5] EIPinCPI – Dynamic Router & EIPinCPI – Message Dispatcher from @bhalchandraswcg
[Ref 6] Get to know Camel’s Simple expression language in SAP Cloud Integration from @MortenWittrock
[Ref 7] DynamicRouter EIP from Enterprise Integration Patterns (https://www.enterpriseintegrationpatterns.com/)
IntroRouting in CPI can get messy. As more routing branches are added, maintaining them becomes a headache, especially when conditions get complex or frequently change. Background / ProblemI’ve explored a few ways to reduce this pain.One is convention-based processDirect routing, e.g. building endpoint dynamically from headers like: /PD_${header.mestyp}_${header.rcvprn}. It’s simple and router-free, but can’t handle complex routing conditions.Another is using value mapping for routing, lookup a field to get the processDirect path. See link at [Ref 1] & [Ref 2]. But VM has serious limits:Only supports single field matchingNo clean way to do AND logic without messy field concatenationCan’t return multiple matching rowsNo support for rich conditions like contains, startsWith, greaterThan, etc.If you try to simulate those rich conditions in VM, it gets hacky, need to embed pseudo-logic in values, then decode with Groovy. That’s fragile and hard to maintain.You could build your own rule engine, but that becomes a long-term maintenance trap. You’ll keep adding custom operators, fixing your own rule evaluation engine, and it won’t be standard. I wouldn’t go that route. InspirationWhile revisiting the Dynamic Router pattern from Gregor Hohpe’s EIP book and Apache Camel, I realized: CPI should be able to support dynamic, flexible routing, if the conditions are externalized and evaluated at runtime.The core idea of a true Dynamic Router:Rules are externally maintainedEasy to add/remove/updateSupports rich conditions (not just equals)Still simple to implementCamel is the backbone of CPI, so I turned to Camel’s Simple Expression language. ResearchAfter revisit a great blog [Ref 3] by Eng Swee, found out able to use evaluate() method to get value using Groovy, however, it doesn’t return single true or false.Until at [Ref 4] Camel SimpleBuilder JavaDoc, found the method: matches() DiscoveryWith SimpleBuilder.matches(), I can evaluate conditions like below with headers/properties in Groovy to Boolean true/false. Please note that for matches() method, Both (“and”, “or”) is not working, have to use actual operator (&&, II). Need double equal sign. Below 2 examples:${header.type} == ‘SO’ && ${header.class} == ‘A’
${header.partNumber.startsWith(‘FG’)}This opened up new possibilities and a clean path: I can store rules as just two fields:endpoint (processDirect path)condition (simple expression)Evaluate them in Groovy using matches(). If it returns true, route to that endpoint. How it work: Dynamic Simple Expression matches()You can build “Simple Expression Tester” to test simple expression from Postman.Step 1: Build an iFlow like below. Allowed Header(s) must be * Step 2: Add the script testSimpleExpression.groovy that do evaluate() and matches() on some Simple Expression:import com.sap.gateway.ip.core.customdev.util.Message
import org.apache.camel.Exchange
import org.apache.camel.builder.SimpleBuilder
def Message processData(Message message) {
def headers = message.getHeaders()
Exchange ex = message.exchange
def evaluateSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).evaluate(ex, String)
}
def matchSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).matches(ex)
}
def condition = evaluateSimple(‘${header.condition}’)
def result = matchSimple(condition)
def output = “condition: ” + condition + “rn” + “result: ” + result
message.setBody(output)
return message
}Step 3: Positive Test Run. Header class & type matched the condition, result = trueStep 4: Negative Test Run. Changed class from A to B, then result = falseStep 5: Tried with some other method like startsWith() still work. I not tried all possible method, however believe as long as the method work in Camel Simple Expression, will work in this iFlow too. Dynamic Router with External Rule Set (Simple Expression)After testing and a few iterations, this worked well. So now ready to build a mini rule engine that is simple, dynamic, and avoids all the earlier pain points.Step 1: Below will be our rule set in JSON, telling the processDirect endpoint and condition. The name field is just as an identifier for the rule.[
{
“name”: “ReceiverA”,
“endpoint”: “/PD_SO_A”,
“condition”: “${header.type} == ‘SO’ && ${header.class} == ‘A'”
},
{
“name”: “ReceiverB”,
“endpoint”: “/PD_PO_B”,
“condition”: “${header.type} == ‘PO’ && ${header.class} == ‘B'”
}
]Step 2: Build below ‘Dynamic Router’ iflow. For demo purpose, the sender is HTTP adapter. In actual you may change to processDirect. Allowed Header(s) must be *Step 3: That “getPDSimpleRule” script, for now you can just use content modifier set static JSON rule set to property “simpleRule”. We going to change to Partner Directory later.Step 4: Script “doDynamicRouting”. This script loop each rule, if condition matched, set the endpoint in property, also do logging in custom headers for traceability.import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonSlurper
import org.apache.camel.Exchange
import org.apache.camel.builder.SimpleBuilder
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
def properties = message.getProperties()
Exchange ex = message.exchange
def matchSimple = { simpleExpression ->
SimpleBuilder.simple(simpleExpression).matches(ex)
}
def simpleRule = message.getProperty(“simpleRule”)
def simpleRuleJSON = new JsonSlurper().parseText(simpleRule)
properties.put(“endpoint”, “/not_found_from_rule”)
for(rule in simpleRuleJSON){
def name = rule.name ?: “”
def endpoint = rule.endpoint ?: “”
def condition = rule.condition ?: “”
def result = matchSimple(condition)
if(result == true){
messageLog.addCustomHeaderProperty(“name”, name)
messageLog.addCustomHeaderProperty(“condition”, condition)
messageLog.addCustomHeaderProperty(“endpoint”, endpoint)
properties.put(“endpoint”, endpoint)
break
}
}
return message
} Step 5: For demo, build 2 different receivers with processDirect. Each with sender address: /PD_SO_A and /PD_PO_B. Positive Test Run (Go to ReceiverA) Positive Test Run (Go to ReceiverB) Negative Test Run (When no rule matches) Where To Store The Rule Set?One possible way is to store Json rule set in Partner Directory. SAP CPI do have UI to upload JSON to Partner Directory. Below are steps to upload JSON to Partner Directory:Step 1: At CPI Monitor, go to ‘Partner Directory’Step 2: Click ‘+’ to add:Step 3: Maintain like below, browse to JSON rule set file, then ‘Create’.Step 4: After maintain, you should see like below:Step 5: Replace the step ‘getPDSimpleRule’ with below script. This script get simpleRule JSON from Partner Directory.import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.pd.PartnerDirectoryService
import com.sap.it.api.ITApiFactory
import com.sap.it.api.pd.BinaryData
def Message processData(Message message) {
def service = ITApiFactory.getApi(PartnerDirectoryService.class, null);
if(service == null){
throw new IllegalStateException(“Partner Directory Service not found”);
}
def simpleRuleData = service.getParameter(“SimpleRule”, “DynamicRouter” , BinaryData.class)
def simpleRule = new String(simpleRuleData.getData())
message.setProperty(“simpleRule”, simpleRule)
return message
}Step 6: If future need to maintain the JSON rule set, come here, Edit..then just Browse and upload updated JSON rule file. Filename doesn’t matter, Partner Directory will auto rename. Closing SummaryThis approach doesn’t aim to replace complex rule engines, but for many real-world routing needs, it strikes the right balance: flexible, simple, and maintainable. If you’ve struggled with hardcoded routers or limited value mappings in CPI, this might be a cleaner path forward.What do you think about this approach?If you’ve tackled similar routing challenges in CPI, I’d be curious how you approached it. Feel free to share your thoughts, improvements, or alternative ideas.Disclaimer: This is a pattern I sparked from EIP pattern(Dynamic Router) and made workable in CPI, but it hasn’t been used in real production environments yet. Use it at your own discretion. References:[Ref 1] IDOC Dispatching – Use case for the ProcessDirect Adapter from @ArielBravo[Ref 2] SAP Cloud Integration IDoc Receiver Handler from @aayushaggarwal[Ref 3] Using Camel’s Simple in CPI Groovy scripts from @engswee[Ref 4] Class SimpleBuilder JavaDoc from Apache Camel[Ref 5] EIPinCPI – Dynamic Router & EIPinCPI – Message Dispatcher from @bhalchandraswcg[Ref 6] Get to know Camel’s Simple expression language in SAP Cloud Integration from @MortenWittrock[Ref 7] DynamicRouter EIP from Enterprise Integration Patterns (https://www.enterpriseintegrationpatterns.com/) Read More Technology Blog Posts by Members articles
#SAP
#SAPTechnologyblog