Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Looping through your XML with NAnt

4.50/5 (3 votes)
31 Oct 2013CPOL5 min read 27.6K   256  
Looping through your XML with NAnt.

Introduction

NAnt is a very powerful tool used by many professionals for Continuous Integration, automated builds and deployments, etc. It is also easily extensible with custom tasks or C# scripts inclusions.

Background

In many cases it would be useful to be able to loop through XML document nodes doing some processing.
I also needed this functionality for my deployment projects and searched internet extensively for some feasible solutions. At that time I couldn’t find anything I liked, and I have just created my own custom task foreachxml instead. I made it very simple. To specify the variables I needed to populate on each step from the XML node attributes I used property task attribute:

XML
<foreachxml file="MyMap.xml" xpath="/services/queuenamepairs/add" property="key,value,lineup">

The task will be retrieving values of key, value and lineup attributes in the XML and assigning these values to the NAnt project properties with the same names. If properties already existed, they would be overwritten. If they happen to be read-only – error will be returned.

Later on I found this nice article of John Ludlow:
http://www.mail-archive.com/nant-developers@lists.sourceforge.net/msg06745.html

His implementation had convenient <xmlpropertybinding> feature. Unfortunately this article did not have enough details for me, so I have just added <xmlpropertybinding> to my <foreachxml> implementation as optional feature.

Using the code

To use this custom task you need to download the NAnt.GF.Custom.Tasks.dll and put it into your NAnt bin folder, where NAnt.exe resides. Now in your build scripts you may use <foreachxml> task like this:

XML
<foreachxml file="MyeMap.xml" xpath="/services/queuenamepairs/add" property="key,value,lineup">
    <echo message="nodecount = ${nodecount}"/>
    <echo message="nodeindex = ${nodeindex}"/>
    <echo message="key = ${key}"/>
    <echo message="value = ${value}"/>
    <echo message="lineup = ${lineup}"/>
    <if test="${lineup == 'true'}">
        <echo message="lineup specific"/>
    </if>
    <if test="${lineup == 'false'}">
        <echo message="lineup neutral"/>
    </if>
</foreachxml> 
Assuming your MyeMap.xml XML file is:
XML
<services>
    <queuenamepairs>
        <add key="My.InputHandler" value="input" lineup="true"/>
        <add key="My.ResponseHandler" value="response" lineup="false"/>
        <add key="My.smtpHandler" value="smtp" lineup="false"/>
        <!-- Add some dummy stuff for the test sake -->
        <add key="My.QueueHandler" value="queue" lineup="false">
            <testnode key="test" value="crap" testattrib="testing-1-2-3"/>
        </add>
        <add key="My.ReportHandler" value="report" lineup="true"/>
    </queuenamepairs>
</services>

The output will be like this:

[echo] Testing foreachxml task with 'property'
[echo]
[echo] nodecount = 5
[echo] nodeindex = 0
[echo] key = My.InputHandler
[echo] value = input
[echo] lineup = true
[echo] lneup specific
[echo] nodecount = 5
[echo] nodeindex = 1
[echo] key = My.ResponseHandler
[echo] value = response
[echo] lineup = false
[echo] lineup neutral
[echo] nodecount = 5
[echo] nodeindex = 2
[echo] key = My.smtpHandler
[echo] value = smtp
[echo] lineup = false
[echo] lineup neutral
[echo] nodecount = 5
[echo] nodeindex = 3
[echo] key = My.QueueHandler
[echo] value = queue
[echo] lineup = false
[echo] lineup neutral
[echo] nodecount = 5
[echo] nodeindex = 4
[echo] key = My.ReportHandler
[echo] value = report
[echo] lineup = true
[echo] lineup specific

Please note that NAnt project properties key, value, lineup will be created (overwritten if existed) as well as task-specific properties nodecount, nodeindex. The latter two are hard-coded ones.

You may use <xmlpropertybinding> attribute as well, if you wish to use NAnt project properties with its own names, not mapped from the XML attributes’ names:

XML
<foreachxml file="MyMap.xml" xpath="/services/queuenamepairs/add">
    <xmlpropertybinding>
        <get xpath="@key" property="key"/>
        <get xpath="@value" property="the.val"/>
        <get xpath="@lineup" property="line-up"/>
    </xmlpropertybinding>
    <do>
        <echo message="nodeindex = ${nodeindex}"/>
        <echo message="key = ${key}"/>
        <echo message="value = ${the.val}"/>
        <if test="${line-up == 'true'}">
            <echo message="lineup specific"/>
        </if>
        <if test="${line-up == 'false'}">
            <echo message="lineup neutral"/>
        </if>
    </do>
</foreachxml>

The output will be like this:

[echo] Testing with DO and xmlpropertybinding
[echo]
[echo] nodeindex = 0
[echo] key = My.InputHandler
[echo] value = input[echo] lineup specific
[echo] nodeindex = 1
[echo] key = My.ResponseHandler[echo] value = response
[echo] lineup neutral
[echo] nodeindex = 2[echo] key = My.smtpHandler
[echo] value = smtp
[echo] lineup neutral[echo] nodeindex = 3
[echo] key = My.QueueHandler
[echo] value = queue[echo] lineup neutral
[echo] nodeindex = 4
[echo] key = My.ReportHandler[echo] value = report
[echo] lineup specific

