boat+hill

·舟山詩詞·淘海洗玉集 – My Poems, and etc.

Archive for the ‘dotNET’ Category

Paged Collection in Data Service

leave a comment »


It is naturally to use Expand in a Linq query to get children collection under an entity, as showed in the following example.

using System.Collections.Generic;
using System.Linq;

public IEnumerable<User> GetMembersByGroup(string groupIdentifier)
{
  var team = this.dataContext
      // each Team has a MemberUsers collection
      .Teams.Expand(t => t.MemberUsers)
      .Where(t => t.Id == groupIdentifier)
      .Single();

  return team.MemberUsers.ToList();
}

The above code would work as long as the size of the collection is small and within server paging size of the data service (- see this blog). In order to get result from all paged collection, the following example is using DataServiceCollection<T>.Load method.

Note: The service reference needs have UseDataServiceCollection enabled in .datasvcmap configuration which should be supported by .NET 3.5 SP1 and 4. In other case, rather than System.Data.Services.Client.DataServiceCollection, the System.Collections.ObjectModel.Collection won’t have Continuation.

using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;

public IEnumerable<User> GetMembersByGroup(string groupIdentifier)
{
  var team = this.dataContext
      // expand both MemberUsers and Children teams
      .Teams.Expand("MemberUsers,Children/MemberUsers")
      .Where(t => t.Id == groupIdentifier)
      .AsEnumerable()
      .FirstOrDefault();

  if (team == null)
  {
    return new List<User>();
  }

  DataServiceCollection<User> users = team.MemberUsers;

  while (users.Continuation!= null)
  {
    users.Load(this.dataContext.Execute(users.Continuation));
  }

  return users;
}

The DataServiceCollection<T> requires a type T. For Query Projction with anonymous (or Tuple) type, the query can load data but may not support Continuation.

Another similar solution is sending data query to data service with GetContinuation.

using System.Collections.Generic;
using System.Linq;

public IEnumerable<User> GetMembersByGroup(string groupIdentifier)
{
  var dataQuery = 
        from t in this.dataContext.Teams
       where t.Id == groupIdentifier
        from u in t.MemberUsers
      select u;

  var users = this.dataContext.GetAll<User>(dataQuery);

  return users;
}

Since only navigation query supports join operation, the query must be on the primary key (as the Id in above code); otherwise, use another query with SingleOrDefault or FirstOrDefault in prior to get the identifier. Also, GetAll method should support Query Projection, so that GetAll(dataQuery) can be used, instead of GetAll((DataServiceQuery)dataQuery).

In order to query all users by a name (which is not a navigation query), the following data query projects the result to a collection of an anonymous typed objects, so that we can get identifiers for navigation queries later.

using System.Collections.Generic;
using System.Linq;

