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:
new AudienceRuleComponent(null, "(", null);
new AudienceRuleComponent("Department", "=", "IT");
new AudienceRuleComponent(null, "OR", null);
new AudienceRuleComponent("Everyone", "Reports Under", "domain\glapointe");
new AudienceRuleComponent(null, ")", null);
new AudienceRuleComponent(null, "AND", null);
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 whenCommit
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 Name | Availability | Build Date |
---|---|---|
gl-addaudiencerule | MOSS 2007 | Released 8/6/2008 |
Parameter Name | Short Form | Required | Description | Example Usage |
---|---|---|---|---|
name | n | Yes | This is the name of the audience for which to apply the rules. | -name "IT Department" , -n "IT Department" |
ssp | No | The name of the SSP that the audience is associated with. If not specified then the default SSP is used. | -ssp SSP1 | |
rules | r | Yes – unless rulesfile provided | The 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='(‘ /><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>" |
rulesfile | rf | Yes – unless rules provided | Specifies 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 |
clear | cl | No | If provided then any existing rules will be removed from the audience. | -clear , -cl |
compile | co | No | If provided then the audience will be compiled after adding the rules. | -compile , -co |
groupexisting | group | No | If provided then any existing rules will be grouped within parentheses. | -groupexisting , -group |
appendop | op | No | Specifies 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