Acorel
Gratis demo

Adding custom fields to an iflow using a groovy script

Pieter Rijlaarsdam

Integration between two SAP cloud systems is usually done by implementing one or more iflows in SAP Cloud Platform Integration (CPI).

Whether this concerns an integration of SAP Cloud for Customer and SAP Marketing Cloud or for instance integration between SAP Cloud for Customer and SAP Field Service Management, you will usually first implement the standard iflow, and enhance it to support additional fields.

In order to maintain upward compatibility, it is advised not to change the standard iflows as delivered by SAP. Instead of this, in most cases, SAP has built in an ‘Exit iflow’ within the standard iflow.

Exit-iflows

This so-called ‘Exit flow’ is a step in the integration process where the standard flow calls another iflow.

When implementing the standard, this other iflow does not exist. By creating an iflow ‘listening’ to the specified URL, you have the ability to implement your own logic whilst leaving the standard flow unchanged (allowing you to blindly update the iflow if SAP rolls-out updates).

Most integration guides contain a link to some usefull blogs on the topic that should get you kick-started on implementing the exit-iflow.

For instance, in the standard SAP help documentation on SAP Cloud for Customer to SAP Field Service Management integration, there is a link to this guide.

After that, there is a part 2 and a part 3

Having read these, conclusion should be that this is fairly easy.

You take the enhanced WSDL’s from the source and target system (or you manually enhance the files downloaded from the main iflow), upload them in the exit iflow, draw the lines, and you’re done.

But what if the standard iflow does not contain a standard mapping? What if there are no source and target WSDL’s? What if the standard iflow uses a groovy script for mapping?

-Total panic- No documentation whatsoever.

Groovy script mapping

If you think this is complex:

, Adding custom fields to an iflow using a groovy script, Acorel

it’s your lucky day. While implementing the iflow for tickets between SAP Cloud for Customer and SAP Field Service Management, I ran into this fancy piece of groovy-script-mapping:

import com.sap.gateway.ip.core.customdev.util.Message;
import com.sap.it.api.ITApiFactory
import com.sap.it.api.mapping.ValueMappingApi

