Hi, all.

I've thought for a while that NAnt could do with an easier way to iterate through an XML document.  I've made an xml-foreach task, which takes an XML file path and a xpath query as attributes, like this:

      <xml-foreach file="test.xml" xpath="/people/person">
        <xmlpropertybinding>
          <get xpath="@name" property="name"/>
          <get xpath="@age" property="age"/>
        </xmlpropertybinding>
        <do>
          <echo message="${name} is ${age} years old"/>
        </do>
      </xml-foreach>

If you run this on the xml below

<people>
  <person name="bob" age="44"/>
  <person name="bill" age="15"/>
  <person name="ted" age="372"/>
  <person name="fred" age="1"/>
</people>

you get this output:

     [echo] bob is 44 years old
     [echo] bill is 15 years old
     [echo] ted is 372 years old
     [echo] fred is 1 years old

I've managed to do slightly more complicated stuff (like adding child elements to each person element and iterating through each child element for each person), though I realised that to get this working the way I want it to is going to be a bit more work than I originally thought.

Here's my code so far:

        [TaskName("xml-foreach")]
        public class XmlForEach : TaskContainer
        {
          protected override void ExecuteTask()
          {
            XmlDocument xd = new XmlDocument();
            xd.Load(_xmlFile);
            XmlNodeList xnl = xd.SelectNodes(_xPath);
            foreach (XmlNode xn in xnl)
            {
              foreach(XmlNode xnProperty in this.XmlNode.SelectNodes("xmlpropertybinding/get"))
              {
                string failonempty = "";
                if (xnProperty.Attributes["failonempty"] != null)
                {
                  failonempty = xnProperty.Attributes["failonempty"].Value;
                }
                if (failonempty == "true" && xn.SelectSingleNode(xnProperty.Attributes["xpath"].Value) == null)
                {
                  throw new BuildException("XPath " + xnProperty.Attributes["xpath"].Value + " returned no results");
                }
                else if (xn.SelectSingleNode(xnProperty.Attributes["xpath"].Value) != null)
                {
                  Properties[xnProperty.Attributes["property"].Value] = xn.SelectSingleNode(xnProperty.Attributes["xpath"].Value).Value;
                }
              }
              _doStuff.Execute();
            }
          }

          [BuildElement("do")]
          public TaskContainer StuffToDo {
              get { return _doStuff; }
              set { _doStuff = value; }
          }
         
          [TaskAttribute("file", Required=true)]
          public string XmlFileName {
              get { return _xmlFile; }
              set { _xmlFile = value; }
          }
          [TaskAttribute("xpath", Required=true)]
          public string XPathQuery {
              get { return _xPath; }
              set { _xPath = value; }
          }
         
          private string _xmlFile;
          private string _xPath;
          private XmlNodeList _results;
          private TaskContainer _doStuff;
        }

One problem is that it's just getting the task's xml node and getting its xmlpropertybinding element from that.  Ideally it needs to be a BuildElement, but I had trouble getting this to recognise the get elements. I get the feeling I need to create a class for both each type of element. 

Another problem is that, if you nest an xml-foreach, there's no easy way to get the parent.  For example, in the example I gave above, I modified the xml document to create a number of child elements for each person, then changed the script to iterate through each child for each person.  To achieve this I added the following into the <do> element of my foreach (basically a nested loop):

      <echo message="Children of ${name}:"/>
      <xml-foreach file="test.xml" xpath="/person/[EMAIL PROTECTED]'${name}']/child">
        <xmlpropertybinding>
          <get xpath="@name" property="name"/>
          <get xpath="@age" property="age"/>
        </xmlpropertybinding>
        <do>
          <echo message="${name} is ${age} years old"/>
        </do>
      </xml-foreach>

Note that the xpath for this inner loop is more complicated as it has to find that element again.

Anyway, maybe this will be useful, or maybe someone will be able to help me solve the issues.  One other thing:  currently I have put in a failonempty attribute on the get element.  This defaults to false and causes the build to fail if that property can't be retrieved for any reason.

Regards

John

Reply via email to