Tuesday, April 26, 2011

SharePoint custom job: configuration file

Do you need to build a custom job with configuration settings?
Here is a comprehensive article - Creating Custom Timer Jobs in Windows SharePoint Services 3.0
I think there is no big secret how to build a custom SharePoint job - Andrew Connell "Creating Custom SharePoint Timer Jobs"

My main focus on this post is How to implement a configuration file for a custom SharePoint job

There is "External Files" mentioned in the Configuration Options of the article . This is the exactly approach that I m going to demonstrate.

My goal is to have a config file for a custom job. In the config file I want to have the following options to set:
=1=.  The schedule based on the environment. If it's QC I want to setup the job execution more frequently than it's on Prod.
=2=. . The list where the job should run based on the  filter.

Here is the config file that I am using for one of my jobs:

<ListJobConfig JobName="Case Closing Job"> 
  <Lists>
    <List RootFolder="Lists/Discussion">
      <CamlFilter>
        <![CDATA[
         <Where>
         <Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Pending</Value></Contains><Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Submitted</Value></Contains><Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Approved</Value></Contains>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Held</Value></Contains>
         </Or></Or></Or>
         </Where>
        ]]>
      </CamlFilter>
    </List>
    </Lists>
  <Schedules>
    <Schedule Env="DEV">
      <SPMinuteSchedule>
        <BeginSecond>0</BeginSecond>
        <EndSecond>59</EndSecond>
        <Interval>5</Interval>
      </SPMinuteSchedule>
    </Schedule> 
    <Schedule Env="PROD">
      <SPWeeklySchedule>
        <BeginDayOfWeek>Sunday</BeginDayOfWeek>          
        <BeginHour>2</BeginHour>       
        <BeginMinute>10</BeginMinute>
        <BeginSecond>0</BeginSecond>
        <EndHour>2</EndHour>
        <EndMinute>20</EndMinute>       
        <EndSecond>0</EndSecond>
        <EndDayOfWeek>Sunday</EndDayOfWeek>
      </SPWeeklySchedule>
    </Schedule>
  </Schedules>
</ListJobConfig>

I have placed this config file in the job feature, every time the activation of the feature takes place, the activator reads the configuration to adjust the job accordingly.

Here is the activator code:
  public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
           
            if (!featureInitilizing(properties))
            {  
                throw new SPException("The feature could not be activated.");
            }        

           
            var web = properties.Feature.Parent as SPWeb;

            isProd = ListJobConfigHelper.isProd(web);

            SPWebApplication myApplication = web.Site.WebApplication;
            SPJobDefinitionCollection myJobsColl = myApplication.JobDefinitions;

          
            // Delete job if already exists
            deleteJob(web, getJobName());
           
            // Activate job
            var listJob = readJobConfiguration(web.Site.WebApplication);

            listJob.Properties.Add(AbstractListJob.SPSITEURL_KEY, web.Url);
            listJob.Update();

            myJobsColl.Add(listJob);
            myApplication.Update();

            EventLog.WriteInfo(string.Format("Job {0} Installed on {1} with settings {2}", listJob.Title, web.Url,ListJobConfigHelper.GetEnviromentName(isProd)));
        }
! I am not going to post all code that I have written to make it work, because it's going to be boring!)
But if you need to get the source code - you can find me in twitter  and ask me to send the code to you.

But I will demonstrate the class that handles the config:
 protected AbstractListJob readJobConfiguration(SPWebApplication webApp)
        {
           
            ConfigSchedule cnfSch = lstJobCong.getConfigSchedule();

          
            string jobName = getJobName();
            AbstractListJob job;
           
         
            if (webApp != null)
            {
                job = (AbstractListJob)Activator.CreateInstance(t, new object[] { jobName, webApp, null, SPJobLockType.None });
            }
            else
            {
                job = (AbstractListJob)Activator.CreateInstance(t);
                   
            }
            job.Title = jobName;
            job.Properties.Add(AbstractListJob.JOB_NAME_KEY, jobName);
            job.Properties.Add(AbstractListJob.CONFIG_PATH_KEY, filePath); - this is important for a calm filtering configuration


            job.Schedule = (SPSchedule)cnfSch.Schedules[ListJobConfigHelper.GetEnviromentConfigValue(isProd)]; - this configuration class that returns schedules depending on the environment
           
           
            return job;
        }

