Updating an XML File in the 14 Hive Using a Custom Timer Job

Applies To: SharePoint 2010, .NET Framework (C#, VB.NET)

As mentioned in a previous post, I’ve recently put together a solution for automatically configuring your SharePoint servers to use the Adobe PDF icon for PDF files. You can download the solution as well as the source for free from CodePlex here: WireBear PDFdocIcon. I’m going to show some of the code as it currently exists below, but be sure to check out the CodePlex site to ensure you have the latest version.

I’ve also provided the bulk of the code and some explanation for installing/uninstalling a custom job from a SharePoint solution in my last post: Implementing a Custom SharePoint Timer Job. In this post we’ll explore what’s actually happening in the execution of the timer job.

The goal is to update the DOCICON.xml file in the 14\TEMPLATE\XML folder within the SharePoint 2010 Hive to include or remove a mapping entry for a specific file extension. Here is the entire DocIconJob class:

The Code:

Imports Microsoft.SharePoint.Administration
Imports System.IO
Imports Microsoft.SharePoint.Utilities
Imports System.Xml

Public Class DocIconJob
    Inherits SPServiceJobDefinition

#Region "Properties"

    Private _dociconPath As String
    Public ReadOnly Property DocIconPath() As String
        Get
            If String.IsNullOrEmpty(_dociconPath) Then _dociconPath = SPUtility.GetGenericSetupPath("TEMPLATE\XML\DOCICON.XML")
            Return _dociconPath
        End Get
    End Property

    Private Const InstallingKey As String = "DocIconJob_InstallingKey"
    Private Property _installing() As Boolean
        Get
            If Properties.ContainsKey(InstallingKey) Then
                Return Convert.ToBoolean(Properties(InstallingKey))
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If Properties.ContainsKey(InstallingKey) Then
                Properties(InstallingKey) = value.ToString
            Else
                Properties.Add(InstallingKey, value.ToString)
            End If
        End Set
    End Property

    Private Const FileExtensionKey As String = "DocIconJob_FileExtensionKey"
    Private Property _fileExtension() As String
        Get
            If Properties.ContainsKey(FileExtensionKey) Then
                Return Convert.ToString(Properties(FileExtensionKey))
            Else
                Return String.Empty
            End If
        End Get
        Set(ByVal value As String)
            If Properties.ContainsKey(FileExtensionKey) Then
                Properties(FileExtensionKey) = value
            Else
                Properties.Add(FileExtensionKey, value)
            End If
        End Set
    End Property

    Private Const ImageFilenameKey As String = "DocIconJob_ImageFilenameKey"
    Private Property _imageFilename() As String
        Get
            If Properties.ContainsKey(ImageFilenameKey) Then
                Return Convert.ToString(Properties(ImageFilenameKey))
            Else
                Return String.Empty
            End If
        End Get
        Set(ByVal value As String)
            If Properties.ContainsKey(ImageFilenameKey) Then
                Properties(ImageFilenameKey) = value
            Else
                Properties.Add(ImageFilenameKey, value)
            End If
        End Set
    End Property

#End Region

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(JobName As String, service As SPService, Installing As Boolean, FileExtension As String, ImageFilename As String)
        MyBase.New(JobName, service)
        _installing = Installing
        _fileExtension = FileExtension
        _imageFilename = ImageFilename
    End Sub

    Public Overrides Sub Execute(jobState As Microsoft.SharePoint.Administration.SPJobState)
        UpdateDocIcon()
    End Sub

    Private Sub UpdateDocIcon()
        Dim x As New XmlDocument
        x.Load(DocIconPath)

        Dim mapNode As XmlNode = x.SelectSingleNode(String.Format("DocIcons/ByExtension/Mapping[@Key='{0}']", _fileExtension))

        If _installing Then
            'Create DocIcon entry
            If mapNode Is Nothing Then
                'Create Attributes
                Dim keyAttribute As XmlAttribute = x.CreateAttribute("Key")
                keyAttribute.Value = _fileExtension
                Dim valueAttribute As XmlAttribute = x.CreateAttribute("Value")
                valueAttribute.Value = _imageFilename

                'Create Node
                mapNode = x.CreateElement("Mapping")
                mapNode.Attributes.Append(keyAttribute)
                mapNode.Attributes.Append(valueAttribute)

                Dim byExtensionNode = x.SelectSingleNode("DocIcons/ByExtension")
                Dim NodeAdded As Boolean = False
                If byExtensionNode IsNot Nothing Then
                    'Add in alphabetic order
                    For Each mapping As XmlNode In byExtensionNode.ChildNodes
                        If mapping.Attributes("Key").Value.CompareTo(_fileExtension) > 0 Then
                            byExtensionNode.InsertBefore(mapNode, mapping)
                            NodeAdded = True
                            Exit For
                        End If
                    Next

                    If Not NodeAdded Then byExtensionNode.AppendChild(mapNode)
                    x.Save(DocIconPath)
                End If
            End If
        Else
            'Remove DocIcon entry
            If mapNode IsNot Nothing Then
                Dim byExtensionNode = x.SelectSingleNode("DocIcons/ByExtension")
                If byExtensionNode IsNot Nothing Then
                    byExtensionNode.RemoveChild(mapNode)
                    x.Save(DocIconPath)
                End If
            End If
        End If
    End Sub

End Class

What’s Going On:

Lines 9-73 are just the declaration of and logic needed to persist some properties. Again more information can be found in my last post, but basically I am using the SPJobDefinition’s Properties HashTable to store my own properties as specified in the constructor. Except for in the case of the DocIconPath property which is really just wrapping up some logic to get a reference to the 14 Hive’s TEMPLATE\XML directory using the SPUtility class.

The Execute method beginning in line 86 is what is called when the Timer Job actually runs. I override this method to ensure my custom code gets called instead. My custom code really begins in the UpdateDocIcon method starting at line 90.

In lines 91-94, I load the DOCICON.xml file into and XmlDocument object and attempt to find the mapping node that applies to the appropriate file extension (In this case it’s going to be pdf).

If this job is installing (Running on Solution Activation), then I just check to see if the node was found. If so, all done! If not, then it’s time to add it. I create the node and setup it’s attributes in lines 100-108 using standard objects from the System.Xml namespace.

In order to work, the mapping node needs to be added as a child of the ByExtension element, so we find that in line 110. By default the mapping nodes are listed in alphabetical order by their extension. Since I’m anal, I use a method in lines 114-120 presented by Steve Goodyear to ensure I insert the mapping node in it’s proper position. Failing that, I add it to the end in Line 122 and save the file in line 123.

If this job is uninstalling (Running on Solution Deactivation) and the mapping exists, we delete it and save the file in lines 128-134.

Isn’t that Super Exciting?!?! Hopefully this example will help make the concepts I was talking about in my previous post make some sense. If not, then sadness will fill my soul and flowers will no longer bloom or something.

Implementing a Custom SharePoint Timer Job

Applies To: SharePoint 2010

As mentioned in my previous post, I’ve recently put together a solution for automatically configuring your SharePoint servers to use the Adobe PDF icon for PDF files. You can download the solution as well as the source for free from CodePlex here: WireBear PDFdocIcon. I’m going to show some of the code as it currently exists below, but be sure to check out the CodePlex site to ensure you have the latest version.

In order to perform the necessary work on each server in the farm, the PDFdocIcon solution uses a custom Timer Job. This post will focus on the plumbing necessary to setup your own custom timer job that runs on every server in the farm. The actual code to change the DOCICON.XML file will be saved for later.

Choosing Your Job Definition Type

To make your own Timer Job you’ll want to subclass an exisiting Job Definition object and override the Execute method. There are several to choose from, here’s a helpful table:

Job Definitions you can inherit from in the Microsoft.SharePoint.Administration namespace:
SPAdministrationServiceJobDefinition Invokes the SharePoint Administration Service
SPAllSitesJobDefinition Iterates through all sites in a Web Application
SPContentDatabaseJobDefinition Executed per Web Application and each Content Database is processed by individual jobs (Pausable)
SPFirstAvailableServiceJobDefinition Timer Job that runs on the first available server where the specified service exists (Pausable)
SPJobDefinition Base Class for Timer Jobs (Generally, this is the one to use)
SPPausableJobDefinition Timer Job that can be paused
SPServerJobDefinition Executed on a specific server (Pausable)
SPServiceJobDefinition Runs on every server in the farm where the service exists (Pausable) – This is the one I chose
SPWorkItemJobDefinition  Works with the Timer Job to process work items (Pausable)

For simple jobs the SPJobDefinition is the most flexible and is what you’ll generally want to use. For the PDFdocIcon solution, I needed the Timer Job to execute on every server in the farm. So I used the SPServiceJobDefinition and specified the Timer Service.

Storing Persistent Properties

You may not need properties, but if you’re doing anything even mildly complex you probably will. There are a couple of different alternatives here, but basically your properties need to serialize down to strings. You can look up a few examples of custom properties objects that do this, or you can just use my method of storing your properties in the JobDefinition’s Properties object (HashTable).

Here’s how I store the Boolean property _installing:

    Private Const InstallingKey As String = "DocIconJob_InstallingKey"
    Private Property _installing() As Boolean
        Get
            If Properties.ContainsKey(InstallingKey) Then
                Return Convert.ToBoolean(Properties(InstallingKey))
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If Properties.ContainsKey(InstallingKey) Then
                Properties(InstallingKey) = value.ToString
            Else
                Properties.Add(InstallingKey, value.ToString)
            End If
        End Set
    End Property

Basically, you have a String key for each property that you use to store/retrieve the value from the Properties HashTable. By wrapping those calls in a property you can treat it like a standard variable in the rest of your code and forget all about the specialized storage/retrieval required.

Constructors

You are required to have an empty (parameterless) constructor for serialization, so make sure you’ve got that:

    Public Sub New()
        MyBase.New()
    End Sub

But you will probably need to implement at least a matching constructor with some custom properties. In my Timer Job, I wanted to pass three properties (which I then store using the method above), so I use this:

    Public Sub New(JobName As String, service As SPService, Installing As Boolean, FileExtension As String, ImageFilename As String)
        MyBase.New(JobName, service)
        _installing = Installing
        _fileExtension = FileExtension
        _imageFilename = ImageFilename
    End Sub

Execution

Depending on the base Job Definition class you chose, the Execute method may have a slightly different signature, but either way this is the method to override to provide your own custom logic. In an SPServiceJobDefinition subclass the signature looks like this:

    Public Overrides Sub Execute(jobState As Microsoft.SharePoint.Administration.SPJobState)
        'Custom code here!!
    End Sub

Installing Your Job with a Solution

Using Visual Studio you can create a new Empty SharePoint Project and add your Timer Job class to it. To deploy it you’ll need to add a Feature (Right-click on Features and choose Add Feature). To install your job, you’ll need to add an Event Receiver (Right-click on your new Feature and choose Add Event Receiver).

Uncomment the FeatureActivated and FeatureDeactivating methods. Create a new method (Mine is named RunDocIconJob) with a Boolean and SPFeatureReceiverProperties parameters. This will be the method where we install or uninstall your custom job. In your FeatureActivated and FeatureDeactivating methods call this new method accordingly:

    Public Overrides Sub FeatureActivated(properties As Microsoft.SharePoint.SPFeatureReceiverProperties)
        RunDocIconJob(True, properties)
    End Sub

    Public Overrides Sub FeatureDeActivating(properties As Microsoft.SharePoint.SPFeatureReceiverProperties)
        RunDocIconJob(False, properties)
    End Sub

Then your Job method will look something like this:

    Private _fileExtension As String = "pdf"
    Private _iconFileName As String = "ICPDF.png"

    Public Sub RunDocIconJob(Installing As Boolean, properties As SPFeatureReceiverProperties)
        Dim JobName As String = String.Format("DocIconJob_{0}", _fileExtension)

        'Ensure job doesn't already exist (delete if it does)
        Dim query = From job As SPJobDefinition In properties.Definition.Farm.TimerService.JobDefinitions Where job.Name.Equals(JobName) Select job
        Dim myJobDefinition As SPJobDefinition = query.FirstOrDefault()
        If myJobDefinition IsNot Nothing Then myJobDefinition.Delete()

        Dim myJob As New DocIconJob(JobName, SPFarm.Local.TimerService, Installing, _fileExtension, _iconFileName)

        'Get that job going!
        myJob.Title = String.Format("{0} icon mapping for {1}", IIf(Installing, "Adding", "Removing"), _fileExtension)
        myJob.Update()
        myJob.RunNow()
    End Sub

This is the method I use for my SPServiceJobDefinition. I am not doing any kind of scheduling since this job just runs once on initial deployment and once when being removed. However, you may want to adjust your method to include a schedule (Just set the myJob.Schedule parameter before the Update() call).

Lines 5-10 are finding any existing job definitions that share the same name and deleting them since creating jobs with duplicate names will cause an error. The Title doesn’t have to be unique, but the name does.

Line 12 actually creates the job with my default parameters and then line 15 sets a Title. This is where you would introduce a schedule if you wanted the job to run more than once, but if not just call Update() to save your job. I want my job to run immediately, so in line 17 I call the RunNow() method to do exactly that.

That’s it! You now have a shell for setting up and installing a custom job – specifically one that runs on every server in the farm. My next post will cover what I’m actually doing in the Execution to ensure the DOCICON.xml file is updated appropriately.

Quick Note about testing: In many cases you will need to either restart the Timer Job Service on each server or change your Assembly Version number to get the timer job to pick up any code changes. This doesn’t always happen, but it happens enough to be annoying.