NAnt task xmllist, way more powerful than xmlpeek (source provided)
UPDATE: See http://weblogs.asp.net/soever/archive/2006/12/01/nant-xmllist-command-updated.aspx for an updated version of the NAnt XmlPeek command.
I have a love-hate relationship with the <xmlpeek> command in NAnt.
The problems I have with it are:
- It report an error when the XPath expression does not resolve into a node, there is NO WAY to test if a node or attribute exists (to my knowledge)
- It’s logging level is set to Level.Info, so there is always output. This should have been Level.Verbose, I don’t want output for every xmlpeek I perform
- It is not possible to return the contents of multiple nodes selected in the XPath expression
Especially the problem that I can’t test for the existance of a node or attribute bothers me. I can set failonerror to false, ant test afterwards if the property exist, but that means that there is still an error that is reported in my buildserver report, while it is expected behaviour!
Based on an implementation by Richard Case I wrote the same version of his <xmllist> task, but a bit more powerful and using the standard naming for the attributes. Using this task you can extract text from an XML file at the locations specified by an XPath expression, and return those texts separated by a delimiter string. If the XPath expression specifies multiple nodes the node are seperated by the delimiter string, if no nodes are matched, an empty string is returned.
See the comments in the code for an extensive example.
I will try to post this code to the NAnt developers mailing list, but it’s here for you to get you starget if you need this kind of functionality.
// NAnt - A .NET build tool // Copyright (C) 2001-2003 Gerry Shaw // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Serge van den Oever (serge@macaw.nl) // Based on idea from weblog entry: http://blogs.geekdojo.net/rcase/archive/2005/01/06/5971.aspx combined with the code of xmlpeek.using System; using System.Globalization; using System.IO; using System.Text; using System.Xml; using System.Collections.Specialized;
using NAnt.Core; using NAnt.Core.Attributes; using NAnt.Core.Types;
namespace Macaw.MGDE { /// <summary> /// Extracts text from an XML file at the locations specified by an XPath /// expression, and return those texts separated by a delimiter string. /// </summary> /// <remarks> /// <para> /// If the XPath expression specifies multiple nodes the node are seperated /// by the delimiter string, if no nodes are matched, an empty string is returned. /// </para> /// </remarks> /// <example> /// <para> /// The example provided assumes that the following XML file (xmllisttest.xml) /// exists in the current build directory. /// </para> /// <code> /// <![CDATA[ /// <?xml version="1.0" encoding="utf-8" ?> /// <xmllisttest> /// <firstnode attrib="attrib1">node1</firstnode> /// <secondnode attrib="attrib2"> /// <subnode attrib="attribone">one</subnode> /// <subnode attrib="attribtwo">two</subnode> /// <subnode attrib="attribthree">three</subnode> /// <subnode attrib="attribtwo">two</subnode> /// </secondnode> /// </xmllisttest> /// ]]> /// </code> /// </example> /// <example> /// <para> /// The example reads numerous values from this file: /// </para> /// <code> /// <![CDATA[ /// <?xml version="1.0" encoding="utf-8" ?> /// <project name="tests.build" default="test" basedir="."> /// <target name="test"> /// <!-- TEST1: node exists, is single node, get value --> /// <xmllist file="xmllisttest.xml" property="prop1" delim="," xpath="/xmllisttest/firstnode"/>
/// <echo message="prop1=${prop1}"/> /// <fail message="TEST1: Expected: prop1=node1" unless="${prop1 == 'node1'}"/> /// /// <!-- TEST2: node does not exist --> /// <xmllist file="xmllisttest.xml" property="prop2" delim="," xpath="/xmllisttest/nonexistantnode" />
/// <echo message="prop2='${prop2}'"/> /// <fail message="TEST2: Expected: prop2=<empty>" unless="${prop2 == ''}"/> /// /// <!-- TEST3: node exists, get attribute value --> /// <xmllist file="xmllisttest.xml" property="prop3" delim="," xpath="/xmllisttest/firstnode/@attrib" />
/// <echo message="prop3=${prop3}"/> /// <fail message="TEST3: Expected: prop3=attrib1" unless="${prop3 == 'attrib1'}"/> /// /// <!-- TEST4: nodes exists, get multiple values --> /// <xmllist file="xmllisttest.xml" property="prop5" delim="," xpath="/xmllisttest/secondnode/subnode" />
/// <echo message="prop5=${prop5}"/> /// <fail message="TEST4: Expected: prop5=one,two,three,two" unless="${prop5 == 'one,two,three,two'}"/> /// /// <!-- TEST5: nodes exists, get multiple attribute values --> /// <xmllist file="xmllisttest.xml" property="prop5" delim="," xpath="/xmllisttest/secondnode/subnode/@attrib" />
/// <echo message="prop5=${prop5}"/> /// <fail message="TEST5: Expected: prop5=attribone,attribtwo,attribthree,attribtwo" unless="${prop5 == 'attribone,attribtwo,attribthree,attribtwo'}"/> /// /// <!-- TEST6: nodes exists, get multiple values, but only unique values --> /// <xmllist file="xmllisttest.xml" property="prop6" delim="," xpath="/xmllisttest/secondnode/subnode" unique="true"/>
/// <echo message="prop6=${prop6}"/> /// <fail message="TEST4: Expected: prop6=one,two,three" unless="${prop6 == 'one,two,three'}"/> /// /// <!-- TEST7: nodes exists, get multiple attribute values --> /// <xmllist file="xmllisttest.xml" property="prop7" delim="," xpath="/xmllisttest/secondnode/subnode/@attrib" unique="true"/>
/// <echo message="prop7=${prop7}"/> /// <fail message="TEST7: Expected: prop7=attribone,attribtwo,attribthree" unless="${prop7 == 'attribone,attribtwo,attribthree'}"/> /// /// <!-- TEST8: node exists, is single node, has namespace http://thirdnodenamespace, get value --> /// <xmllist file="xmllisttest.xml" property="prop8" delim="," xpath="/xmllisttest/x:thirdnode">
/// <namespaces> /// <namespace prefix="x" uri="http://thirdnodenamespace" /> /// </namespaces> /// </xmllist> /// <echo message="prop8=${prop8}"/> /// <fail message="TEST8: Expected: prop8=namespacednode" unless="${prop8 == 'namespacednode'}"/> /// </target> /// </project> /// ]]> /// </code> /// Result when you run this code: /// <code> /// <![CDATA[ /// test: /// /// [echo] prop1="node1" /// [echo] prop2="''" /// [echo] prop3="attrib1" /// [echo] prop5="one,two,three,two" /// [echo] prop5="attribone,attribtwo,attribthree,attribtwo" /// [echo] prop6="one,two,three" /// [echo] prop7="attribone,attribtwo,attribthree" /// [echo] prop8="namespacednode" /// /// BUILD SUCCEEDED /// ]] /// </code> /// </example> [TaskName ("xmllist")] public class XmlListTask : Task { #region Private Instance Fieldsprivate FileInfo _xmlFile; private string _xPath; private string _property; private string _delimiter = ","; private bool _unique = false; // assume we return all values private XmlNamespaceCollection _namespaces = new XmlNamespaceCollection(); #endregion Private Instance Fields #region Public Instance Properties /// <summary> /// The name of the file that contains the XML document /// that is going to be interrogated. /// </summary> [TaskAttribute("file", Required=true)] public FileInfo XmlFile { get { return _xmlFile; } set { _xmlFile = value; } } /// <summary> /// The XPath expression used to select which nodes to read. /// </summary> [TaskAttribute ("xpath", Required = true)] [StringValidator (AllowEmpty = false)] public string XPath { get { return _xPath; } set { _xPath = value; } } /// <summary> /// The property that receives the text representation of the XML inside /// the nodes returned from the XPath expression, seperated by the specified delimiter. /// </summary> [TaskAttribute ("property", Required = true)] [StringValidator (AllowEmpty = false)] public string Property { get { return _property; } set { _property = value; } } /// <summary> /// The delimiter string. /// </summary> [TaskAttribute ("delim", Required = false)] [StringValidator (AllowEmpty = false)] public string Delimiter { get { return _delimiter; } set { _delimiter = value; } } /// <summary> /// If unique, no duplicate vaslues are returned. By default unique is false and all values are returned. /// </summary> [TaskAttribute ("unique", Required = false)] [BooleanValidator()] public bool Unique { get { return _unique; } set { _unique = value; } } /// <summary> /// Namespace definitions to resolve prefixes in the XPath expression. /// </summary> [BuildElementCollection("namespaces", "namespace")] public XmlNamespaceCollection Namespaces { get { return _namespaces; } set { _namespaces = value; } } #endregion Public Instance Properties #region Override implementation of Task /// <summary> /// Executes the XML reading task. /// </summary> protected override void ExecuteTask() { Log(Level.Verbose, "Looking at '{0}' with XPath expression '{1}'.", XmlFile.FullName, XPath); // ensure the specified xml file exists if (!XmlFile.Exists) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, "The XML file '{0}' does not exist.", XmlFile.FullName), Location); } try { XmlDocument document = LoadDocument(XmlFile.FullName); Properties[Property] = GetNodeContents(XPath, document); } catch (BuildException ex) { throw ex; // Just re-throw the build exceptions. } catch (Exception ex) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, "Retrieving the information from '{0}' failed.", XmlFile.FullName), Location, ex); } } #endregion Override implementation of Task #region private Instance Methods /// <summary> /// Loads an XML document from a file on disk. /// </summary> /// <param name="fileName">The file name of the file to load the XML document from.</param> /// <returns> /// A <see cref="XmlDocument">document</see> containing /// the document object representing the file. /// </returns> private XmlDocument LoadDocument(string fileName) { XmlDocument document = null; try { document = new XmlDocument(); document.Load(fileName); return document; } catch (Exception ex) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, "Can't load XML file '{0}'.", fileName), Location, ex); } } /// <summary> /// Gets the contents of the list of nodes specified by the XPath expression. /// </summary> /// <param name="xpath">The XPath expression used to determine the nodes.</param> /// <param name="document">The XML document to select the nodes from.</param> /// <returns> /// The contents of the nodes specified by the XPath expression, delimited by /// the delimiter string. /// </returns> private string GetNodeContents(string xpath, XmlDocument document) { XmlNodeList nodes; try { XmlNamespaceManager nsMgr = new XmlNamespaceManager(document.NameTable); foreach (XmlNamespace xmlNamespace in Namespaces) { if (xmlNamespace.IfDefined && !xmlNamespace.UnlessDefined) { nsMgr.AddNamespace(xmlNamespace.Prefix, xmlNamespace.Uri); } } nodes = document.SelectNodes(xpath, nsMgr); } catch (Exception ex) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, "Failed to execute the xpath expression {0}.", xpath), Location, ex); } Log(Level.Verbose, "Found '{0}' nodes with the XPath expression '{1}'.", nodes.Count, xpath); // collect all strings in a string collection, skip duplications if Unique is true StringCollection texts = new StringCollection(); foreach (XmlNode node in nodes) { string text = node.InnerText; if (!Unique || !texts.Contains(text)) { texts.Add(text); } } // Concatenate the strings in the string collection to a single string, delimited by Delimiter StringBuilder builder = new StringBuilder(); foreach (string text in texts) { if (builder.Length > 0) { builder.Append(Delimiter); } builder.Append(text); } return builder.ToString(); } #endregion private Instance Methods }
}