If you read my last post, Creating Audiences via STSADM, then you know that I’ve been working on a project which requires me to be able to script out the creation of audiences via STSADM. My last post covered the creation of the audience itself, but an audience with no rules isn’t all that useful, so for this post I’ll be covering my next custom command, gl-addaudiencerule, which enables you to add complex rules to an audience.

I’ll reiterate a couple of things regarding creating rules from my last post. First off, when you create rules via the browser you are limited to just simple rules – in other words, you may have multiple rules but the boolean logic is limited to all rules matching or any rules matching – there is no combination or complex boolean logic with grouping. This is not the case if you create the rules programmatically – by programmatically creating the rules we can use grouping (up to 3 levels deep) and any combination of boolean logic. The catch is that as soon as you add any complex rules to an audience you will now no longer be able to manage that audience via the browser – you’ll still be able to compile the audience and view memberships but you won’t be able to manage or even view any rules associated with the audience and you won’t be able to delete the audience. I created two more commands that allow you to see the rules and delete the audience via STSADM but I’ll talk about them in follow-up posts.

Microsoft took an interesting approach to storing the rules – they basically use an ArrayList of objects of type AudienceRuleComponent. Each object represents a part of the rule, including the the parentheses and logic operators (AND, OR). So a rule like the following would consist of 7 objects:

(Department == "IT" OR Reports Under == "domain\glapointe") AND IsContractor == false

The above would be broken down into objects in the following fashion:

  1. new AudienceRuleComponent(null, "(", null);
  2. new AudienceRuleComponent("Department", "=", "IT");
  3. new AudienceRuleComponent(null, "OR", null);
  4. new AudienceRuleComponent("Everyone", "Reports Under", "domain\glapointe");
  5. new AudienceRuleComponent(null, ")", null);
  6. new AudienceRuleComponent(null, "AND", null);
  7. new AudienceRuleComponent("IsContractor", "=", "false");

The objects created above would be added to the rules collection array list in the order listed. One thing you may have noticed above is that the field for the “reports under” operation is “Everyone” – the field for the “member of” operation is actually “DL”. It’s important to note that if you change the rules you must reassign the AudienceRules property rather than manipulate the items via the property:

  • audience.AudienceRules.Add(new AudienceRuleComponent(null, "(", null)); This will not work as the property will not be marked as dirty and will therefore not be saved when Commit is called.
  • ArrayList rules = audience.AudienceRules;
    rules..Add(new AudienceRuleComponent(null, "(", null));
    audience.AudienceRules = rules; This assignment marks the audience rules as dirty and will thus be saved.

The way I decided to handle the creation of these rules was to allow a simple XML structure to be passed into the command either directly via a parameter or indirectly by passing in a file containing the rules. The structure of the XML is similar to the structure of the above code – you create one or more <rule /> elements which are wrapped in a <rules /> element. The <rule /> element contains one required attribute, “op”, and two optional (depending on the operation) attributes, “field” and “value”. Grouping operations do not require the field and value attributes and member of and reports under operations do not require the field attribute. Here’s an example of the above:

1 <rules>
2    <rule op="(" />
3    <rule field="Department" op="=" value="IT" />
4    <rule op="or" />
5    <rule op="reports under" value="domain\glapointe" />
6    <rule op=")" />
7    <rule op="and" />
8    <rule field="IsContractor" op="=" value="false" />
9</rules>

You could easily pass this same XML structure in as a parameter by removing the line breaks and replacing the quotes with tick marks. By using this simple XML approach I was able to write the code very quickly. The only part that tripped me up was the “member of” operation. The value of this operation must be a fully distinguished AD name and not the login name as depicted – but I wanted to be able to use just the login name. What I found was that the API provides a handy little method for converting the login name (you can also pass in an email address) to a distinguished name, as seen in this snippet:

1case "member of":
2    field = "DL";
3    val = rule.GetAttribute("value");
4    ArrayList path = AudienceManager.GetADsPath(val);
5    if (path.Count == 0)
6        throw new ArgumentException(string.Format("Security group or distribution list was not found: {0}", val));
7    val = ((string)path[0]).Replace("LDAP://", "");
8    break;