def Message processData(Message message) {
def body = message.getBody(String.class);
root = new XmlParser().parseText(body);
valueMapApi = ITApiFactory.getApi(ValueMappingApi.class, null)

def action, prob_type, team_role; Node sc_data, sc_item, var_node
root_output = new NodeBuilder().Service_Requests{}
boolean recon_ind = root?.MessageHeader?.ReconciliationIndicator?.text()?.equals("true") ? true : false

root.ServiceRequestReplicationRequestMessage.each {
sr = it.ServiceRequest
action = sr.BTDReference.BTDReference.find({node -> node.BusinessSystemID.text().equals(root.MessageHeader.RecipientParty.text())}) == null ? 'CREATE' : 'UPDATE'

if(sr[0].@ServiceSupportTeamIndicator.equals('true')) {
team_role = "28"
}else {
team_role = "42"
}
var_node = sr.Party.find({node -> node.RoleCode.text().equals(team_role)})
team_role = var_node == null ? null : var_node.PartyID.text()
team_role = valueMapApi.getMappedValue('C4C', 'Service_technician_team', team_role, 'FSM', 'REGION')

if(message.getProperty("serv_categ_val_map").toLowerCase().trim().equals('true')) {
prob_type = valueMapApi.getMappedValue('C4C', 'Catalog#Version#CategoryID',
sr.ServiceCategory.CatalogueID.text() + '#' + sr.ServiceCategory.VersionID.text() + '#' + sr.ServiceCategory.ID.text(),'FSM', 'Problem_Type')
prob_type = prob_type == null ? "" : prob_type
}else {
prob_type = sr.ServiceTerms.ServiceCategoryText.text()
}

sc_data = new NodeBuilder().data(action: action){
externalId(sr.ID.text())
subject(sr.Name.text())
origin(sr.ReceiverDataOriginTypeText.text())
status(sr.ServiceTerms.ReceiverUserStatusText.text())
priority(sr.ServiceTerms.ReceiverPriorityText.text())
problemType(prob_type)
type(sr.ReceiverProcessingTypeText.text())
earliestStartDateTime(sr.TicketTimeline.RequestedStart.text())
dueDateTime(sr.TicketTimeline.ResolutionDueDate.text())
equipments() //Filled below
durationInMinutes(sr.TicketTimeline.Duration.text().equals("")? 0: Math.round(Math.floor(sr.TicketTimeline.Duration.text().toDouble())))
resolution(sr.TextCollection.Text.find({node -> node.TypeCode.text().equals("10022")})?.ContentText?.text() ?: "")
remarks(sr.TextCollection.Text.find({node -> node.TypeCode.text().equals("10004")})?.ContentText?.text() ?: "")
address{
building(sr.Location.BuildingID.text())
city(sr.Location.CityName.text())
country(sr.Location.CountryCode.text())
floor(sr.Location.FloorID.text())
room(sr.Location.RoomID.text())
state(sr.Location.RegionCode.text())
street(sr.Location.StreetName.text())
streetNumber(sr.Location.HouseID.text())
zipCode(sr.Location.StreetPostalCode.text())
}

}

// Business Partner role code 1001 with Main indicator and that BP's main contact
var_node = sr.Party.find({node -> node.RoleCode.text().equals('1001') && node.MainIndicator.text().equals('true')})
if(var_node != null) {
sc_data.append(new NodeBuilder().businessPartner{externalId(var_node.PartyID.text())})
var_node = var_node.ContactParty.find({node -> node.MainIndicator.text().equals('true')})
if(var_node.equals(null)) {
sc_data.append(new NodeBuilder().contact{})
}else {
sc_data.append(new NodeBuilder().contact{externalId(var_node.PartyID.text() + '~ID-JOIN~' + sc_data.businessPartner[0].externalId.text())})
}
}

// Responsible role code 40 with Main indicator
var_node = sr.Party.find({node -> node.RoleCode.text().equals('40') && node.MainIndicator.text().equals('true')})
if(var_node != null) {
sc_data.append(new NodeBuilder().responsibles{externalId(var_node.PartyID.text())})
}

//Replicate Main ipoint of RP or Func loc(if param is true)
var_node = sr.ServiceReferenceObjects.find({node -> node.MainIndicator.text().equals('true')})
if(var_node != null) {
if((var_node.InstallationPointTypeCode.text().equals('6')
&& message.getProperty("func_loc_enabled").toLowerCase().trim().equals('true')) ||
var_node.InstallationPointTypeCode.text().equals('2')) {
sc_data.equipments[0].append(new NodeBuilder().externalId(var_node.InstallationPointID.text()))
}
}

sr.TicketSkill.findAll({node -> node.SkillSource.text().equals('07') || node.SkillSource.text().equals('06')}).each{
// 07-Manual, 06-IBase
var_node = it
sc_data.append(new NodeBuilder().requirements{
mandatory(var_node?.Mandatory?.text())
skill{
externalId(var_node?.SkillID?.text())
}
})
}
//If RP skill is not determined then only check for Products' skills
if(sr.TicketSkill.find({node -> node.SkillSource.text().equals('01')}) == null) {
sr.TicketSkill.findAll({node -> node.SkillSource.text().equals('02')}).each{
// 02-Product
var_node = it
sc_data.append(new NodeBuilder().requirements{
mandatory(var_node.Mandatory.text())
skill{
externalId(var_node.SkillID.text())
}
})
}
}

for(item in sr.Item) {
if(item.FSMRelevanceCode.text().equals('1')) {
sc_item = new NodeBuilder().activities(){
externalId(item.UUID.text())
subject(item.Description.text())
remarks(item.ItemtextCollection.Text.find({node -> node.TypeCode.text().equals("10011")})?.ContentText?.text() ?: "")
}

var_node = item.ItemScheduleLines.find({node -> node.TypeCode.text().equals('1')})
if(var_node != null) {
sc_item.append(new NodeBuilder().earliestStartDateTime(var_node.DateTimePeriod.StartDateTime.text()))
sc_item.append(new NodeBuilder().dueDateTime(var_node.DateTimePeriod.EndDateTime.text()))
if(!var_node.Quantity.text().equals("")) {
sc_item.append(new NodeBuilder().durationInMinutes(Math.round(Math.floor(var_node.Quantity.text().toDouble()))))
}
}

if((item.ItemServiceReferenceObjects.InstallationPointTypeCode.text().equals('6')
&& message.getProperty("func_loc_enabled").toLowerCase().trim().equals('true')) ||
item.ItemServiceReferenceObjects.InstallationPointTypeCode.text().equals('2')) {
sc_item.append(new NodeBuilder().equipment{externalId(item.ItemServiceReferenceObjects.InstallationPointID.text())})
}

item.TicketItemSkill.findAll({node -> node.SkillSource.text().equals('07') || node.SkillSource.text().equals('06')}).each{
// 07-Manual, 06-IBase
var_node = it
sc_item.append(new NodeBuilder().requirements{
mandatory(var_node?.Mandatory?.text())
skill{
externalId(var_node?.SkillID?.text())
}
})
}
//If RP skill is not determined then only check for Products' skills
if(item.TicketItemSkill.find({node -> node.SkillSource.text().equals('01')}) == null) {
item.TicketItemSkill.findAll({node -> node.SkillSource.text().equals('02')}).each{
// 02-Product
var_node = it
sc_item.append(new NodeBuilder().requirements{
mandatory(var_node.Mandatory.text())
skill{
externalId(var_node.SkillID.text())
}
})
}
}

if ((!team_role.equals(null)) && (item?.ServiceItemAdditionalInfo?.ServiceItemUpdated?.text()?.equals("false") || recon_ind.equals(true)
|| sr[0]?.@TeamChangeIndicator?.equals("true") || sr[0]?.@ReleaseToFSMIndicator?.equals("true"))) {
sc_item.append(new NodeBuilder().region{code(team_role)})
}
sc_data.append(sc_item)
}
}

// Reserved Materials mapping
for(item in sr.Item) {
if(item.FSMRelevanceCode.text().equals('2')) {
var_node = item.ItemScheduleLines.find({node -> node.TypeCode.text().equals('1')})
sc_item = new NodeBuilder().reservedMaterials(){
externalId(item.UUID.text())
warehouse{
code(message.getProperty("FSM_Default_Warehouse_Code"))
}
quantity(item.ServiceTransactionProcessingTypeCode.text().equals('0004') && sr?.ReadATPConfirmedQtyIndicator?.text().equals('true')? item.ConfirmedQuantity.text() : var_node.Quantity.text())
}

if(!item.ItemProduct.ProductInternalID.text().equals("")) {
sc_item.append(new NodeBuilder().item{externalId(item.ItemProduct.ProductInternalID.text())})
}

sc_data.append(sc_item)
}
}

root_output.append(sc_data)
}

StringWriter stringWriter = new StringWriter()
XmlNodePrinter nodePrinter = new XmlNodePrinter(new PrintWriter(stringWriter))
nodePrinter.setPreserveWhitespace(true)
nodePrinter.print(root_output)
message.setBody(stringWriter.toString())
return message;

}