=1=. The schedule based on the environment
The implementation is hybrid. The schedule is stored in the xml file, the indicator of environment is in the site itself where the feature activation happens.
 I have a config list on my site where I store different key\value pairs. One of them is Env with appropriate hardcoded value:

   isProd = ListJobConfigHelper.isProd(web);
    public static bool isProd(SPWeb web)
        {
            var env = SharePointHelper.GetConfigValue(CommonConstants.ConfigKeyEnv, web);
            if (env != null && !env.ToLower().Contains(CommonConstants.ConfigKeyEnvDEV.ToLower()))
            {
                return env.ToLower().Contains(CommonConstants.ConfigKeyEnvPROD.ToLower());
            }
            return false;
        }

        public static string GetConfigValue(string key, SPWeb web)
        {
            string result=null;
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite elevatedSite = new SPSite(web.Site.ID))
                {
                    using (SPWeb elevatedWeb = elevatedSite.OpenWeb(web.ID))
                    {
                        SPList configList = GetListByUrlPattern(elevatedWeb, CommonConstants.ConfigListUrl);
                        SPQuery caml = new SPQuery();
                        string queryText = string.Format(
                                  @"<Where>
                          <Eq>
                            <FieldRef Name='Title' />
                            <Value Type='Text'>{0}</Value>                          
                          </Eq>
                       </Where>", key);
                        caml.Query = queryText;
                        SPListItemCollection results = configList.GetItems(caml);
                        if (results.Count > 0)
                            result = results[0]["Value"].ToString();

                      
                    }
                }
            })
            ;
            return result;
        }
Once the code has found the type of env, it retracts the appropriate schedule for the job:
  job.Schedule = (SPSchedule)cnfSch.Schedules[ListJobConfigHelper.GetEnviromentConfigValue(isProd)]

