Showing posts with label Custom Task. Show all posts
Showing posts with label Custom Task. Show all posts

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.

2008/03/27

Getting info back out of a custom MSBuild Task



So I am working on getting some build automation and such. Unfortunatly, my client is running Visual Source Safe (how I miss Team Foundation Server). So I am tasked with getting the latest code out of VSS, build numerous configurations of our solutions ensuring that *.config are set to reflect the envirounment required for each configuration to be built. After I get the code, I want to label it so if we need to go back and rebuild version x.y.z we can grab its label.




In TFS, I would just let the vanilla installation and TeamBuild.targets deal with coming up with a label number. But again, I gots no TFS. So I am going to generate a label based on the date and use MSBuild Community Tasks (available at http://msbuildtasks.tigris.org/) to handle the communication to VSS.




So how do you get the date formatted the way you want? Well, there is the easy System.DateTime.ToString(string format), right? All you have to do to get to this functionality is to write an MSBuild custom task.




Enough talk, on to the code. You start by creating a new class library project and adding references to Microsoft.Build.Framework and Microsoft.Build.Utilities.





Now you need to start building your class. Something like this should do the trick:



using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace MSBuilding
{
public class GetDateTime : Task
{
DateTime _now;
string _format = string.Empty;
///
/// The format string to be passed to DateTime.ToString method
///

[Required]
public string Format
{
set { this._format = value; }
}
///
/// This is the TaskParameter that is returned.
///

[Output]
public string ReturnValue
{
get { return _now.ToString(_format); }
}
///
/// Required to implement the Microsoft.Build.Utilities abstract class.
///

///
public override bool Execute()
{
bool result = true;
try
{
_now = DateTime.Now;
}
catch (Exception ex)
{
//something failed set the result to false and log the Exception
result = false;
bool showStackTrace = true;
Log.LogErrorFromException(ex, showStackTrace);
}
return result;
}
}
}


Here's what's happening. I'm declaring the class GetDateTime and having it inherit from Microsoft.Build.Utilities.Task Then I'm setting up some class-level variables for _format and _now. Then I am setting a required property for Format this ensures that a format is passed in from the MSBuild project. Also, I am putting minmal logic into the Execute method as this class is really doing minimal.


To get info back out of the task, you need some properties with the [Output] attribute tagged onto them. In the case of this class, we have one property, ReturnValue.


Now all we have to do is write up a simple proj file and invoke the task we wrote. A simple, single target project is good enough for testing:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Test">
<UsingTask AssemblyFile="MSBuilding.dll" TaskName="MSBuilding.GetDateTime"/>
<Target Name="Test">
<GetDateTime
Format="yyyyMMdd_HHmmss_ff">
<Output TaskParameter="ReturnValue" PropertyName="MyTime"/>
</GetDateTime>
<Message Text="$(MyTime)"/>
</Target>
</Project>
Now we just call MSBuild.exe passing our proj file as the only argument.


That's about it. Look for more MSBuild goodness as I get time.

Comments