Essentially, this piece of gibberish reads the incoming message and creates a totally different outgoing message.

And you know what? It actually works.

Mind you that that all is behind this little box:

, Adding custom fields to an iflow using a groovy script, Acorel

The SAP Cloud for Customer to SAP Field Service Management Ticket Exit iflow

So let’s take a look at our exit-iflow.

As you can see in the screenshot, the exit iflow is only called if the iflow is configured (and deployed) with the ‘isextended’ parameter set to true.

If so, a sub-process is called.

, Adding custom fields to an iflow using a groovy script, Acorel

From left to right, in some understandable words:

The first block states that furter processing should be for node ‘Service_Requests’ only. This is a subnode in the output generated by the complex groovy mapping thing.

The second block basically concatenates the original payload (so the original incoming message) to the current message body.


<ns1:Messages xmlns:ns1="http://sap.com/xi/XI/SplitAndMerge">
<ns1:Message1>
${property.P_OriginalPayload}
</ns1:Message1>
<ns1:Message2>
${in.body}
</ns1:Message2>
</ns1:Messages>

The third block calls the actual exit iflow on URL /C4C/FSM/ReplicateTicket_Exit.

Implementing our exit iflow

So I create an exit iflow and run the trace. What you get is an XML containing 2 messages. The original message (containing my custom fields from SAP Cloud for Customer by the way, thank G*D) and the designated output message.