Let's look closer at the Config classes:
 filePath = properties.Feature.Definition.RootDirectory + CONFIG_FILE_PATH;
 lstJobCong = ListJobConfigHelper.loadConfiguration(filePath);
 public static ListJobConfig loadConfiguration(string filePath)
        {
          
            var sz = new XmlSerializer(typeof(ListJobConfig));
            if (File.Exists(filePath))
            {
                using (var sr = File.OpenRead(filePath))
                {
                    try
                    {
                        return sz.Deserialize(sr) as ListJobConfig;
                    }
                    catch (Exception ex)
                    {

                        EventLog.WriteError(string.Format("Can't load the config file{0}", filePath));
                        EventLog.WriteError(ex);
                        return null;
                    }
                }
            }

            return null;

        }
 
 The class ListJobConfig is generated by xsd.exe based on the following xsd:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="ListJobConfig" >
    <xs:complexType>     
      <xs:all>
        <xs:element name="Lists">
          <xs:complexType>           
            <xs:sequence>
              <xs:element name="List" type="List" minOccurs="1" maxOccurs="unbounded"/>
            </xs:sequence>         
          </xs:complexType>
        </xs:element>
        <xs:element name="Schedules">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Schedule" type="Schedule" minOccurs="1" maxOccurs="unbounded"/>
            </xs:sequence>         
          </xs:complexType>
        </xs:element>
        <xs:element name="CustomProperties" minOccurs="0">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="CustomProperty" type="CustomProperty" maxOccurs="unbounded"/>
            </xs:sequence>           
          </xs:complexType>
        </xs:element>
      </xs:all>
      <xs:attribute name="JobName" type="xs:string" use="required" />
    </xs:complexType>   
  </xs:element>

  <xs:complexType name="CustomProperty">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="Name" type="xs:string"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>


  <xs:complexType name="List">  
      <xs:sequence>
        <xs:element name="CamlFilter" type="xs:string" minOccurs="0"/>
      </xs:sequence>
    <xs:attribute name="RootFolder" type="xs:string" use="required" />
  </xs:complexType>
 
 
  <xs:complexType name="Schedule">
    <xs:choice>
      <xs:element name="SPMinuteSchedule" type="SPMinuteSchedule"/>
      <xs:element name="SPWeeklySchedule" type="SPWeeklySchedule"/>
      <xs:element name="SPDailySchedule" type="SPDailySchedule"/>
    </xs:choice>
   
    <xs:attribute name="Env" use="required">
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:pattern value="DEV|PROD"/>
        </xs:restriction>
      </xs:simpleType>
    </xs:attribute>  
  </xs:complexType>
 
  <xs:complexType name="SPMinuteSchedule">
    <xs:all>
      <xs:element name="BeginSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="EndSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>     
      <xs:element name="Interval">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>      
    </xs:all>
  </xs:complexType>

  <xs:complexType name="SPDailySchedule">
    <xs:all>
      <xs:element name="BeginHour">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="23"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="BeginMinute">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="BeginSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
    
      <xs:element name="EndHour">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="23"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="EndMinute">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="EndSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
    </xs:all>

  </xs:complexType>
 
  <xs:complexType name="SPWeeklySchedule">
    <xs:all>

      <xs:element name="BeginDayOfWeek ">
        <xs:simpleType>
          <xs:restriction base="xs:string">
            <xs:pattern value="Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
     
      <xs:element name="BeginHour">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="23"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="BeginMinute">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="BeginSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>

      <xs:element name="EndDayOfWeek">
        <xs:simpleType>
          <xs:restriction base="xs:string">
            <xs:pattern value="Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
     
      <xs:element name="EndHour">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="23"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="EndMinute">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
      <xs:element name="EndSecond">
        <xs:simpleType>
          <xs:restriction base="xs:integer">
            <xs:minInclusive value="0"/>
            <xs:maxExclusive value="60"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:element>
    </xs:all>
   
  </xs:complexType>
</xs:schema>
Are you still here?)
If yes - you are really interested in the topic!

So, let me explain the job scheduling:
ConfigSchedule cnfSch = lstJobCong.getConfigSchedule(); - the method converts xml data into SPSchedule
public static ConfigSchedule getConfigSchedule(this ListJobConfig lstJobCnfg)
        {
            return new ConfigSchedule(lstJobCnfg.Schedules);
        }

    public ConfigSchedule(Schedule[] schedules)
        {
            createSchedForEnv(schedules);
        }
        private void createSchedForEnv(Schedule[] schedules)
        {

            foreach (Schedule schedule in schedules)
            {
              
                encSched.Add(schedule.Env,getSch(schedule.Item));
            }
        }
      
        private object getSch(object item)
        {
            Type t;
            object schedule;

            string scheduleType = item.GetType().Name;
            switch (scheduleType)
            {
                case "SPMinuteSchedule":
                    t = typeof(Microsoft.SharePoint.SPMinuteSchedule);
                    schedule = new Microsoft.SharePoint.SPMinuteSchedule
                    {
                        BeginSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPMinuteSchedule)item).BeginSecond),
                        EndSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPMinuteSchedule)item).EndSecond),
                        Interval = Convert.ToInt32(((SharePoint.Common.Jobs.SPMinuteSchedule)item).Interval)
                    };

                    break;
                case "SPWeeklySchedule":
                    t = typeof(Microsoft.SharePoint.SPWeeklySchedule);
                    schedule = new Microsoft.SharePoint.SPWeeklySchedule()
                    {
                        BeginDayOfWeek = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), ((SharePoint.Common.Jobs.SPWeeklySchedule)item).BeginDayOfWeek),
                        BeginHour = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).BeginHour),
                        BeginMinute = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).BeginMinute),
                        BeginSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).BeginSecond),
                        EndDayOfWeek = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), ((SharePoint.Common.Jobs.SPWeeklySchedule)item).EndDayOfWeek),
                        EndHour = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).EndHour),
                        EndMinute = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).EndMinute),
                        EndSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPWeeklySchedule)item).EndSecond)

                    };
                    break;
                case "SPDailySchedule":
                    t = typeof (Microsoft.SharePoint.SPDailySchedule);
                    schedule = new Microsoft.SharePoint.SPDailySchedule()
                                   {
                                       BeginHour = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).BeginHour),
                                       BeginMinute = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).BeginMinute),
                                       BeginSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).BeginSecond),
                                       EndHour = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).EndHour),
                                       EndMinute = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).EndMinute),
                                       EndSecond = Convert.ToInt32(((SharePoint.Common.Jobs.SPDailySchedule)item).EndSecond)
                
                                   };

                    break;
                default:
                    t = null;
                    schedule = null;
                    break;

            }

            if (t == null)
            {
                return null;
            }

            return schedule;
        }