The complete code for the command can be seen below:

  1#if MOSS
  2using System;
  3using System.Collections;
  4using System.Collections.Specialized;
  5using System.IO;
  6using System.Text;
  7using System.Xml;
  8using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  9using Lapointe.SharePoint.STSADM.Commands.SPValidators;
 10using Microsoft.Office.Server;
 11using Microsoft.Office.Server.Audience;
 12using Microsoft.Office.Server.Search.Administration;
 13using Microsoft.SharePoint;
 14 
 15namespace Lapointe.SharePoint.STSADM.Commands.Audiences
 16{
 17    public class AddAudienceRule : SPOperation
 18    {
 19        public enum AppendOp
 20        {
 21            AND, OR
 22        }
 23 
 24        /// <summary>
 25        /// Initializes a new instance of the <see cref="AddAudienceRule"/> class.
 26        /// </summary>
 27        public AddAudienceRule()
 28        {
 29            SPEnumValidator appendOpValidator = new SPEnumValidator(typeof(AppendOp));
 30 
 31            SPParamCollection parameters = new SPParamCollection();
 32            parameters.Add(new SPParam("name", "n", true, null, new SPNonEmptyValidator()));
 33            parameters.Add(new SPParam("ssp", "ssp", false, null, new SPNonEmptyValidator()));
 34            parameters.Add(new SPParam("rules", "r", false, null, new SPNonEmptyValidator()));
 35            parameters.Add(new SPParam("rulesfile", "rf", false, null, new SPFileExistsValidator()));
 36            parameters.Add(new SPParam("clear", "cl"));
 37            parameters.Add(new SPParam("compile", "co"));
 38            parameters.Add(new SPParam("groupexisting", "group"));
 39            parameters.Add(new SPParam("appendop", "op", false, "and", appendOpValidator));
 40 
 41            StringBuilder sb = new StringBuilder();
 42            sb.Append("\r\n\r\nAdds simple or complex rules to an existing audience.  The rules XML should be in the following format: ");
 43            sb.Append("<rules><rule op='' field='' value='' /></rules>\r\n");
 44            sb.Append("Values for the \"op\" attribute can be any of \"=,>,>=,<,<=,<>,Contains,Not contains,Reports Under,Member Of,AND,OR,(,)\"\r\n");
 45            sb.Append("The \"field\" attribute is not required if \"op\" is any of \"Reports Under,Member Of,AND,OR,(,)\"\r\n");
 46            sb.Append("The \"value\" attribute is not required if \"op\" is any of \"AND,OR,(,)\"\r\n");
 47            sb.Append("Note that if your rules contain any grouping or mixed logic then you will not be able to manage the rule via the browser.\r\n");
 48            sb.Append("Example: <rules><rule op='Member of' value='sales department' /><rule op='AND' /><rule op='Contains' field='Department' value='Sales' /></rules>");
 49            sb.Append("\r\n\r\nParameters:");
 50            sb.Append("\r\n\t-name <audience name>");
 51            sb.Append("\r\n\t-rules <rules xml> | -rulesfile <xml file containing the rules>");
 52            sb.Append("\r\n\t[-ssp <SSP name>]");
 53            sb.Append("\r\n\t[-clear (clear existing rules)]");
 54            sb.Append("\r\n\t[-compile]");
 55            sb.Append("\r\n\t[-groupexisting (wraps any existing rules in parantheses)]");
 56            sb.Append("\r\n\t[-appendop <and (default) | or> (operator used to append to existing rules)]");
 57            Init(parameters, sb.ToString());
 58        }
 59 
 60        /// <summary>
 61        /// Gets the help message.
 62        /// </summary>
 63        /// <param name="command">The command.</param>
 64        /// <returns></returns>
 65        public override string GetHelpMessage(string command)
 66        {
 67            return HelpMessage;
 68        }
 69 
 70        /// <summary>
 71        /// Executes the specified command.
 72        /// </summary>
 73        /// <param name="command">The command.</param>
 74        /// <param name="keyValues">The key values.</param>
 75        /// <param name="output">The output.</param>
 76        /// <returns></returns>
 77        public override int Execute(string command, StringDictionary keyValues, out string output)
 78        {
 79            output = string.Empty;
 80 
 81            string rules;
 82            if (Params["rules"].UserTypedIn)
 83                rules = Params["rules"].Value;
 84            else
 85                rules = File.ReadAllText(Params["rulesfile"].Value);
 86 
 87            AddRules(Params["ssp"].Value,
 88                     Params["name"].Value,
 89                     rules,
 90                     Params["clear"].UserTypedIn,
 91                     Params["compile"].UserTypedIn,
 92                     Params["groupexisting"].UserTypedIn,
 93                     (AppendOp)Enum.Parse(typeof(AppendOp), Params["appendop"].Value, true));
 94 
 95            return OUTPUT_SUCCESS;
 96        }
 97 
 98        /// <summary>
 99        /// Validates the specified key values.
100        /// </summary>
101        /// <param name="keyValues">The key values.</param>
102        public override void Validate(StringDictionary keyValues)
103        {
104            SPBinaryParameterValidator.Validate("rules", Params["rules"].Value, "rulesfile", Params["rulesfile"].Value);
105            
106            if (Params["clear"].UserTypedIn && (Params["appendop"].UserTypedIn || Params["groupexisting"].UserTypedIn))
107                throw new SPSyntaxException("The -clear parameter cannot be used with the -appendop or -groupexisting parameters.");
108 
109            base.Validate(keyValues);
110        }
111 
112        /// <summary>
113        /// Adds the rules.
114        /// </summary>
115        /// <param name="sspName">Name of the SSP.</param>
116        /// <param name="audienceName">Name of the audience.</param>
117        /// <param name="rules">The rules.</param>
118        /// <param name="clearExistingRules">if set to <c>true</c> [clear existing rules].</param>
119        /// <param name="compile">if set to <c>true</c> [compile].</param>
120        /// <param name="groupExisting">if set to <c>true</c> [group existing].</param>
121        /// <param name="appendOp">The append op.</param>
122        public static void AddRules(string sspName, string audienceName, string rules, bool clearExistingRules, bool compile, bool groupExisting, AppendOp appendOp)
123        {
124            ServerContext context;
125            if (string.IsNullOrEmpty(sspName))
126                context = ServerContext.Default;
127            else
128                context = ServerContext.GetContext(sspName);
129 
130            AudienceManager manager = new AudienceManager(context);
131 
132            if (!manager.Audiences.AudienceExist(audienceName))
133            {
134                throw new SPException("Audience name does not exist");
135            }
136 
137            Audience audience = manager.Audiences[audienceName];
138            /*
139            Operator        Need left and right operands (not a group operator) 
140            =               Yes 
141            >               Yes 
142            >=              Yes 
143            <               Yes 
144            <=              Yes 
145            Contains        Yes 
146            Reports Under   Yes (Left operand must be 'Everyone') 
147            <>              Yes 
148            Not contains    Yes 
149            AND             No 
150            OR              No 
151            (               No 
152            )               No 
153            Member Of       Yes (Left operand must be 'DL') 
154            */
155            XmlDocument rulesDoc = new XmlDocument();
156            rulesDoc.LoadXml(rules);
157 
158            ArrayList audienceRules = audience.AudienceRules;
159            bool ruleListNotEmpty = false;
160 
161            if (audienceRules == null || clearExistingRules)
162                audienceRules = new ArrayList();
163            else
164                ruleListNotEmpty = true;
165 
166            //if the rule is not emply, start with a group operator 'AND' to append
167            if (ruleListNotEmpty)
168            {
169                if (groupExisting)
170                {
171                    audienceRules.Insert(0, new AudienceRuleComponent(null, "(", null));
172                    audienceRules.Add(new AudienceRuleComponent(null, ")", null));
173                }
174 
175                audienceRules.Add(new AudienceRuleComponent(null, appendOp.ToString(), null));
176            }
177 
178            if (rulesDoc.SelectNodes("//rule") == null || rulesDoc.SelectNodes("//rule").Count == 0)
179                throw new ArgumentException("No rules were supplied.");
180 
181            foreach (XmlElement rule in rulesDoc.SelectNodes("//rule"))
182            {
183                string op = rule.GetAttribute("op").ToLowerInvariant();
184                string field = null;
185                string val = null;
186                bool valIsRequired = true;
187                bool fieldIsRequired = false;
188 
189                switch (op)
190                {
191                    case "=":
192                    case ">":
193                    case ">=":
194                    case "<":
195                    case "<=":
196                    case "contains":
197                    case "<>":
198                    case "not contains":
199                        field = rule.GetAttribute("field");
200                        val = rule.GetAttribute("value");
201                        fieldIsRequired = true;
202                        break;
203                    case "reports under":
204                        field = "Everyone";
205                        val = rule.GetAttribute("value");
206                        break;
207                    case "member of":
208                        field = "DL";
209                        val = rule.GetAttribute("value");
210                        ArrayList path = AudienceManager.GetADsPath(val);
211                        if (path.Count == 0)
212                            throw new ArgumentException(string.Format("Security group or distribution list was not found: {0}", val));
213                        val = ((string)path[0]).Replace("LDAP://", "");
214                        break;
215                    case "and":
216                    case "or":
217                    case "(":
218                    case ")":
219                        valIsRequired = false;
220                        break;
221                    default:
222                        throw new ArgumentException(string.Format("Rule operator is invalid: {0}", rule.GetAttribute("op")));
223                }
224                if (valIsRequired && string.IsNullOrEmpty(val))
225                    throw new ArgumentNullException(string.Format("Rule value attribute is missing or invalid: {0}", rule.GetAttribute("value")));
226 
227                if (fieldIsRequired && string.IsNullOrEmpty(field))
228                    throw new ArgumentNullException(string.Format("Rule field attribute is missing or invalid: {0}", rule.GetAttribute("field")));
229                
230                AudienceRuleComponent r0 = new AudienceRuleComponent(field, op, val);
231                audienceRules.Add(r0);
232            }
233 
234            audience.AudienceRules = audienceRules;
235            audience.Commit();
236            if (compile)
237                CompileAudience(context, audience.AudienceName);
238        }
239 
240        /// <summary>
241        /// Compiles the audience.
242        /// </summary>
243        /// <param name="context">The context.</param>
244        /// <param name="audienceName">Name of the audience.</param>
245        public static void CompileAudience(ServerContext context, string audienceName)
246        {
247            SearchContext searchContext = SearchContext.GetContext(context);
248 
249            string[] args = new string[4];
250            args[0] = searchContext.Name;
251            args[1] = "1"; //"1" = start job, "0" = stop job 
252            args[2] = "1"; //"1" = full compilation, "0" = incremental compilation (optional, default = 0) 
253            args[3] = audienceName;
254 
255            AudienceJob.RunAudienceJob(args);
256        }
257    }
258}
259#endif

