It's 3:00 P.M. on a sunny Saturday afternoon. The birds are chirping, the leaves are blowing, and you can hear the lake waters breaking on its rocky shores. The sounds of a baseball game randomly crack in the distance, and the roar of competition erupts on the basketball courts nearby.
My burgers are just getting medium-well as my wife is returning from a little potty walk with the dog. Our blanket is set and our picnic looks like it will be wonderful. Who knew a Web geek like me could pull off such a seemingly perfect day? Interestingly enough, I owe much of its success to XML and XPath.
XPath is to XML what SQL is to databases. Databases would be quite pointless if you could not query information out of them, and the same holds true for XML documents. XPath is a language for finding information in XML documents. XPath provides access to all of the elements and attributes of an XML document. It became a World Wide Web Consortium (W3C) recommendation on November 16, 1999 and since then it has become a huge part of the XML world. It is a major element of the W3C's XSLT standard, and both XQuery and XPointer are built upon XPath expressions.
An expression is XPath's primary construct, a string that, in its basic form, resembles a file path. True XPath engines examine the expression and return a node-set (more on nodes later), Boolean, number, or string from the XML document. ColdFusion's implementation of XPath only supports the node-set return type. For more complete XPath support you can use one of the many Java libraries available on the Internet.
XML has reared its head all over the Web, and with XPath being such a cornerstone of the XML world, a solid understanding of it is crucial. XML can be found any place where you need to provide or acquire data from a third-party vendor, client, application, or server. Web services, WDDX packets, and RSS feeds are all implemented on XML technology. XPath could be used to integrate various combinations of any of these technologies. A good example of this type of integration was the weekend planner application I used to find the perfect Saturday activity.
The application reads an RSS feed of a local community events calendar. It leverages XPath to read the zip codes and dates of each event. These zip codes and dates are then sent to a Web service that provides the weather forecast for that area of town, on that day. That information showed me that a picnic at Blanchard Park on Saturday was a better idea than going to the carnival at the Central Florida Fair Grounds on Sunday.
XPath can be used anytime you need a subset of data from a larger XML dataset. Its use is analogous to the use of Regular Expressions. Regular Expressions retrieve a substring based on pattern matching within a string. They are very powerful, although one might not think so at first look. This is because ColdFusion implements Regular Expressions as a single argument used in a small handful of functions. A deeper look into Regular Expressions would reveal book after book dedicated to its intricacies. In the same manner, XPath makes its ColdFusion appearance as a lone argument of one function, yet has novels dedicated to its deeper functionality.
XPath is leveraged via the function XmlSearch(). XmlSearch() requires two parameters: xmlDoc; an XML Object, and XPathString; and XPath expression. It returns an array, with each element of the array containing an XML node. As stated before, XmlSearch() cannot return strings, Booleans, or numbers.
The first parameter of XmlSearch() is an XML Object. When XML is read in by ColdFusion it is treated just like any other string, however calling the function XmlParse() on that string will load it into memory, create, and return a ColdFusion XML Object. Figure 1 shows the comparison of an unparsed XML document dumped on the left, and the same XML document parsed and dumped on the right.
Now that our XML has been converted to an object, we can run our first XPath expression against it. In order to understand how to write XPath expressions, we must understand and identify the different parts and elements that make up an XML document.
The easiest way to identify the parts of an XML document is to compare it to a file structure. If we were to run the command "C:\inetpub\wwwroot" in Windows, the OS would open the "wwwroot" directory, showing all of its contents. In this example, "C:" of course is a reference to the disk, and "inetpub\wwwroot" tells the system to look into the "wwwroot" folder, which is nested inside the "inetpub" folder. As nomenclature of nested sets goes, it could be said that "inetpub" is the parent "wwwroot." Likewise, "wwwroot" is the child of "inetpub." Extending that paradigm out, "C:" would then be a grand parent of "wwwroot." In such instances, "C:" would be known as an ancestor and "wwwroot" would be known as a descendant. Since "C:" is the absolute oldest ancestor or the top node in the nested set, it is also given a special name - root.
If we were to change our command to "C:/inetpub" and run it, the "inetpub" folder would open showing all of its children. Besides "wwwroot," there might be other directories or files. For example, I have an "ftproot" folder nested below "inetpub." Since "inetpub" is a parent to "ftproot" as well as to "wwwroot," "ftproot" and "wwwroot" are said to be siblings.
XML nodes can also be identified using the same rules as the "family-name-game" above. Listing 1 is a breakdown of every battle from Iron Chef America: The Series (Season 1). In it, the "ICA" node is equivalent to "C:." It is the root node (also known as the document node in XML), an ancestor to the "IronChef" node, and parent to the "Battle" node. Since the "IronChef" and "Battle" nodes share the parent node "Battle," they are siblings. In XPath, just as in our file path example above, we can reference the "IronChef" node using a simple path syntax: "/ICA/Battle/IronChef."
This expression, when run against our XML file, will return a 10-element array, one element for each Iron Chef in each battle. The expression is very simple to follow. The "/" at the beginning of the string tells the XPath engine to start at the root node. Each "/node" combination after the leading "/" tells the XPath engine which set of nested nodes to select and finally return.
You can also start an XPath expression with a double slash "//." A double slash tells the XPath engine to search the entire XML document for the specified node. For example:
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//IronChef")>
will return the same array as:
<cfset XPathResult = xmlSearch(ironChefXMLObject, "/ICA/Battle/IronChef")>
because both expressions are selecting and returning the "IronChef" node.
This XPath shortcut can save the developer a lot of time when parsing through lengthy or deeply nested XML documents. However, since the path is not explicitly listed, it can lead to problems. "/ICA/Battle/IronChef/Score" will return an array with 10 elements, while the expression "//Score," on the other hand, will return an array with 20 elements. This is because the "//Score" expression does not differentiate between the challenger and Iron Chef's scores, thus returning all nodes named "Score."
Besides the nodes that are being returned, the attributes of those nodes are also returned when using XPath. Referencing attributes in XPath is very similar to referencing nodes, with one minor difference. XPath uses the "@attribute" syntax to reference node attributes. All slash "/" and double slash "//" rules apply to the attributes of nodes the same way they apply to nodes themselves.
<cfset XPathResult = xmlSearch(ironChefXMLObject,"//@food")>
The expression "//@food" will return the types of food that both the Iron Chefs and their challengers prepare, while
<cfset XPathResult = xmlSearch(ironChefXMLObject,"/ICA/Battle/IronChef/@food")>
will return only the Iron Chef's culinary style.
The "/", "//," and "@" symbols can be seen as the syntax used in XPath to define the FROM clause commonly used in SQL statements. In order to filter out nodes you will need XPath's equivalent to an SQL WHERE clause. That functionality is realized through XPath's rich set of predicates, functions, and operators.
XPath predicates use a bracket notation similar to the notation used to reference individual elements of an array.
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Battle[1]")>
As you may have guessed, the expression "//Battle[1]" will return only the first "Battle" node. Likewise "//Battle[2]" will return only the second "Battle" node and so forth. Commonly with arrays you can use the arrayLen() function to find out what the last element's position will be. In XPath you can use the function last() to achieve similar results.
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Battle[last()]")>
In this expression only the last "Battle" node is returned. Since there is no sorting or ORDER BY mechanism in XPath, nodes are returned in the same order as they appear in the original XML document. Thus the expression "//Battle[last()]" returns the "Battle" node in which potatoes are the secret ingredient, because it is the last one in the XML file.
You can also use a combination of functions and operators to pare down the results returned. This expression will return only the second to last "Battle" node:
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Battle[last()-1]")>
and this expression will return the first three "Battle" nodes:
<cfset XPathResult = xmlSearch(ironChefXMLObject,
"//Battle[position()<4]")>
You can also reference the values of nodes using the various set of operators in XPath:
<cfset XPathResult = xmlSearch(ironChefXMLObject,
"//Battle/IronChef/Score[Total>50]")>
All "Score" nodes that belong to Iron Chefs who scored a total above 50 are returned with this expression. This use of operators can apply to attributes as well as nodes. The following returns all "Battle" nodes where Bobby Flay is the Iron Chef:
<cfset XPathResult = xmlSearch(ironChefXMLObject,
"//Battle[IronChef/@name='Bobby Flay']")>
and what follows returns all "Challenger" nodes where the challenger cooks Italian food.
<cfset XPathResult = xmlSearch(ironChefXMLObject,
"//Challenger[@food='Italian']")>
This example brings about a problem. There is both a challenger, Roberto Donna, and an Iron Chef, Mario Batali, who each cook Italian food. However the previous example only returns Roberto Donna since the expression explicitly lists "//Challenger" before the "@food='Italian'" clause. XPath has a logical OR operator which will give us a work around to this problem. The pipe "|" character will concatenate two or more expressions together.
<cfset XPathResult = xmlSearch(ironChefXMLObject,
"//Challenger[@food='Italian']|//IronChef[@food='Italian']")>
This expression is really composed of two separate sub-expressions: "//Challenger[@food='Italian']" and "//IronChef[@food='Italian']." The first sub-expression returns the complete "Challenger" node, including its children and descendants, for all chefs who cook Italian food. The second sub-expression returns the "IronChef" node, including its children and descendants, for each Iron Chef who cooks Italian food. The first sub-expression returns one node set, the second four node sets, thus creating a five-node set that ColdFusion returns as a five-element array.
As you can see XPath has a full set of operators. Here is a list of all of the XPath operators and their description:
| Node set concatenation.
+ Addition
- Subtraction
* Multiplication div Division
= Equal
!= Not Equal
< Less than
<= Less than or equal to
> Greater than
>= Greater than or equal to
or Or
and And
mod Modulus
Many of the operators have multiple uses, however, remember that since ColdFusion can only return node sets, some common uses of these operators cannot be used. For example, you cannot add together the total scores of all the Iron Chefs and see which one had the highest overall score for the season. To do this you will have to leverage other features of ColdFusion, which are out of the scope of this article. So far we have written expressions based off of known XML schemas. Oftentimes you will not know the complete schema before you need to manipulate the data within the file. XPath has three built-in wildcard handlers for this very situation: *, @,* and node().
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Challenger/*")>
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Challenger/@*")>
<cfset XPathResult = xmlSearch(ironChefXMLObject, "//Challenger/node()")>
"//Challenger/*" will return the nodes and their values of all the children and descendants of "Challenger," but not "Challenger" itself. It also does not return any of the attributes or attribute values of any of the node sets. This is different than "//Challenger," which will return "Challenger" and it children/descendants, as well as the attributes of those nodes. "//Challenger/@*" will return the same node set, however it will only return the attributes of those nodes. If you want to return all of the nodes and attributes of those nodes, you would use the node() function, ("//Challenger/node()").
Running XPath expressions against XML documents in ColdFusion does not change the original XML document. This allows developers to experiment freely when trying to learn XPath, without worrying about the consequence (as they might when playing with SQL statements). There are scores of reference materials dedicated to the ins and outs of XPath, just be careful when investing any money into reference materials, remembering that ColdFusion does not support all that XPath has to offer. Do not be put-off by ColdFusion's crippled XPath engine though, Macromedia has made sure that the most useful and timesaving features are available to you. If you would like more information on XPath and how ColdFusion implements it, you can check out both the CFML reference guide and the ColdFusion developer's guide - available on macromedia.com. The W3C also has two great XPath references, the XML Path Language guide (www.w3.org/TR/XPath) and the W3C Schools XPath tutorial (www.w3schools.com/XPath).