Fair enough, sound like a pretty straightforward plan… The lower part of the XML (Message2) should be updated to also include the custom data from the upper part of the XML (Message1).

The custom fields should be fitted into a udfValues node like this (from the documentation):

, Adding custom fields to an iflow using a groovy script, Acorel

Mapping appears out of the question, so let’s implement a similar Groovy Script, but let’s try to keep it a bit simpler.

Trust me, it took me a while to figure this out, but let me give it to you:


import com.sap.gateway.ip.core.customdev.util.Message;

def Message processData(Message message) {
def body = message.getBody(String.class);
root = new XmlParser().parseText(body);

def nsn1 = new groovy.xml.Namespace("http://sap.com/xi/AP/CustomerExtension/BYD/A0BGN")
def field1, field2, field3

root.each {
if (it.name().getLocalPart() == "Message1"){
  it.each {
    it.each{
    if (it.name() == "ServiceRequestReplicationRequestMessage"){
       field1 = it.ServiceRequest[nsn1.Z_field1].text()
       field2 = it.ServiceRequest[nsn1.Z_field2].text()
       field3 = it.ServiceRequest[nsn1.Z_field3].text()
       }
    }
  }
}
if (it.name().getLocalPart() == "Message2"){
   it.each {
      it.each{
         udfvalues = new NodeBuilder().udfvalues{}
         udfvalues.append(new NodeBuilder().udfMeta(){externalId("Z_field1")
         udfvalues.append(new NodeBuilder().value(field1)
         it.append(udfvalues)

         udfvalues = new NodeBuilder().udfvalues{}
         udfvalues.append(new NodeBuilder().udfMeta(){externalId("Z_field2")
         udfvalues.append(new NodeBuilder().value(field2)
         it.append(udfvalues) 

         udfvalues = new NodeBuilder().udfvalues{}
         udfvalues.append(new NodeBuilder().udfMeta(){externalId("Z_field3")
         udfvalues.append(new NodeBuilder().value(field3)
         it.append(udfvalues) 
      }
   }
}
}

StringWriter stringWriter = new StringWriter()
XmlNodePrinter nodePrinter = new XmlNodePrinter(new PrintWriter(stringWriter))
nodePrinter.setPreserveWhitespace(true)
nodePrinter.print root
message.setBody(stringWriter.toString())
return message;

}

Note that I have used field1, field2 and field3 where you would normally use proper fieldnames.

What this does, is that it

  • First parses the XML.
  • Then loops over the messages (first processing Message1 and then Message2).
  • Finds the values of the 3 custom fields.
  • Finds the location for the new values.
  • Creates the new nodes and sets the values.
  • Returns the data to the string and returns the message.

So the result is now an XML containing the original message (Message1) and the designated output (Message2) with the additional fields.

This is then returned to the standard flow, where part of the message is filtered out for further processing:

, Adding custom fields to an iflow using a groovy script, Acorel

Note that the standard flow now tries to filter on the node ‘Service_Requests’ (which is part of the output structure) in Message1 (which is the input structure).

As well as the intentions might be, I can already tell you this will not work. I expect this to be changed by SAP soon. This of course should be ‘Message2’.

The idea is then clear.

, Adding custom fields to an iflow using a groovy script, Acorel

 

So, despite the headbreaking puzzle of the custom groovy script I amglad I was able to share this with you eventually.

I can also imagine that you have other complex challenges (I also had a few more not worth mentioning here).

If so, feel free to drop a comment to see if we can help out.

Pieter Rijlaarsdam

Meer nieuws