The help for the command is shown below:

C:\>stsadm -help gl-addaudiencerule

stsadm -o gl-addaudiencerule


Adds simple or complex rules to an existing audience.  The rules XML should be in the following format: <rules><rule op='' field='' value='' /></rules>
Values for the "op" attribute can be any of "=,>,>=,<,<=,<>,Contains,Not contains,Reports Under,Member Of,AND,OR,(,)"
The "field" attribute is not required if "op" is any of "Reports Under,Member Of,AND,OR,(,)"
The "value" attribute is not required if "op" is any of "AND,OR,(,)"
Note that if your rules contain any grouping or mixed logic then you will not be able to manage the rule via the browser.
Example: <rules><rule op='Member of' value='sales department' /><rule op='AND' /><rule op='Contains' field='Department'value='Sales' /></rules>

Parameters:
        -name <audience name>
        -rules <rules xml> | -rulesfile <xml file containing the rules>
        [-ssp <SSP name>]
        [-clear (clear existing rules)]
        [-compile]
        [-groupexisting (wraps any existing rules in parantheses)]
        [-appendop <and (default) | or> (operator used to append to existing rules)]

The following table summarizes the command and its various parameters:

Command NameAvailabilityBuild Date
gl-addaudienceruleMOSS 2007Released 8/6/2008
Parameter NameShort FormRequiredDescriptionExample Usage
namenYesThis is the name of the audience for which to apply the rules.-name "IT Department", -n "IT Department"
sspNoThe name of the SSP that the audience is associated with. If not specified then the default SSP is used.-ssp SSP1
rulesrYes – unless rulesfile providedThe XML rules that are to be created. Use tick marks instead of quotes. Using this parameter, as opposed to the rulesfile parameter, is convenient when using a batch script in which you’d like to pass variables into the XML.-rules "<rules><rule op='(&#8216; /><rule field='Department' op='=' value='IT' /><rule op='or' /><rule op='reports under' value='domain\glapointe' /><rule op=')' /><rule op='and' /><rule field='IsContractor' op='=' value='false' /></rules>"
rulesfilerfYes – unless rules providedSpecifies the path to an XML file containing the rules to be created. The file extension does not matter. Using this parameter, as opposed to the rules parameter, is convenient when you’d like to save your rules for later reference or recreation in other environments as well as easy modification.-rulesfile c:\Audiences\ITDepartment.rules
clearclNoIf provided then any existing rules will be removed from the audience.-clear, -cl
compilecoNoIf provided then the audience will be compiled after adding the rules.-compile, -co
groupexistinggroupNoIf provided then any existing rules will be grouped within parentheses.-groupexisting, -group
appendopopNoSpecifies how the passed in rules will be appended to any existing rules. Valid values are “and” or “or”. The default, if omitted, is “and”.-appendop or, -op or

The following is an example of how to add rules to an audience named “IT Department”:

stsadm -o gl-addaudiencerule -name "IT Department" -rules "<rules><rule op='(‘ /><rule field='Department' op='=' value='IT' /><rule op='or' /><rule op='reports under' value='domain\glapointe' /><rule op=')' /><rule op='and' /><rule field='IsContractor' op='=' value='false' /></rules>" -clear -compile