- Basically the same, with the difference that your task now uses properties key, the.val, line-up defined in <xmlpropertybinding> element instead of XML file attributes names.

Custom Task Implementation

Custom task <foreachxml> implementation may be found in attached LoopXmlTask.cs
Some points of interest are my solutions, are they smart or stupid, to the issues I had to resolve.

Issue 1: restoring NAnt project properties values. If the properties were overwritten in the task (because of names collision), it is nice to restore them after the task completes. For this end I have used 2 dictionaries:

C#
// loop variables names / saved values
private Dictionary<string, string> _loopVars = new Dictionary<string, string>();
// loop variables names / bind paths (if using <xmlpropertybinding>)
private Dictionary<string, string> _bindPaths = new Dictionary<string, string>();

_loopVars containing names and initial values, I was filling with property elements, if property attribute was defined in the task, or from <xmlpropertybinding> otherwise. _bindPaths containing names and Xpaths, are being filled from <xmlpropertybinding>

C#
// fill _loopVars from Property or from <xmlpropertybinding>
// backup old values
if (StringUtils.IsNullOrEmpty(Property))
{
	XmlNodeList xnlBind = this.XmlNode.SelectNodes("nant:xmlpropertybinding/nant:get", this.NamespaceManager);
	int nTemp = xnlBind.Count;
	foreach (XmlNode xnProperty in xnlBind)
	{
		string prop = xnProperty.Attributes["property"].Value;
		_loopVars.Add(prop, Properties[prop]);
		string xpath = xnProperty.Attributes["xpath"].Value;
		_bindPaths.Add(prop, xpath);
	}
}
else
{
	Log(Level.Verbose, "Using Property attribute: '{0}'. xmlpropertybinding element will be ignored", Property);
	// fill _loopVars from Property
	foreach (string prop in _props)
	{
		string theProp = prop.Trim();
		_loopVars.Add(prop, Properties[prop]);
	}
}

Restoring NAnt project properties values at the end:

C#
Finally
{
	// Restore all of the old property values
	foreach (KeyValuePair<string, string> pair in _loopVars)
	{
		Properties[pair.Key] = pair.Value;
	}
	_loopVars.Clear();
	_bindPaths.Clear();
}

Issue 2: custom element <xmlpropertybinding>.

Correct way to handle this new element would be to create the class for the element of that type, making this element known to the base TaskContainer class. Being lazy though (or let me call it politely “lean and mean”, “minimalistic”) I have just used it to fill my two dictionaries above and skip it, switching to embedded <do> element. This is necessary because otherwise base TaskContainer class will fail to recognize our <xmlpropertybinding> element in the current node.

Points of Interest

I have DEV-tested my custom task on the test file, like the one provided in this article. The task is actively and successfully used now in many auto-deployment projects. But it is used on one specific type of XML files. It would be very interesting to find how it works for different users on different XML files. Your feedback would be highly appreciated.

Custom Tasks in the Provided DLL

Provided NAnt.GF.Custom.Tasks.dll contains following custom tasks: foreachwhile, foreachxml, waitforfile, getparamcsv, hello

foreachxml
- We have just discussed.

foreachwhile
- Described in this article:
http://www.codeproject.com/Articles/314222/Breaking-from-NAnt-loop

hello
- Is used like:

XML
<hello person="Jack" />

Outputs:
Hello, Jack!

Easy to guess that the only use for such a smart task is for testing NAnt.GF.Custom.Tasks.dll availability. If Jack replies, we are good to go. If the task fails – something wrong with DLL deployment.

waitforfile
- Is used like:

XML
<waitforfile file="C:\Work\Test\test.txt" interval="1000" timeout="30000" property="found">
</waitforfile>

Use case:
Suppose NAnt starts some process, possibly remotely, that is hard to control by NAnt. The process eventually generates some file that NAnt has to wait for. If the file appears within timeout, task immediately returns, assigning requested property (here it is called "found") boolean value true. Otherwise the task returns after timeout with the value false.

getparamcsv
- Is used like:

XML
<property name="csvfile" value="C:\Projects\NANT\World_Redesign_Params.csv"/>

<getparamcsv csvfile="${csvfile}" environmentid="DEV" paramname="LocalProjectDir" property="result"/>
<property name="dev.project.dir" value="${result}"/>    

<getparamcsv csvfile="${csvfile}" environmentid="QA" paramname="LocalProjectDir" property="result"/>
<property name="qa.project.dir" value="${result}"/>

Use case:
Deployment scripts are used for multiple environments (DEV, QA, etc.) Each environment has multiple parameters. To provide easy way for the 3rd person to control these parameters without touching scripts itself, I have created CSV file that contains in the first column environment IDs, in the upper row – parameters IDs, intersection cells – environment-specific parameter value. The task getparamcsv gets any parameter for any environment from the specified CSV file.

History 

No history so far.

Eagerly waiting for your feedback to do some fixes or improvements.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)