And finally assignment the schedule to the job!:
  job.Schedule = (SPSchedule)cnfSch .Schedules[ListJobConfigHelper.GetEnviromentConfigValue(isProd)];

=2=.  The list where the job should run based on the  filter.
 I guess you are really tired by now. I m going to try to keep it simple.  
 Usually, the jobs do something with items from list; the list is filtered to get the appropriate bunch of items. I see this scenario too common to leave without automation. I would prefer to have configuration settings where I can put the list and filter to the list to fetch the data that job should work with.
 My configuration job xsd waits following xml:
<Lists>
    <List RootFolder="Lists/Discussion">
      <CamlFilter>
        <![CDATA[
         <Where>
         <Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Pending</Value></Contains><Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Submitted</Value></Contains><Or>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Approved</Value></Contains>
         <Contains><FieldRef Name='Status' /><Value Type='Text'>Held</Value></Contains>
         </Or></Or></Or>
         </Where>
        ]]>
      </CamlFilter>
    </List>
    </Lists>
I love to use CamlFilter because it keeps my configuration kind of  standartized. Here is the way the xml is handled in the job inside:
protected virtual void processJob(SPWeb web)
        {
          

            foreach (var lst in lstJobCong.Lists)
            {
                SPList list = SharePointHelper.GetListByUrlPattern(web, lst.RootFolder);

                SPQuery caml = ListJobConfigHelper.GetQuery(lst);

                List<int> processedItems = new List<int>();
               

                    foreach (SPListItem item in list.GetItems(caml))
                    {
                        try
                        {
                            if (DoBusinessJob(item, isProd))
                            {
                                processedItems.Add(item.ID);
                            }
                        }
                        catch (Exception ex)
                        {
                            EventLog.WriteWarning(string.Format("An error occured in processing of item {0}. See error log below."
                                , item["Title"] != null ? item["Title"].ToString() : NO_TITLE));
                            EventLog.WriteError(ex);
                            EventLog.WriteError(String.Format("The job {0} is aborted on site {1}", Properties[JOB_NAME_KEY], Url));
                            return;
                        }
                    }
                   
                    EventLog.WriteInfo(string.Format("{0}: The total items processed is: {1} The items are:{2}", lstJobCong.JobName, processedItems.Count, processedItems.ToString(",")));
                }
        }

 That is it!  I want to add - in my real world app - I have created an abstract class for a custom job and custom job activator. Every time I need to develop a new one - I use inheritance to reduce the code that I write and the only thing that I need to do is to implement the virtual method DoBusinessJob.

Additional reading:
I like the alternative way of looking at the custom job development from Alexander Bruett