I've moved my blog to!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Thursday, August 7, 2008

Assigning Rules to Audiences via STSADM

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:
   1: case "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
   2: using System;
   3: using System.Collections;
   4: using System.Collections.Specialized;
   5: using System.IO;
   6: using System.Text;
   7: using System.Xml;
   8: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   9: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  10: using Microsoft.Office.Server;
  11: using Microsoft.Office.Server.Audience;
  12: using Microsoft.Office.Server.Search.Administration;
  13: using Microsoft.SharePoint;
  15: namespace Lapointe.SharePoint.STSADM.Commands.Audiences
  16: {
  17:     public class AddAudienceRule : SPOperation
  18:     {
  19:         public enum AppendOp
  20:         {
  21:             AND, OR
  22:         }
  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));
  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));
  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:         }
  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:         }
  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;
  81:             string rules;
  82:             if (Params["rules"].UserTypedIn)
  83:                 rules = Params["rules"].Value;
  84:             else
  85:                 rules = File.ReadAllText(Params["rulesfile"].Value);
  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));
  95:             return OUTPUT_SUCCESS;
  96:         }
  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);
 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.");
 109:             base.Validate(keyValues);
 110:         }
 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);
 130:             AudienceManager manager = new AudienceManager(context);
 132:             if (!manager.Audiences.AudienceExist(audienceName))
 133:             {
 134:                 throw new SPException("Audience name does not exist");
 135:             }
 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);
 158:             ArrayList audienceRules = audience.AudienceRules;
 159:             bool ruleListNotEmpty = false;
 161:             if (audienceRules == null || clearExistingRules)
 162:                 audienceRules = new ArrayList();
 163:             else
 164:                 ruleListNotEmpty = true;
 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:                 }
 175:                 audienceRules.Add(new AudienceRuleComponent(null, appendOp.ToString(), null));
 176:             }
 178:             if (rulesDoc.SelectNodes("//rule") == null || rulesDoc.SelectNodes("//rule").Count == 0)
 179:                 throw new ArgumentException("No rules were supplied.");
 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;
 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")));
 227:                 if (fieldIsRequired && string.IsNullOrEmpty(field))
 228:                     throw new ArgumentNullException(string.Format("Rule field attribute is missing or invalid: {0}", rule.GetAttribute("field")));
 230:                 AudienceRuleComponent r0 = new AudienceRuleComponent(field, op, val);
 231:                 audienceRules.Add(r0);
 232:             }
 234:             audience.AudienceRules = audienceRules;
 235:             audience.Commit();
 236:             if (compile)
 237:                 CompileAudience(context, audience.AudienceName);
 238:         }
 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);
 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;
 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>

        -name <audience name>
        -rules <rules xml> | -rulesfile <xml file containing the rules>
        [-ssp <SSP name>]
        [-clear (clear existing rules)]
        [-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 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

compile co No If provided then the audience will be compiled after adding the rules. -compile

groupexisting group No If provided then any existing rules will be grouped within parentheses. -groupexisting

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

1 comment:

Anonymous said...

There is a typo in the example (gl-addaudiencerule(s)