2008/04/21

XML editing via MSBuild

Every environment I have been in has strived for a single build that can be deployed to multiple environments. This means one compilation and a single set of binaries. Different settings per environment are to be set in config/xml files. No big surprise. And yes, I know that the MSBuild Community Tasks have an xml editing task. I prefer mine. In part to the config files having a single naespace and if Microsoft desides to change the namespace declaration, I won't have to revist my proj files. Also, I have options to replace full or partial values for attributes or InnerText. Also, you can recursively search a directory for files matching the search pattern.

Bringing on the class




using System;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using Microsoft.Build.Utilities;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Framework;
using System.IO;
namespace PaulMontgommery.Custom.Tasks
{
///<summary>
/// Updates a XML document using a XPath.
/// </summary>
/// <example>Update a XML element.
/// <code><![CDATA[
/// <xmledit document="C:\VSProjects\MyProject\*.config" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p" attribute="value">
/// ]]></code>
/// </example>
/// <remarks>
/// The XML node being updated must exist before using the XmlUpdate task.
/// </remarks>
public class EditXml : Task
{
#region Class Variables
string _document = string.Empty;
string _folder = string.Empty;
string _xPath = string.Empty;
string _value = string.Empty;
string _replacedText = string.Empty;
string _attribute = string.Empty;
string _prefix = string.Empty;
bool _recursive = false;
bool _continueOnError = false;
bool _condition = true;
#endregion
#region Public Properties
/// <summary>
/// Required. Document to perform edit on. Wildcards are allowed.
/// </summary>
[Required]
public string Document
{
get { return _document; }
set { _document = value; }
}
/// <summary>
/// Optional. Folder to begin searching.
/// </summary>
public string Folder
{
get { return _folder; }
set { _folder = value; }
}
/// <summary>
/// Required. XPath statement to find value to edit.
/// </summary>
[Required]
public string XPath
{
get { return _xPath; }
set { _xPath = value; }
}
/// <summary>
/// Optional. Namespace prefix for XPath statement.
/// </summary>
public string Prefix
{
get { return _prefix; }
set { _prefix = value; }
}
/// <summary>
/// Required. Value to be placed into document as InnerText or as "value" attribute if innertext is null.
/// </summary>
[Required]
public string Value
{
get { return _value; }
set { _value = value; }
}
/// <summary>
/// Optional. Value to be replaced with <see cref="Value">.
/// </summary>
public string ReplacedText
{
get { return _replacedText; }
set { _replacedText = value; }
}
/// <summary>
/// Optional name of attribute to perform edit on.
/// </summary>
public string Attribute
{
get { return _attribute; }
set { _attribute = value; }
}
/// <summary>
/// Optional. Specifies whether subfolders of <see cref="Folder">should be searched.
/// Default is false.
/// </summary>
public bool Recursive
{
get { return _recursive; }
set { _recursive = value; }
}
/// <summary>
/// Optional. Specifies whether process should continue if an exception is thrown. Default is false.
/// </summary>
public bool ContinueOnError
{
get { return _continueOnError; }
set { _continueOnError = value; }
}
/// <summary>
/// Optional. A Run-time check to see if this process should execute. Default is true.
/// </summary>
public bool Condition
{
get { return _condition; }
set { _condition = value; }
}
#endregion
#region Public Methods
public override bool Execute()
{
// System.Diagnostics.Debugger.Launch();
if (!_condition)
return true;
bool success = false;
try
{
//We must have a folder or document to edit
if (string.IsNullOrEmpty(_folder) && _recursive)
throw new NullReferenceException("Folder must be specified for recursive searches.");
//We must have an XPath statement or ReplacedText to edit
if (string.IsNullOrEmpty(_xPath) && string.IsNullOrEmpty(_replacedText))
throw new NullReferenceException("XPath or ReplacedText must be specified.");
List<string> files = getFiles();
if (files == null)
throw new NullReferenceException(string.Format("Could not find a part of the path '{0}\\{1}'", _folder, _document));
foreach (string file in files)
{
editXml(file);
}
success = true;
}
catch (Exception ex)
{
// System.Diagnostics.Debugger.Launch();
Log.LogErrorFromException(ex);
success = _continueOnError;
}
return (success _continueOnError);
}
#endregion
#region Private Methods
#region editXml
private void editXml(string file)
{
//load file into XmlDocument
Log.LogMessage(MessageImportance.Normal, string.Format("Loading file '{0}'.", file));
XmlDocument doc = new XmlDocument();
doc.Load(file);
//Get namespace manager
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
if (!string.IsNullOrEmpty(doc.DocumentElement.NamespaceURI))
{
if (string.IsNullOrEmpty(Prefix))
{
nsmgr.AddNamespace("ns", doc.DocumentElement.NamespaceURI);
//Inject the namespace into the xpath query
XPath = XPath.Replace("/", "/ns:").Replace("/ns:/ns:", "//ns:");
}
else
{
nsmgr.AddNamespace(Prefix.EndsWith(":") ? Prefix.Replace(":", "") : Prefix, doc.DocumentElement.NamespaceURI);
}
}
int i = 0;
foreach (XmlNode node in doc.SelectNodes(XPath, nsmgr))
{
Log.LogMessage(MessageImportance.Normal, string.Format(" Found node '{0}'.", XPath));
if (string.IsNullOrEmpty(Attribute))
{
//Value setting
if (string.IsNullOrEmpty(ReplacedText))
{
Log.LogMessage(MessageImportance.Normal, string.Format("\t Setting Value to '{0}'.", Value));
if (node.Value == null && string.IsNullOrEmpty(node.InnerText))
node.Attributes["value"].Value = Value;
else
node.InnerText = Value;
}
else
{
//Value replacement
string oldvalue;
if (node.Value == null && string.IsNullOrEmpty(node.InnerText))
{
oldvalue = node.Attributes["value"].Value;
node.Attributes["value"].Value = node.Attributes["value"].Value.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced '{0}' with '{1}'.", oldvalue, node.Attributes["value"].Value));
}
else
{
oldvalue = node.InnerText;
node.InnerText = node.InnerText.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced '{0}' with '{1}'.", oldvalue, node.InnerText));
}
}
}
else
{
if (string.IsNullOrEmpty(ReplacedText))
{
//Attribute setting
Log.LogMessage(MessageImportance.Normal, string.Format("\t Setting Attribute '{0}' to '{1}'.", Attribute, Value));
node.Attributes[Attribute].Value = Value;
}
else
{
//Attribute replacement
string oldvalue = node.Attributes[Attribute].Value;
node.Attributes[Attribute].Value = node.Attributes[Attribute].Value.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced value of Attribute '{0}' from '{1}' to '{2}'.", Attribute, oldvalue, node.Attributes[Attribute].Value));
}
}
i++;
//end of foreach
}
if (i == 0)
Log.LogWarning("Unable to locate node '{0}'.", XPath);
Log.LogMessage(MessageImportance.Normal, string.Format("Document completed with {0} change(s).", i));
doc.Save(file);
}
#endregion
#region getFiles
private List<string> getFiles()
{
if (Document.Contains("\\") && !string.IsNullOrEmpty(Folder))
throw new NotSupportedException("Document cannot have path information when Folder is specified.");
if (!string.IsNullOrEmpty(Folder))
return getFiles(Folder, Document);
else
return getFiles(Document);
}
private List<string> getFiles(string Document)
{
// Split folder from filename
return getFiles(Document.Substring(0, Document.LastIndexOf("\\")),
Document.Substring(Document.LastIndexOf("\\") + 1));
}
private List<string> getFiles(string Folder, string Document)
{
List<string> files = new List<string>();
// Add item for each file matching the search criteria
foreach (string file in Directory.GetFiles(Folder, Document))
files.Add(file);
//Check sub directories for additional files.
if (Recursive)
{
//Call getFiles with each subdirecotry and the Document.
foreach (string directory in Directory.GetDirectories(Folder))
files.AddRange(getFiles(directory, Document));
}
return files;
}
#endregion
#endregion
}
}




Now, how to use these roughly 300 lines of code. As you can see in the XML documentation prior to the class declarations, there are the following examples:


<xmledit document="C:\VSProjects\MyProject\*.config" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p" attribute="value">

If you have any suggestions, let me know.

No comments:

Comments