In our CA PPM journey, we get to pen down so many GEL scripts to work in all those CA PPM workflows. In many of them, we often xog out an object instance, modify some values or add some values to the xogged out xml and xog back the modified xml document to affect a change. One crucial part in this exercise is searching for the right node or element in the xogged out xml document. Me being a big zero in JAVA, I found using the XPath predicates too simple and handy for this purpose. Sharing the same in this blog post. Let's take this ride!
The Story Begins
It all began when I had a new requirement to modify the monthly values in a financial plan via XOG. As I had never used XPath predicates before, the task seemed almost impossible to me at first. Let us look at a sample Benefit Plan xog out below. Imagine trans_class as a custom attribute linked to a transaction class lookup, which we created in the "Benefit Plan Detail" object.
Now how will you navigate to the first of the three Detail sections you see here and extract one particular numerical value in the above picture? There can be many different ways to do this, like getting the first of these in this array list. But rather than trusting on any order or list, I will choose to use XPath predicates here. Please note that I have stored the xog read output under xogReadOutput variable.
Our Hero - XPath Predicate
If there was only one month segment under the Detail_1 section and if you are OK with filtering on that DetailName field, then something as simple as below will get you the monthly value.
<gel:set asString="true" select="$xogReadOutput//BenefitPlan/Details/Detail[@detailName='Detail_1']/Benefit/segment/@value" var="to_print"/>
The piece of code above highlighted in RED within square brackets is called as Predicate. It filters a node-set by evaluating the predicate expression on each node. As you see here, we filter on the detailName attribute verifying if it has a value of Detail_1.
But I don't want to filter by the detailName field as it is a name field. What if I would like to filter through each Detail section above by looking at that custom lookup attribute I had created in the Benefit Plan Detail object - trans_class? Here we can make use of Nested Predicates.
<gel:set asString="true" select="$xogReadOutput///BenefitPlan/Details/Detail/Benefit/segment[../../CustomInformation/ColumnValue[@name='trans_class']/text()='trans_class_1']/@value" var="to_print"/>
As you see above, we have used predicate inside a predicate here. The ../../ allows us to jump from the segment element to its grandparent Detail node and then go into the CustomInformation node. Unlike our screenshot above, trans_class may not be the only custom attribute there, so we filter again using another predicate to go into the trans_class columnValue element and use the text() to retrieve the value of it and validate it against the value we want to filter on, i.e. trans_class_1.
Now let us come to a real life scenario when you have multiple month segments as in the above screenshot. Here you will need more than one predicate joined together in AND logic.
<gel:set asString="true" select="$xogReadOutput///BenefitPlan/Details/Detail/Benefit/segment[@start='2018-04-01T00:00:00'][../../CustomInformation/ColumnValue[@name='trans_class']/text()='trans_class_1']/@value" var="to_print"/>
Multiple predicates are written here one after the other and they give us the effect of an AND condition. Only thing to remember here is that the predicate expressions must make sense to the single node/element against which they are applied. Just like here, we mention the first predicate without any path modification as 'start' is an attribute existing on the same segment element but the second predicate is appropriately modified and prefixed to reach the CustomInformation node in the hierarchy.
Predicates in GEL Scripts
Once you are clear until here, I assume now my blog post's header image will make sense to you. Attaching the same image below again. (Please click on it to enlarge it)
As you see, I am getting all the billing changes I need to do in the GEL via some SQL query named as "Billing", each row of which I am storing to "Billing_Changes".In the first gel:set line, I am checking for the existence of some billing values already for the particular transaction class and the particular month in the xogged out Benefit Plan.
The next core:choose code helps me to add the delta value to the existing Billing value and get the new Billing value to replace OR if there is no existing Billing value for that month, then just insert the delta value as such against that month along with the full segment element to lie under the Benefit node. Note here, how we use the same predicates again while inserting/replacing some values in the XML, not just for value extraction from the XML.
Some Basics for GEL Beginners
I am not sure if all my above explanation and code will make complete sense to a GEL script beginner. So some more details and GEL script tips just for them. Experts, please ignore
1. XPath search / and //
You might have noticed usage of both / and // in the above code. There is indeed a difference between the two. / is used when we know the parent element and when we construct the continuous parent-child path. But as in the above example, if you don't want to mention each of the nodes until the node you need and want to quickly jump to a particular node in the XML document, then we use the // search directive.
2. Beware of the two types of gel:set statements
Syntax alert! We use the one with asString="true" to extract some value or information from the XML document specified in the select attribute and store it in the variable we mention in the var attribute. We use the other one with insert="true" to insert/replace some information in the XML document at the location specified in the select attribute, with the information provided in the value attribute.
3. When to use the @ construct and the text()
In the above piece of code, you might have noticed usage of both @ and text() while extracting data from the XML document. It is simple. When you want to extract data from an XML attribute (like the value attribute in the segment element in our example), then we use @attribute. On the other hand, if your data lies within an XML element (like the ColumnValue element in our example), then we use the text().
4. Double check for element existence
Whenever you want to modify some information in an object instance and if you do it via the xogout-xogin method, make sure the value you want to replace/modify does exist in the xogged out XML document. It can very well happen that the particular attribute on the object instance is blank/null in which case a XOG READ output of that object instance will not contain the XML element at all. And then imagine you trying to navigate to that xml element assuming the xog out code will have it and trying to set a value inside it --- this will certainly result in an error. That's where the core:choose section in the above screenshot helps. It first checks for the existence of the xml element in the XOG OUT code, does some actions if it really exists there. It will also parse and create an entire XML element to insert into the appropriate node if it does not exist.
5. Never trust only on state="SUCCESS"
This final tip is a word of caution. While there are many GEL scripts I see around which check for each value against failureRecords, insertedRecords, totalNumberOfRecords, updatedRecords etc in the XOGOutput section which we get as a result of a XOG read/write operation, a much bigger number of people are lazy. Me too, sometimes! They tend to check only the state attribute in the Status element and if it shows SUCCESS, they gel:log it saying "hurray...we did it". But I have seen in many cases, especially while working on financial plans, that a SUCCESS state alone may not imply that the new financial plan has been updated or created. There could be reasons like an exchange rate not existing in the system etc. In all those cases, the ErrorInformation node in the XOG output tells us what went wrong. So it is best to add this one more additional line where you extract the ErrorInformation node and store it to display later when an error happens. Also, you could check for it being empty along with our state="SUCCESS" check. Refer the screenshot below.
That's it guys. We come to the conclusion part of this post. Will be back again if I find something tricky or useful. I know, this one was lengthy. But I assume this will be helpful to someone in this community.