Tuesday, June 30, 2009

Writing a Custom NAnt Task (Part 2)

Over the past couple of months, I have been working on writing the SchemaSpy Task for NAnt located on CodePlex.  When I began to create a few custom tasks for NAnt in the past, I found very little documentation about how to create one.  Much of the documentation surrounding how to create a custom task is based solely on examples without any supporting information.  While this works very well for basic things, there were some experiences I had to work through while writing the task for SchemaSpy that I would like to talk about here.  In this, the second of four parts, we'll be extending what we created on Part 1 by adding a FileSet collection to add some flexibility to our application.  In addition, the accompanying source code will remain in both C# and VB.Net similar to the first section in this series.


Changing the Input Directory Attribute:

In Part 1 of this series, our Log Combining task contained an attribute to identify the directory that held all files that were to be combined.  What we want to do first is remove this attribute and provide an alternative solution using the NAnt FileSet object.  The reason to change this is to allow us to not be tied to a single directory for combining files, as well as provide a ways to be more selective of the files to combine. For more general information on the FileSet Type of NAnt, make sure to check out the official documentation for FileSet over at the NAnt website.

In order to use a FileSet object, we'll need to import the NAnt.Core.Types namespace.  In addition, we have to instantiate a private FileSet object in order to be exposed by the new property.  The reason for this is that the NAnt engine does not look to ensure that the collection-based objects are instantiated.  On collection-based elements that contains children elements, the NAnt engine ultimately just adds items into the collection.  If the collection is not instantiated, a null-reference error will be thrown.

After the property has been setup and the private object has been instantiated, the code for such should look similar to the following:

   1:  private FileSet _logFileSet = new FileSet();
   3:  /// <summary>
   4:  /// The files that will be combined by this task.
   5:  /// </summary>
   6:  [BuildElement("fileset")]
   7:  [StringValidator(AllowEmpty = false)]
   8:  public FileSet LogFileSet 
   9:  {
  10:      get { return _logFileSet; } 
  11:      set { _logFileSet = value; }
  12:  }


Once this is done, we can update the project's build file to look something like the below to mimic the same functionality as we had with the previous attribute.

   1:  <logCombiner outputFile="combined.txt" >
   2:      <fileset basedir="..\logs\">
   3:          <include name="**/*" />
   4:      </fileset>
   5:  </logCombiner>


Updating the Combining Method to use the FileSet:

Now that we have the task attribute and build file setup, we need to update the code to use the collection. In order to keep things simple, I'm not going to touch the current CombineFiles() method.  Instead, I want to ensure the information I will be getting from the FileSet object will be able to be valid and fit into that method call.

To do this, we need to change the name of the ValidateInputDirectory() method to ValidateInputFile().  This will ensure the name of the method will not confuse other developers.  In addition, we want to change the logic of this method slightly by changing Directory.Exists() to File.Exists().  We will be working with files at this point instead of directories so testing we want to make sure we test for the proper item.

   1:  /// <summary>
   2:  /// Validates the Input Directory Value
   3:  /// </summary>
   4:  /// <param name="path">The directory location to validate</param>
   5:  private void ValidateInputFile(string path)
   6:  {
   7:      if (!File.Exists(path))
   8:      {
   9:          throw new BuildException("The input directory of " + path + " does not exist.");
  10:      }
  11:  }

Next, we need to update the GetInputFiles() method.  This method returns an array of strings representing the file paths.  These paths are then passed to the CombineFiles() method.  We want to keep this signature.  Inside of the method, we need to instantiate an item to hold our string array; like a List<string>.  Next, we need to loop through the FileSet.FileNames collection and add each entry into our string list.  Lastly, we want to make sure the files are validated through our ValidateInputFile() method before returning the string array.  Once all of this is done, the method should look like the following:

   1:  /// <summary>
   2:  /// Retrieves a listing of files from a provided directory
   3:  /// </summary>
   4:  /// <returns>A string array of all file paths.</returns>
   5:  private string[] GetInputFiles()
   6:  {
   7:      List<string> output = new List<string>();
   9:      foreach (string path in _logFileSet.FileNames)
  10:      {
  11:          ValidateInputFile(path);
  12:          output.Add(path);
  13:      }
  15:      return output.ToArray();
  16:  }


Looking Ahead:

At this point, we have updated our NAnt task from using a single input directory to combine all files in the directory to a more robust solution that allows us to specify which files to include and exclude.  We can compile the code and install the task as described in the previous segment for testing.  In the next post in this series, I'll be diving into writing a custom element collection that can be added to our NAnt task in order to provide more opportunities.


Source Code:

kick it on DotNetKicks.comShout it

No comments:

Post a Comment