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:
<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:
<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:
<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 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:
<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:
private Dictionary<string, string> _loopVars = new Dictionary<string, string>();
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
>
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);
foreach (string prop in _props)
{
string theProp = prop.Trim();
_loopVars.Add(prop, Properties[prop]);
}
}
Restoring NAnt project properties values at the end:
Finally
{
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:
<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:
<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:
<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.