public IEnumerable<string> GetUsersByName(string userName)
{
  var dataQuery = 
        from u in this.Users
       where u.Name == userName
      select new {
      {
        Id = u.Id, Alias = u.Alias
      }
  var result = this.dataContext.GetAll(dataQuery);
  var users = result.Select(a => a.Id);

  return users;
}

Here is the source of GetAll extension (with support of Query Projection):

using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;

public static IEnumerable<T> GetAll<T>(
  this DataServiceContext dataContext, 
  IQueryable<T> dataServiceQuery)
{
  QueryOperationResponse<T> response = 
    (QueryOperationResponse<T>)
    ((DataServiceQuery<T>)dataServiceQuery).Execute();

  DataServiceQueryContinuation<T> continuation = null;

  do
  {
    if (continuation != null)
    {
     response = this.dataContext.Execute(continuation);
    }

    foreach (var result in response)
    {
      yield return result;
    }

    continuation = response.GetContinuation();
  }
  while (continuation != null);
}

Advertisements

Written by Boathill

2014-01-26 at 22:00

EqualityComparer

leave a comment »


In C#, for a list of string (or some primitive type), IEnumerable.Distinct() method can help reduce the duplicates.

var distinctList = strList.Distinct();
 

However, for a list of complex type, this may not work as expected, since Distinct() will produce a new list based on the hash code of each item. For any Foo type class, in order to use Distinct() method, Equals() and GetHashCode() need to be override.

public override bool Equals(object obj)
{
  Foo other = obj as Foo;
  return 
    other != null && 
    other.Name.Equals(this.Name) && 
    other.Prop.Equals(this.Prop);
}

public override int GetHashCode()
{
  var tuple = new Tuple<string, PropType>(this.Name, this.Prop);
  int hashCode = tuple.GetHashCode();
  return hashCode;
}

For hash code, see algorithms, here, and here. Do remember the rule: “If two objects compare as equal, the GetHashCode method for each object must return the same value. However, if two objects do not compare as equal, the GetHashCode methods for the two object do not have to return different values.” (See more rules and guidelines on this blog)

In many cases, the developer may not own the code of class Foo to override Equals() and GetHashCode(), or the Distinct may vary in run time. One solution is to use GroupBy method in System.Linq.Enumerable extension applying on item’s property (e.g. using Name as a key to distinguish each item):

var distinctList = myList
    .GroupBy(i => i.Name)
    .Select(g => g.First())
    .ToList();

And for more than 1 key property (e.g. Prop1 and Prop2) in a group:

var distinctList = myList
    .GroupBy(i => new { i.Prop1, i.Prop2 })
    .Select(g => g.First())
    .ToList();

Same can be done with group operator using System.Linq:

var distinctList = (
    from i in recipients
    group i by new { i.Prop1, i.Prop2 } into grp
    select grp.First()).ToList();

Alternatively IEnumerable.Distinct() method can take an IEqualityComparer in the following style

var distinctList = myList.Distinct(
    EqualityFactory.Create<FooType>(
    (x, y) => x.Prop1 == y.Prop1 && x.Prop2 == y.Prop2)
    ).ToList();

Here is the source code of the factory:

// -------------------------------------
// <copyright file="EqualityFactory.cs" company="OpenSource">
//  Copyleft (c) All rights released.
// </copyright>
// -------------------------------------

namespace Common.Helpers
{
  using System;
  using System.Collections.Generic;

  /// <summary>
  /// Factory to produce instances of the <see cref="EqualityComparer{T}" /> class.
  /// <typeparam name="T">the type of an object.</typeparam>
  /// </summary>
  public class EqualityFactory
  {
    /// <summary>
    /// Creates a new instance of the <see cref="IEqualtyComparer{T}" /> class.
    /// </summary>
    /// <typeparam name="T">the type of an object.</typeparam>
    /// <returns>returns an instance of equality comparer.</returns>
    public static IEqualityComparer<T> Create<T>(Func<T, T, bool> funcComparer)
    {
      return new ImpEqualityComparer<T>(funcComparer);
    }

    #region internal equality comparer class

    /// <summary>
    /// Implements <see cref="IEqualityComparer" /> interface.
    /// </summary>
    /// <typeparam name="T">the type of an object.</typeparam>
    private class ImpEqualityComparer<T> : IEqualityComparer<T>
    {
      /// <summary>the comparison function.</summary>
      private Func<T, T, bool> comparer;

      /// <summary>the default comparison function.</summary>
      private IEqualityComparer<T> defaultComparer;

      /// <summary>
      /// Initializes a new instance of the <see cref="ImpEqualityComparer{T}" /> class.
      /// </summary>
      public ImpEqualityComparer(Func<T, T, bool> delegateComparer)
      {
        this.comparer = delegateComparer;
        this.defaultComparer = EqualityComparer<T>.Default;
      }

      /// <summary>
      /// Compares objects by using equality comparer.
      /// </summary> 
      /// <returns>returns True if objects are equal; otherwise False.</returns> 
      bool IEqualityComparer<T>.Equals(T x, T y)
      {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;

        return this.comparer(x, y);
      }

      /// <summary>
      /// Get hash code by the equality comparer.
      /// </summary>
      /// <returns>returns the hash code.</returns>
      int IEqualityComparer<T>.GetHashCode(T obj)
      {
        // In order to use the comparer, the hash code has to be the same.
        return 0;
      }
    }

    #endregion
  }
}
// class EqualityFactory

If hash code comparison is required, use an LambdaEqualityComparer class instead. In the following example, the distinct result will be based on objects’ Name, and other properties (Prop1 and Prop2).

var distinctList = myList.Distinct(
    new LambdaEqualityComparer<FooType>(
    (x, y) => x.Prop1 == y.Prop1 && x.Prop2 == y.Prop2,
    (t) => t.Name.GetHashCode()) // assume t.Name is a string
    ).ToList();

See LambdaEqualityComparer class source code:

// -------------------------------------
// LambdaEqualityComparer.cs
// -------------------------------------

namespace Common.Helpers
{
  using System;
  using System.Collections.Generic;

  /// <summary>
  /// Implements <see cref="IEqualityComparer" /> interface.
  /// </summary>
  /// <typeparam name="T">the type of an object.</typeparam>
  public class LambdaEqualityComparer<T> : IEqualityComparer<T>
  {
    /// <summary>
    /// Initializes a new instance of the <see cref="LambdaEqualityComparer{T}" /> class.
    /// </summary>
    public LambdaEqualityComparer(
      Func<T, T, bool> funcEquals, 
      Func<T, int> funcGetHashCode = null)
    {
      if (funcEquals == null)
      {
        throw new ArgumentNullException("funcEquals", "An equals function is required.");
      }
      this.GetHashCodeMethod = funcGetHashCode;
      this.EqualsMethod = funcEquals;
    }

    /// <summary>Gets and sets the method used to compute equals.</summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }

    /// <summary>Gets and sets the method used to compute a hash code.</summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    /// <summary>
    /// Implements Equals from <see cref="IEqualityComparer{T}" /> interface.
    /// </summary>
    /// <returns>returns result of the comparison.</returns>
    bool IEqualityComparer<T>.Equals(T x, T y)
    {
      return this.EqualsMethod(x, y);
    }

    /// <summary>
    /// Implements GetHashCode from <see cref="IEqualityComparer{T}" /> interface.
    /// </summary>
    /// <returns>returns hash code.</returns>
    int IEqualityComparer<T>.GetHashCode(T obj)
    {
      if (this.GetHashCodeMethod == null) return 0;

      return this.GetHashCodeMethod(obj);
    }
  }  
}// class LambdaEqualityComparer

More advanced, to wrap GetHashCode into a projection (MiscUtil):

// -------------------------------------
// ProjectionEqualityComparer.cs
// -------------------------------------

namespace Common.Helpers
{
  using System;
  using System.Collections.Generic;

  /// <summary>
  /// Comparer uses projected keys from source element.
  /// </summary>
  /// <typeparam name="TSource">Type of elements the comparer to project.</typeparam>
  /// <typeparam name="TKey">Type of the key projected from the element.</typeparam>
  public class ProjectionEqualityComparer<TSource, TKey> : IEqualityComparer<TSource>
  {
    /// <summary>the comparison function.</summary>
    private readonly Func<TSource, TKey> projection;

    /// <summary>the equality comparer.</summary>
    private readonly IEqualityComparer<TKey> comparer;

    /// <summary>
    /// Initializes a new instance of the <see cref="ProjectionEqualityComparer{TSource, TKey}" />.
    /// Using default comparer for the projected type.
    /// </summary>
    public ProjectionEqualityComparer(
      Func<TSource, TKey> projection) : this(projection, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ProjectionEqualityComparer{TSource, TKey}" />.
    /// The default comparer for the projected type is used if a comparer not specified.
    /// </summary>
    public ProjectionEqualityComparer(
      Func<TSource, TKey> projection, IEqualityComparer<TKey> comparer)
    {
      if (projection == null)
      {
        throw new ArgumentNullException("projection");
      }
      this.comparer = comparer ?? EqualityComparer<TKey>.Default;
      this.projection = projection;
    }

    /// <summary>
    /// Compares the two specified values for equality by applying the projection to
    /// each value and then using the equality comparer on the resulting keys.
    /// Null references are never passed to the projection.
    /// </summary>
    /// <returns>returns True if objects are equal; otherwise False.</returns>
    public bool Equals(TSource x, TSource y)
    {
      if (x == null && y == null) return true;
      if (x == null || y == null) return false;

      return this.comparer.Equals(this.projection(x), this.projection(y));
    }

    /// <summary>
    /// Produces a hash code for the given value by projecting it and then
    /// asking the equality comparer to find the hash code of the resulting key.
    /// </summary>
    /// <returns>returns the hash code.</returns>
    public int GetHashCode(TSource obj)
    {
      if (obj == null)
      {
        throw new ArgumentNullException("obj");
      }
      return this.comparer.GetHashCode(this.projection(obj));
    }
  }
}// class ProjectionEqualityComparer

Or using string value of a property name (see Cuemon.Reflection):

// -------------------------------------
// PropertyEqualityComparer.cs
// -------------------------------------

namespace Common.Helpers
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Reflection;

  /// <summary>
  /// Implements <see cref="IEqualityComparer{T}" /> interface.
  /// </summary>
  /// <typeparam name="T">the type of an object.</typeparam>
  public class PropertyEqualityComparer<T> : IEqualityComparer<T>
  {
    /// <summary>the <see cref="PropertyInfo"/> object.</summary>
    private PropertyInfo propertyInfo;

    /// <summary>
    /// Initializes a new instance of the <see cref="PropertyEqualityComparer{T}" /> class.
    /// </summary>
    public PropertyEqualityComparer(string propertyName)
    {
      // store a reference to the <see cref="PropertyInfo"/> for use in comparison
      this.propertyInfo = typeof(T).GetProperty(
        propertyName, 
        BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public
        );
      if (this.propertyInfo == null)
      {
        var message = string.Format(
          "The name '{0}' is not a property of type {1}.", 
          propertyName, typeof(T));
        throw new ArgumentException(message);
      }
    }

    #region Methods :: Implments IEqualityComparer

    /// <summary>
    /// Implements Equals from <see cref="IEqualityComparer{T}" /> interface.
    /// </summary>
    /// <returns>returns result of the comparison.</returns>
    public bool Equals(T x, T y)
    {
      // get the current value of the comparison property of x and of y
      var valueX = this.propertyInfo.GetValue(x, null);
      var valueY = this.propertyInfo.GetValue(y, null);
    
      // consider equal only if both xValue and yValue are null
      if (valueX == null)
      {
        return valueY == null;
      }
      // use default comparer
      return valueX.Equals(valueY);
    }

    /// <summary>
    /// Implements GetHashCode from <see cref="IEqualityComparer{T}" /> interface.
    /// </summary>
    /// <returns>returns hash code.</returns>
    public int GetHashCode(T obj)
    {
      var propertyValue = this.propertyInfo.GetValue(obj, null);

      if (propertyValue != null)
      {
        return propertyValue.GetHashCode();
      }
      return 0;
    }
  
    #endregion
  }  
}

Happy coding!

Written by Boathill

2014-01-02 at 22:00

CCNet vs. CruiseControl

with 22 comments


Set once and let it go, this is how Continuous Integration works for automated build process along with development cycle. For a large number of projects, the maintenance for such continously integrated build environment could be complicate and cumbersome. This article will provide and discuss sample config files from practice experience to make easy using CruiseControl.NET and CruiseControl with subversion repository. For comparison of all other similar products, see ThoughtWorks CI Feature Matrix.

Contents

CruiseControl.NET

CCNET (CruiseControl.NET) starts with ccnet.config:

<!--ccnet.config-->
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE cruisecontrol SYSTEM "file:ccnet_definitions.dtd">

<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
  <cb:include href="ccnet_definitions.xml" xmlns:cb="urn:ccnet.config.builder"/>

  <queue name="$(computername)" duplicates="ApplyForceBuildsReAdd" />
  <cb:include href="ccnet_Project.Standard_Solution.xml" xmlns:cb="urn:ccnet.config.builder"/>

  <cb:include href="ccnet_MyProject.xml" xmlns:cb="urn:ccnet.config.builder"/>
</cruisecontrol>

In order to maintain CCNET config and projects in clear XML code, the ccnet.config in above example takes advantage of CCNET powerful preprocessor features. There are three types of CCNET configuration preprocessors: constant (including text/variable and nodeset XML fragment), nested expansion (defining a class whose instance can take parameters), and include file (<cb:include />).

Including external config files helps putting constant/variable definitions, project definitions, and reusable XML pieces together in an organized structure, so that ccnet.config can be read in a from-top-to-down order while presenting high-level view in the main config. Another advanced use of include files is to allow editing definition or project file individually without touching ccnet.config. CCNET has the ability to notice any include file changed and reload ccnet.config automatically.

Like writing a program, the first thing in design is to define constants, variables, and reusable functions (or templates) that can be shared across the projects. These are all implemented by <cb:define /> in CCNET. Constants and XML fragments can be defined by DTD, such as in XHTML DTDs and following sample of ccnet_definitions.dtd included in ccnet.config.

<!--ccnet_definitions.dtd-->
<!ENTITY dir_ccnet "$(ProgramFiles)\CruiseControl.NET">
<!ENTITY dir_ccnet_server "$(ProgramFiles)\CruiseControl.NET\server">
<!ENTITY dir_ccnet_artifacts "$(ProgramFiles)\CruiseControl.NET\server\projects">
<!ENTITY dir_svn "$(ProgramFiles)\CollabNet Subversion Server">
<!ENTITY dir_svn_target "\\10.0.1.100\svnbuilds">
<!ENTITY dir_dotnet "$(SystemRoot)\Microsoft.NET\Framework\v3.5">
<!ENTITY dir_msvs "$(ProgramFiles)\Microsoft Visual Studio 9.0">

<!ENTITY svn_server "svn://192.168.0.100">
<!ENTITY auth_svn "<username>svn_user</username><password>svn_password</password>" >

<!ENTITY exec_devenv "<executable>&dir_msvs;\Common7\IDE\devenv.com</executable>" >
<!ENTITY exec_nmake "<executable>&dir_msvs;\VC\bin\nmake.exe</executable>" >
<!ENTITY exec_msbuild "<executable>&dir_dotnet;\MSBuild.exe</executable>" >
<!ENTITY exec_svn "<executable>&dir_svn;\svn.exe</executable>" >

<!ENTITY url_ccnet "http://localhost/ccnet">

However, entity definition has its limitation in use (such as in string value of a property) and modifying a system file included by <!DOCTYPE > cannot trigger ccnet.config to reload. In CCNET, <cb:define /> is recommended to perform the same and even better job. The following sample illustrates syntax of defining text constant (variable) and nodeset (xml fragment). The third usage of <cb:define />, nested expansion, is for reusable class that can take parameters by instance, such as defining a project template.

<!--ccnet_definitions.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<!--# Preprocessor: Text Constants -->
  <cb:define const_name="value" /> <-- defines $(const_name), or <cb:const_name/> -->
  <cb:define dir_ccnet="$(ProgramFiles)\CruiseControl.NET" />
  <cb:define dir_ccnet_server="$(dir_ccnet)\server" />
  <cb:define dir_ccnet_artifacts="$(dir_ccnet_server)\projects" />
  <cb:define dir_ccnet_buildlogger="$(dir_ccnet)\server\ThoughtWorks.CruiseControl.MsBuild.dll" />
  <cb:define dir_dotnet="$(SystemRoot)\Microsoft.NET\Framework\v3.5" />
  <cb:define dir_svn="$(ProgramFiles)\CollabNet Subversion Server" />
  <cb:define dir_svn_source="c:\svn_checkout" />
  <cb:define dir_svn_builds="d:\svn_builds" />
  <cb:define dir_svn_target="\\10.0.1.100\svnbuilds" />
  <cb:define dir_system32="$(SystemRoot)\system32" />
  <cb:define dir_msvs="$(ProgramFiles)\Microsoft Visual Studio 9.0" />

  <cb:define svn_username="svn_username" />
  <cb:define svn_password="svn_password" />

  <cb:define url_svn_target="file://///10.0.1.100/svnbuilds" />
  <cb:define url_ccnet="http://localhost/ccnet" />

<!--# Preprocessor: Nodeset Constants -->
  <cb:define name="xml_default_extlink">
    <externalLink name="CCNET Builds [Ctrl+Click to open in Explorer]" url="$(url_svn_target)" />
  </cb:define>
  <cb:define name="xml_logger">
    <xmllogger logDir="$(dir_ccnet_server)\projects\$(project)" />
  </cb:define>

  <cb:define name="auth_svn">
    <username>$(svn_username)</username>
    <password>$(svn_password)</password>
  </cb:define>
  <cb:define name="exec_devenv">
    <executable>$(dir_msvs)\Common7\IDE\devenv.com</executable>
  </cb:define>
  <cb:define name="exec_nmake">
    <executable>$(dir_msvs)\VC\bin\nmake.exe</executable>
  </cb:define>
  <cb:define name="exec_vcvars">
    <executable>$(dir_msvs)\VC\vcvarsall.bat</executable>
    <baseDirectory>$(dir_msvs)\VC</baseDirectory>
    <buildArgs>x86</buildArgs>
  </cb:define>
  <cb:define name="exec_msbuild">
    <executable>$(dir_dotnet)\MSBuild.exe</executable>
  </cb:define>
  <cb:define name="exec_svn">
    <executable>$(dir_svn)\svn.exe</executable>
  </cb:define>

<!--# Preprocessor: Nested Expansions (see other ccnet_*.xml)
  <cb:define name="xml_element"><some_element property="$(var1)" /><more>$(var2)</more></cb:define>
  <cb:xml_element var1="value1" var2="value2" />
-->

</cb:config-template>

The use of definitions in CCNET is easy to understand from above sample config file. First of all, any system environment variables can be referenced by $(env_var), where $(env_var) is an environment variable accessible in CCNET runtime context. For example, if CCNET is started by ccnet.exe manually in a Command Prompt window, all system variables and logon user variables should be available; otherwise, if CCNET is started in service.msc, only system variables can be used.

Once a CCNET constant/variable (<cb:define var_name="text" />) or XML fragment (<cb:define name="element_name" >...</cb:define>) is defined, it can be referenced immediately afterward thru whole CCNET runtime environment. The constant/variable can be referenced as a string by $(var_name), or <cb:var_name />. The XML fragment must be referenced by <cb:element />. All <cb:define /> are global definitions. To control the scope of a preprocessor definition, use <cb:scope />. See configuration preprocessors.

Next, let’s take a look at how to use CCNET nested expansions and parameters of preprocessor definition to create a project template. This template can be used to meet the following conditions:

  • All project properties can be initiated by an instance of the template
  • There is only one build target path needed to be published
  • There is only one developer and who will be in notification for all build states
  • The project can be built by a MS Visual Studio 2008 solution file

Similar to ccnet_definitions.xml, ccnet_Project.Standard_Solution.xml uses <cb:config-template /> but only defines one XML fragment named “project_template_solution“.

<!--ccnet_Project.Standard_Solution.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:define name="project_template_solution">
<project name="$(project_name)" queue="$(project_queue)" queuePriority="0">
  <category>$(project_category)</category>
  <workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
  <artifactDirectory>$(dir_ccnet_artifacts)\$(project_name)</artifactDirectory>
  <webURL>$(url_ccnet)</webURL>

  <labeller type="svnRevisionLabeller">
    <prefix>$(project_name)-r</prefix>
    <major>0</major>
    <minor>0</minor>
    <url>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</url>
    <cb:auth_svn />
  </labeller>
  <!--
  <labeller type="lastChangeLabeller"><prefix>$(project_name)-</prefix></labeller>
  -->
    <triggers>
    <!--NOTE: build machine must sync clock with svn server!!!-->
    <intervalTrigger buildCondition="IfModificationExists" seconds="$(project_checkTime)" initialSeconds="60" />
  </triggers>
  <sourcecontrol type="svn">
    <trunkUrl>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</trunkUrl>
    <cb:auth_svn />
    <cb:exec_svn />
    <workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
    <timeout units="seconds">$(project_checkTime)</timeout>
  </sourcecontrol>

  <tasks>
    <msbuild>
      <cb:exec_msbuild />
      <projectFile>$(project_solution)</projectFile>
      <workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
      <buildArgs>/noconsolelogger /p:Configuration=Release</buildArgs>
      <logger>$(dir_ccnet_buildlogger)</logger>
    </msbuild>
  </tasks>

  <publishers>
    <cb:xml_logger project="$(project_name)" />

    <buildpublisher>
      <sourceDir>$(dir_svn_source)\$(project_name)\$(project_buildTarget)</sourceDir>
      <publishDir>$(dir_svn_target)</publishDir>
      <useLabelSubDirectory>true</useLabelSubDirectory>
      <alwaysPublish>false</alwaysPublish>
    </buildpublisher>

    <cb:include href="ccnet_email.xml" xmlns:cb="urn:ccnet.config.builder" />
    <cb:ccnet_email
        developer_name="Developer" developer_alias="$(project_developer)"
        notification="Always"
        />
  </publishers>

</project>
</cb:define>

</cb:config-template>

Different from ccnet_definitions.xml, ccnet_Project.Standard_Solution.xml also uses some variables that have never been defined, such as $(project_name), $(project_path), and etc., basically anything with “project_” prefix in this example. This is the exclusive feature of CCNET preprocessor that allow all these variable dynamically bound to an instance of the template when it is used. In ccnet.config, after ccnet_Project.Standard_Solution.xml is loaded, a project file ccnet_MyProject.xml comes after to initiate a project configuration based on the template. Here is code:

<!--ccnet_MyProject.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:project_template_solution
    project_name="My Project"
    project_alias="myproject"
    project_category="My Category"
    project_path="MyProject"
    project_root=""
    project_branch="trunk"
    project_svnServer="192.168.10.100"
    project_svnPath="svn://192.168.10.100/Software/MyProject/trunk"
    project_solution="MyProject.sln"
    project_buildTarget="Setup\Release"
    project_buildTime="1800"
    project_checkTime="600"
    project_developer="myalias"
    project_queue="$(computername)"
/>

</cb:config-template>

The properties defined in “My Project” configuration makes the template used in complete. Actually, the property names after cb:project_template_solution may not be necessary matching what have been defined in the template. At least CCNET does not check if a referenced constant or variable is predefined. Undefined constant/variable will be replaced by empty string (- be caution that this could cause exception at the runtime). And any property defined in the instance but not used in the template will be ignored (e.g. project_svnPath).

CCNET preprocessor config template can be nested. Here is the file of ccnet_email.xml that has been included in ccnet_Project.Standard_Solution.xml:

<!--ccnet_email.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">

  <cb:define name="ccnet_email">
    <email from="Builder@mycompany.com" 
           mailhost="192.168.10.25" includeDetails="TRUE" useSSL="FALSE"
           subjectPrefix="CCNET:" description="CruiseControl.NET E-mail Notification ">
      <users>
        <user name="Builder" group="Builder" address="builder@mycompany.com"/>
        <user name="$(developer_name)" group="Developer" address="$(developer_alias)@mycompany.com"/>
        <user name="Build Request" group="Supervisor" address="buildrequest@mycompany.com"/>
        <user name="Admin" group="Admin" address="admin@mycompany.com"/>
      </users>
      <groups>
        <group name="Admin" notification="Failed"/>
        <group name="Builder" notification="Success"/>
        <group name="Developer" notification="$(notification)"/>
      </groups>

      <modifierNotificationTypes>
        <NotificationType>Always</NotificationType>
        <NotificationType>Failed</NotificationType>
        <NotificationType>Fixed</NotificationType>
        <NotificationType>Success</NotificationType>
        <NotificationType>Change</NotificationType>
      </modifierNotificationTypes>

      <subjectSettings>
        <subject buildResult="Broken" value="Build Failed - ${CCNetProject} - [Broken] :: ${CCNetBuildCondition}" />
        <subject buildResult="StillBroken" value="Build Failed - ${CCNetProject} - [StillBroken] :: ${CCNetBuildCondition}" />
        <subject buildResult="Fixed" value="Build Success - ${CCNetProject} - [Fixed] :: ${CCNetBuildCondition}" />
        <subject buildResult="Exception" value="Build Failed - ${CCNetProject} - [Exception] :: ${CCNetBuildCondition}" />
        <subject buildResult="Success" value="Build Success - ${CCNetProject} - [Success] :: ${CCNetBuildCondition}" />
      </subjectSettings>
    </email>
  </cb:define>

  <!--# after <cb:include href="ccnet_email.xml" />, define following properties:
        developer_name  - developer name
        developer_alias - developer email alias
        notification    - notification type: Always|Change|Failed|Success|Fixed
  <cb:ccnet_email
      developer_name=""
      developer_alias=""
      notfication=""
    />
  ==-->
</cb:config-template>

For label variables used in <subjectSettings />, please refer to CCNET Email Publisher.

The last thing left uncovered in the project template is “svnRevisionLabeller”. This is a modified plugin based on original svnrevisionlabeller. The code (SvnRevisonLabeller.cs) is attached below:

/**
 ****************************************
 * Class Name:  SvnRevisionLabeller
 * Description: Subversion functions used in NAnt
 * Requisite:   Reference to NAnt.Core.dll
 * Notes:       See http://code.google.com/p/svnrevisionlabeller/
 ****************************************
 */
using System;
using System.Xml;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Core.Util;

namespace ccnet.SvnRevisionLabeller.plugin
{
 /// <summary>
 /// Generates label numbers using the Subversion revision number.
 /// </summary>
 /// <remarks>
 /// This class was inspired by Jonathan Malek's post on his blog 
 /// (<a href="http://www.jonathanmalek.com/blog/CruiseControlNETAndSubversionRevisionNumbersUsingNAnt.aspx">CruiseControl.NET and Subversion Revision Numbers using NAnt</a>),
 /// which used NAnt together with Subversion to retrieve the latest revision number. This plug-in moves it up into 
 /// CruiseControl.NET itself, so that you can see the latest revision number appearing in CCTray. The label can
 /// then be retrieved from within NAnt by accessing the property <c>${CCNetLabel}</c>.
 /// </remarks>
 [ReflectorType("svnRevisionLabeller")]
 public class SvnRevisionLabeller : ILabeller
 {
  #region Private members

  private int major;
  private int minor;
  private string _url;
  private string executable;
  private string prefix;
  private string username;
  private string password;
  private const string RevisionXPath = "/log/logentry/@revision";

  #endregion

  #region Constructors

  /// <summary>
  /// Initializes a new instance of the <see cref="SvnRevisionLabeller"/> class.
  /// </summary>
  public SvnRevisionLabeller()
  {
   major = 1;
   minor = 0;
   executable = "svn.exe";
   prefix = String.Empty;
  }

  #endregion

  #region Public methods

  /// <summary>
  /// Returns the label to use for the current build.
  /// </summary>
  /// <param name="resultFromLastBuild">IntegrationResult from last build used to determine the next label</param>
  /// <returns>the label for the new build</returns>
  public string Generate(IIntegrationResult resultFromLastBuild)
  {
   // Get the last revision from the Subversion repository
   int svnRevision = GetRevision();

   // Get the last revision from CruiseControl
   Version version = ParseVersion(svnRevision, resultFromLastBuild);

   // If the revision number hasn't changed (because no new check-ins have been made), increment the build number;
   // Otherwise, reset the build number to 0
   int revision = (svnRevision == version.Build) ? version.Revision + 1 : 0;

   // Construct a new version number, adding any specified prefix
   Version newVersion = new Version(major, minor, svnRevision, revision);

            if (major == 0 && minor == 0)
            {
                // <major>0</major><minor>0</minor> must be explicitly defined
                // assume we only care about subversion revision - 2008-11-18 jzhu@zetron.com
                return prefix + svnRevision;
            }
            else // keep original usage for a 4-field version format
            {
                return prefix + newVersion;
            }
  }

  /// <summary>
  /// Runs the task, given the specified <see cref="IIntegrationResult"/>, in the specified <see cref="IProject"/>.
  /// </summary>
  /// <param name="result"></param>
  public void Run(IIntegrationResult result)
  {
   result.Label = Generate(result);
  }

  #endregion

  #region Public properties

  /// <summary>
  /// Gets or sets the major build number.
  /// </summary>
  /// <value>The major build number.</value>
  [ReflectorProperty("major", Required=false)]
  public int Major
  {
   get
   {
    return major;
   }
   set
   {
    major = value;
   }
  }

  /// <summary>
  /// Gets or sets the minor build number.
  /// </summary>
  /// <value>The minor build number.</value>
  [ReflectorProperty("minor", Required=false)]
  public int Minor
  {
   get
   {
    return minor;
   }
   set
   {
    minor = value;
   }
  }

  /// <summary>
  /// Gets or sets the repository URL from which the <c>svn log</c> command will be run.
  /// </summary>
  /// <value>The repository.</value>
  [ReflectorProperty("url", Required = true)]
  public string Url
  {
   get
   {
    return _url;
   }
   set
   {
    _url = value;
   }
  }

  /// <summary>
  /// Gets or sets the Subversion client executable.
  /// </summary>
  /// <value>The path to the executable.</value>
  /// <remarks>
  /// If the value is not supplied, the task will expect to find <c>svn.exe</c> in the <c>PATH</c> environment variable.
  /// </remarks>
  [ReflectorProperty("executable", Required=false)]
  public string Executable
  {
   get
   {
    return executable;
   }
   set
   {
    executable = value;
   }
  }

  /// <summary>
  /// Gets or sets an optional prefix for the build label.
  /// </summary>
  /// <value>A string to prefix the version number with.</value>
  [ReflectorProperty("prefix", Required=false)]
  public string Prefix
  {
   get 
   { 
    return prefix; 
   }
   set 
   {
    prefix = value;
   }
  }

  /// <summary>
  /// Gets or sets the username to access SVN repository.
  /// </summary>
  /// <value>The repository.</value>
  [ReflectorProperty("username", Required = false)]
  public string Username
  {
   get
   {
    return username;
   }
   set
   {
    username = value;
   }
  }

  /// <summary>
  /// Gets or sets the password to access SVN repository.
  /// </summary>
  /// <value>The repository.</value>
  [ReflectorProperty("password", Required = false)]
  public string Password
  {
   get
   {
    return password;
   }
   set
   {
    password = value;
   }
  }

  #endregion

  #region Private methods

  /// <summary>
  /// Parses the version.
  /// </summary>
  /// <param name="revision">The revision.</param>
  /// <param name="resultFromLastBuild">The result from last build.</param>
  private Version ParseVersion(int revision, IIntegrationResult resultFromLastBuild)
  {
   try
   {
    string label = resultFromLastBuild.LastSuccessfulIntegrationLabel;
    if (prefix.Length > 0)
    {
     label = label.Replace(prefix, String.Empty).TrimStart('_');
    }
    return new Version(label);
   }
   catch (SystemException)
   {
    return new Version(major, minor, revision, 0);
   }
  }

  /// <summary>
  /// Gets the latest Subversion revision by checking the last log entry.
  /// </summary>
  private int GetRevision()
  {
   // Set up the command-line arguments required
   ProcessArgumentBuilder argBuilder = new ProcessArgumentBuilder();
   argBuilder.AppendArgument("log");
   argBuilder.AppendArgument("--xml");
   argBuilder.AppendArgument("--limit 1");
   argBuilder.AppendArgument(Url);
   if (Username != null && Username.Length > 0 && Password != null && Password.Length > 0)
   {
    AppendCommonSwitches(argBuilder); 
   }

   // Run the svn log command and capture the results
   ProcessResult result = RunProcess(argBuilder);
   Log.Debug("Received XML : " + result.StandardOutput);

   // Load the results into an XML document
   XmlDocument xml = new XmlDocument();
   xml.LoadXml(result.StandardOutput);

   // Retrieve the revision number from the XML
   XmlNode node = xml.SelectSingleNode(RevisionXPath);
   return Convert.ToInt32(node.InnerText);
  }

  /// <summary>
  /// Appends the arguments required to authenticate against Subversion.
  /// </summary>
  /// <param name="buffer"><The argument builder./param>
  private void AppendCommonSwitches(ProcessArgumentBuilder buffer)
  {
   buffer.AddArgument("--username", Username);
   buffer.AddArgument("--password", Password);
   buffer.AddArgument("--non-interactive");
   buffer.AddArgument("--no-auth-cache");
  }

  /// <summary>
  /// Runs the Subversion process using the specified arguments.
  /// </summary>
  /// <param name="arguments">The Subversion client arguments.</param>
  /// <returns>The results of running the process, including captured output.</returns>
  private ProcessResult RunProcess(ProcessArgumentBuilder arguments)
  {
   ProcessInfo info = new ProcessInfo(executable, arguments.ToString(), null);
   Log.Debug("Running Subversion with arguments : " + info.Arguments);

   ProcessExecutor executor = new ProcessExecutor();
   ProcessResult result = executor.Execute(info);
   return result;
  }
  #endregion
 }
}

CruiseControl

Comparing to CCNET, CruiseControl does not provide very powerful preprocessor and template feature. There is no syntax to define XML fragment (except using DTD). The main config file, config.xml, cannot be automatically reloaded if any configuration changed.

<!--config.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--# Always deploy ccnet.config with following files under ${env.CCDIR}
                    config.properties
                    cc_antpublisher_copy.xml
                    prj_*.xml
      also place cruisecontrol.jar under "${env.CCDIR}\lib" to overwrite
      original one for a customized new plugin: <ZSVNLabeller />
==-->
<cruisecontrol>
  <property file="config.properties" />
  <property environment="env" toupper="true" />
  <property name="url_cruisecontrol" value="${url_cchost}/cruisecontrol" />
  <property name="url_buildresults" value="${url_cruisecontrol}/buildresults" />
  <property name="url_ccdashboard" value="${url_cchost}/dashboard" />
  <property name="url_ccdoc" value="${url_cchost}/documentation" />

  <include.projects file="prj_MyProject.xml" />

</cruisecontrol>

As in above sample of config.xml, property definition is either by <property name="var" value="text" /> element or an external file (e.g. config.properties).

###############################################################################
# Filename: config.properties
#    Usage: called by cruisecontrol element, i.e.:
#           <property file="config.properties" />
#   Syntax: defined by the class java.util.Properties, with the same rules
#           about how non-ISO8859-1 characters must be escaped.
###############################################################################

### default value predefinitions
def_hostip=10.0.1.194
def_hostname=hostname
def_project_buildTime=1800
def_project_checkTime=600

### dir/path predefinitions
dir_cruisecontrol=%ProgramFiles%\CruiseControl
dir_svn_target=\\10.0.1.100\svnbuilds
pwd_cruisecontrol=/srv/cruisecontrol-bin-2.8.2
smb_svn_target=/svnbuilds

### svn settings predefinitions
svn_server=svn://192.168.10.100
svn_username=svn_username
svn_password=svn_password

### user id predefinitions
uid_admin=admin
uid_builder=builder

### url predefinitions
url_cchost=http://hostname:8080
url_ccdashboard=http://hostname:8080/dashboard
url_ccdoc=http://hostname:8080/documentation
url_cruisecontrol=http://hostname:8080/cruisecontrol
url_buildresults=http://hostname:8080/cruisecontrol/buildresults
url_ccnet=http://localhost/ccnet

Certainly, a project file (e.g. prj_MyProject.xml) can be included in config.xml so that the configuration is well organized by each individual config file. Here only provides a project template file that a real project config (e.g. prj_MyProject.xml) will be based on.

<!--prj_project.template.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--#
  For each project, use following steps to create a project file from this template by
  copying this file to a new project file, in convention of "prj_$[project_name].xml":
  (1) replace "$[project_name]" with a project name
  (2) replace any place holder definition, like in format of $[place_holder], in property definitions
  (3) optionally use ${def_project_buildTime} for ${project_buildTime}
  (4) optionally add more ${project_buildTarget[n]} properties and publishers
  (5) choose one of the build methods in <project><schedule></schedule></project> block
  (6) replace "\\" to "/" on Linux system
  (7) replace ${dir_svn_target} with ${smb_svn_target} on a Linux build system
  (8) define properties ${eid_*} for build notification; ${uid_builder} has been notified on success
  (9) refer to any property definitions in config.properties
==-->
<cruisecontrol>
  <project name="$[project_name]">
    <property name="project_name"        value="${project.name}" />
    <property name="project_alias"       value="$[project_alias]" />
    <property name="project_category"    value="$[project_category]" />
    <property name="project_root"        value="$[project_root]" />
    <property name="project_path"        value="$[project_path]" />
    <property name="project_branch"      value="$[project_branch]" />
    <property name="project_svnServer"   value="$[project_svnServer]" />
    <property name="project_svnPath"     value="svn://${project_svnServer}/${project_root}${project_path}/${project_branch}" />
    <property name="project_source"      value="${env.CCDIR}\\projects\\${project.name}" />
    <property name="project_buildTarget" value="$[project_buildTarget]" />
    <property name="project_buildTime"   value="${def_project_buildTime}" />
    <property name="project_buildTime"   value="$[project_buildTime]" />
    <property name="project_checkTime"   value="30" />
    <property name="url_cruisecontrol"   value="${url_cruisecontrol}" />
    <property name="eid_failure"         value="builder" />
    <property name="eid_success"         value="builder" />
    <property name="eid_always"          value="developer_alias" />

    <plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN" />
    <plugin name="ZSVNLabeller" classname="net.sourceforge.cruisecontrol.labelincrementers.ZSVNLabeller" />

    <ZSVNLabeller separator="r" labelprefix="r" workingcopypath="${project_source}"
                  />
    <!--#
    <listeners>
      <currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
    </listeners>
    ==-->
    <bootstrappers>
      <!--#
      <antbootstrapper anthome="apache-ant-1.7.0" 
                       buildfile="projects/${project.name}/build.xml" 
                       target="clean" />
     <execbootstrapper command=""
                       args="" workingdir="projects/${project.name}"
                       errorstr="" showProgress=""
                       timeout=""
                       />
      ==-->
      <svnbootstrapper localWorkingCopy="${project_source}"
                       username="${svn_username}"
                       password="${svn_password}"
                       />
    </bootstrappers>

    <modificationset quietperiod="60">
      <!--# <filesystem/> triggers a build if any file in ${project.name} project is touched
      <filesystem folder="${project_source}" includedirectories="false" />
      ==-->
      <!--# <svn/> sets property ${svnrevision}
      ==-->
      <svn localWorkingCopy="${project_source}"
           useLocalRevision="false"
           username="${svn_username}"
           password="${svn_password}"
           property=""
         />
    </modificationset>

    <schedule interval="{project_checkTime}">
      <!--#<ant />
      <ant anthome="apache-ant-1.7.0" buildfile="projects/${project.name}/build.xml"/>
      ==-->
      <!--#<exec/>
      ==-->
      <exec command=".\\build.cmd"
            args='"${project_source}"' workingdir="${project_source}"
            errorstr="Build Failed" showProgress="true"
            timeout="${project_buildTime}"
            />
    </schedule>

    <log>
      <!--# merge from log files with specified pattern from specified dir
      <merge dir="${project_source}\\log" pattern="*.log" />
      ==-->
      <deleteartifacts every="30" unit="DAY" />
      <delete every="30" unit="DAY" />
    </log>

    <publishers>
      <htmlemail buildresultsurl="${url_cruisecontrol}/buildresults/${project_name}"
                 defaultsuffix=".com" mailhost="192.168.10.66" mailport="25" usessl="false"
                 returnaddress="builder@mycompany.com" returnname="Builder"
                 subjectprefix="CruiseControl:"
           >
        <success address="${uid_builder}@mycompany.com" /><!--send to builder success notification-->
        <success address="${eid_success}@mycompany.com" />
        <failure address="${eid_failure}@mycompany.com" />
        <always  address="${eid_always}@mycompany.com"  />
      </htmlemail>

      <onsuccess>
        <antpublisher anthome="apache-ant-1.7.0"
                      buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
                      target="publish_dir"
                      >
          <property name="publish_source" value="${project_source}\\${project_buildTarget}" />
          <property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
        </antpublisher>
        <!--
        <antpublisher anthome="apache-ant-1.7.0"
                      buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
                      target="publish_file"
                      >
          <property name="publish_source" value="${project_source}\\${project_buildTarget}" />
          <property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
        </antpublisher>
        ==-->
      </onsuccess>
    </publishers>

  </project>

</cruisecontrol>

The template cannot be used as nicely as in CCNET. But it gives a start for most projects. Please refer to usage comments at beginning of the template. Also remember since CruiseControl is running on both Windows and Linux systems, backslash or forward-slash need to be carefully used wherever a path is typed.

One of the difference in CruiseControl than CCNET is the sequence of continuous integration cycle. In CCNET, the <sourcecontrol /> compares last build state with repository server, then checkout (for the first time of check) or update source code on local working directory before starting build tasks. But in CruiseControl, the bootstrapper is always called at the beginning to update local working directory from server before using <modificationset /> to detect if tasks in <schedule /> need to be executed.

Such difference implies that the local working directory and source code in CruiseControl project must be manually checked out from the repository (such as `svn co`) at the time when the project is setup, which is not necessary in CCNET – where the local working directory can be created automatically if it does not exist. This also explains why LocalWorkingCopy (rather than RepositoryLocation) property should be used in <svn /> source control under <modificationset /> to detect new commit for a CruiseControl project.

Note: There could be a chance that local working copy has not been updated yet when a new change is committed on repository server after bootstrapper but before modificationset. The race condition will end up building old source against a newer revision label.

Modification of subversion in CruiseControl is parsed from `svn log -r` by checking between last build time and current time. If useLocalRevision property is used in <svn /> source control, the local revision number will be in place of current time.

At the end of the template, artifacts publishing is implemented by the following include file.

<!--cc_antpublisher_coppy.xml-->
<project name="antpublisher_copy" default="publish">
    <target name="publish">
        <echo>Copying ${publish_source} to ${publish_target} ...</echo>
        <copy todir="${publish_target}"><fileset dir="${publish_source}" /></copy>
    </target>
    <target name="publish_dir">
        <echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
        <copy todir="${publish_target}-${label}"><fileset dir="${publish_source}" /></copy>
    </target>
    <target name="publish_file">
        <echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
        <copy todir="${publish_target}-${label}" file="${publish_source}"></copy>
    </target>
    <target name="publish_artifacts">
        <echo>Copying ${publish_source_dir} to ${publish_target} ...</echo>
        <copy todir="${publish_target}">
   <fileset dir="${publish_source_dir}">
    <include name="${publish_source_files}" />
   </fileset>
  </copy>
    </target>
</project>

The Java source code of “ZSVNLabeller” plugin (based on SVNLabelIncrementer, implements LabelIncrementer) is attached as below. The file “ZSVNLabeller.java” should be place under source code folder “net\sourceforge\cruisecontrol\labelincrementers“. After rebuilt, copy “cruisecontrol.jar” to ${env.CCDIR}, where CruiseControl installed.

package net.sourceforge.cruisecontrol.labelincrementers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import net.sourceforge.cruisecontrol.LabelIncrementer;

import org.apache.log4j.Logger;
import org.jdom.Element;

/**
 * This class provides a label incrementation based on svn revision numbers.
 * This class expects the label format to be "x&lt;sep&gt;y[&lt;sep&gt;z]",
 * where x is any String and y is an integer and &lt;sep&gt; a separator, the
 * last part z, is optional, and gets generated and later incremented in case a
 * build is forced. The default separator is "." and can be modified using
 * {@link #setSeparator}.
 *
 * @author Ketan Padegaonkar &lt; KetanPadegaonkar gmail &gt;
 */

/// UPDATE: return revisionNumber if separator is as same as prefix - Boathill

public class ZSVNLabeller implements LabelIncrementer {
    private static final Logger LOG = Logger.getLogger(ZSVNLabeller.class);

    private String workingCopyPath = ".";

    private String labelPrefix = "svn";

    private String separator = ".";

    public boolean isPreBuildIncrementer() {
        return true;
    }

    public String incrementLabel(String oldLabel, Element buildLog) {
        String revisionNumber = "";
        String result = oldLabel;
        try {
            revisionNumber = getSvnRevision();

            if (getSeparator().equals(getLabelPrefix())) {
                return labelPrefix + revisionNumber; // return svn revison only
            }
            if (revisionNumber == null || revisionNumber.equals("")) {
                return labelPrefix;
            }
            result = labelPrefix + getSeparator() + revisionNumber;

            if (oldLabel.indexOf(result) > -1) {
                int lastSeparator = oldLabel.lastIndexOf(getSeparator());
                int firstSeparator = oldLabel.indexOf(getSeparator());
                int lastPart = 1;
                if (lastSeparator != firstSeparator) {
                    String suffix = oldLabel.substring(lastSeparator + 1);
                    lastPart = Integer.parseInt(suffix) + 1;
                }
                result += getSeparator() + lastPart;
            }
            LOG.debug("Incrementing label from " + oldLabel + " to " + result);
        } catch (IOException e) {
            LOG.error("could not execute svn binary", e);
        } catch (NumberFormatException e) {
            LOG.error("could not increment label. Old label was " + oldLabel + ". svn revision was " + revisionNumber,
                    e);
        }

        return result;
    }

    protected String getSvnRevision() throws IOException {
        String rev;
        Process p = null;
        try {
            p = Runtime.getRuntime().exec(new String[]{"svnversion", workingCopyPath});
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
            rev = stdInput.readLine();
        } finally {
            if (p != null) {
                p.destroy();
            }
        }
        LOG.debug("SVN revision is: " + rev);
        return rev;
    }

    public boolean isValidLabel(String label) {
        // we don't mind what the previous label is,
        // when the next label is built, then parsing is performed to add / increment a suffix.
        return true;
    }

    public void setWorkingCopyPath(String path) {
        LOG.debug("Working Path is: " + path);
        workingCopyPath = path;
    }

    public String getLabelPrefix() {
        return this.labelPrefix;
    }

    public void setLabelPrefix(String labelPrefix) {
        this.labelPrefix = labelPrefix;
    }

    public String getDefaultLabel() {
        return getLabelPrefix() + getSeparator() + "0";
    }

    public String getSeparator() {
        return this.separator;
    }

    public void setSeparator(String separator) {
        this.separator = separator;
    }
}

Comparison

Some other Continuous Integration products, e.g. Cruise and Hudson, have much nicer web interface, while CruiseControl.NET and CruiseControl provides better flexibility and can be greatly customized. However, the flexibility sometimes means difficulty. For example, CruiseControl has three web sites – dashboard (:8080), project cruisecontrol (:8000), and JMS console – but not be seen on one page; CruiseControl supports CCNET CCTray but could not integrate with CCNET projects or dashboard (even if both developed by ThoughtWorks).

The build publisher supports using build label in both CCNET (by <labeller />) and CruiseControl (${label} in ant config). And in CCNET, the build publisher has to copy all contents in a specified source to another location although ant or nant can be used to customize such task. CruiseControl web interfaces do have a little more advanced features than CCNET. As an instance, the build target files (artifacts) can be published on CruiseControl dashboard so that can be downloaded. But the artifact in CCNET is just a local folder not accessible from the web.

On the other hand, CruiseControl does not display the runtime log on web interfaces. The build log can only be reviewed after the build is done. In CCNET dashboard, this is provided by a server log link. Although the server log in CCNET is not automatically updated (as nicely as a real time console display in Hudson), it at least gives user some hint before knowing the build succeeded or failed. Having better integration with Windows, CCNET can be run in either command line or service mode. For CruiseControl, you have to write a /etc/init.d/cruisecontrol.

Since CruiseControl.NET derived from CruiseControl, both work the same way and the cofiguration shares similarity. As open source projects, it is convenient to develop or modify plugin to suite need during implementation. CruiseControl is cross-platform by Java technology. CCNET has improved on web integration and preprocessor configuration so that project file can be well structured via nodeset expansion template.

Note: Source code samples are formatted by SyntaxHighlighter.

Written by Boathill

2009-07-16 at 09:00

%d bloggers like this: