Merging package definitons

Continuing on from previous discussions about packages and their definitions, I found myself needing to quickly merge together the definitions of two different packages recently. And that sounded like an opportunity for a simple tool to add to my collection.

The requirement was simple:

  1. Take a package definition xml file, and merge it with the deployable items from a number of other definitions to create a new package.
  2. It should also be a tool that can run without the need for a Sitecore instance.

You could do this in script, but I went with C#.

So to create a command line tool, we need to capture some command line parameters. The same parsing library from the automatic package generation tool will do just fine. We need to capture three parameters, and the definition for them is as follows:

public class CommandLineArguments
{
    [Argument(ArgumentType.Required | ArgumentType.AtMostOnce, ShortName = "p", HelpText = "The primary package file to merge things into.")]
    public string PrimaryFile;

    [Argument(ArgumentType.Required | ArgumentType.MultipleUnique, ShortName="f", HelpText="The set of other package files to be merged.")]       
    public string[] OtherFiles;

    [Argument(ArgumentType.Required | ArgumentType.AtMostOnce, ShortName="o", HelpText="The output file to save with the merged data.")]
    public string OutputFilename;
}

The primary file will be the one we want to add extras in to. The parameter will take a path to find the appropriate file. If the user specifies more than one “other file” then these paths will be collated together into an array. Finally the output file name will be the name to save the resultant merged package to. Simple enough.

So the overall pattern for the application is as follows:

public class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Sitecore package merge");
        Console.WriteLine("======================");

        CommandLineArguments cmdParams = new CommandLineArguments();
        if (CommandLine.Parser.ParseArgumentsWithUsage(args, cmdParams))
        {
            var primaryFile = new PrimaryPackageFile(cmdParams.PrimaryFile);
            var fileSet = new PackageFileSet(cmdParams.OtherFiles);

            primaryFile.Merge(fileSet);

            primaryFile.SaveAs(cmdParams.OutputFilename);
        }

        Console.WriteLine("Done.");
    }
}

If the command line arguments parse successfully then the code will load the primary package file, and then load the other files. It will them merge the other files into the primary one, and save the result.

Package files are just XML documents, so both the primary and other package files can be represented in memory with the same base type:

public class PackageFile
{
    protected XDocument _fileXml;

    public string PackageName { get; private set; }

    public PackageFile(string file)
    {
        _fileXml = XDocument.Load(file);
        PackageName = _fileXml.Root
            .Element("Metadata")
            .Element("metadata")
            .Element("PackageName")
            .Value;
    }

    public IEnumerable<XElement> FetchItemSets()
    {
        foreach (XElement itemSet in _fileXml.Root.Element("Sources").Elements("xfiles"))
        {
            yield return itemSet;
        }
    }
}

The constructor loads the XML into memory as an XDocument object, and then uses a simple query to extract the name from the metadata of the package. It also provides a method to return an enumerable list of all the item sets in the package. FetchItemSets() uses a query to find all the xfiles elements, and then returning an enumerable list of them.

The set of secondary files is just a collection of these PackageFile objects:

public class PackageFileSet
{
    private List<PackageFile> _fileSet = new List<PackageFile>();

    public IEnumerable<PackageFile> PackageFiles { get { return _fileSet; } }

    public PackageFileSet(string[] files)
    {
        foreach(string file in files)
        {
            _fileSet.Add(new PackageFile(file));
        }
    }
}

The constructor takes a list of file names, and loads each one in turn.

The primary file adds an extra methods for merge and save to the basic behaviour for the file:

public class PrimaryPackageFile : PackageFile
{
    public PrimaryPackageFile(string primaryFile) : base(primaryFile)
    {
    }

    public void Merge(PackageFileSet fileSet)
    {
        foreach(var packageFile in fileSet.PackageFiles)
        {
            foreach(var itemSet in packageFile.FetchItemSets())
            {
                itemSet.Element("Name").Value = itemSet.Element("Name").Value + " from " + packageFile.PackageName;
                _fileXml.Root.Element("Sources").Add(itemSet);
            }
        }
    }

    public void SaveAs(string outputName)
    {
        _fileXml.Save(outputName);
    }
}

The SaveAs() method just gets the XDocument to save itself to disk. The merge operation goes through each of the “other” files, and for each of those then goes through the collection of item sets. For each one, the name is updated to include both the item set name and the package name (so we can tell the difference between them later) and then the chunk of XML is merged into the primary file’s tree.

With that code in place we can run a command line like:

PackageMerge.exe /p:Package1.xml /f:Package2.xml /f:Package3.xml /o:output.xml

and end up with a file called output.xml which contains the metadata, files and items from Package1.xml, combined with the files and items from Package2.xml and Package3.xml.

So with some better error handling, and a bit of polish, that’s job done.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s