Importing Audiences using STSADM
I recently posted about exporting audiences using my gl-exportaudiences STSADM command. Of course an export wouldn’t be of much use if you didn’t also have an import so I give you gl-importaudiences.
Developing this was really easy as I already had code that created an audience and its associated rules. All I had to do was read in the source XML file, do a little refactoring of the audience creation code and then call the rules creation code. One cool thing I added was the ability to output a mapping file which provides the search and replace XML necessary to use my gl-replacefieldvalues command so that you can replace the old GUIDs used in audience targeting with the new GUID of the new audience. The code can be seen below:
1: #if MOSS
2: using System;
3: using System.Collections;
4: using System.Collections.Specialized;
5: using System.Collections.Generic;
6: using System.IO;
7: using System.Text;
8: using System.Xml;
9: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
10: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
11: using Microsoft.Office.Server;
12: using Microsoft.Office.Server.Audience;
13:
14: namespace Lapointe.SharePoint.STSADM.Commands.Audiences
15: {
16: public class ImportAudiences : SPOperation
17: {
18: /// <summary>
19: /// Initializes a new instance of the <see cref="ImportAudiences"/> class.
20: /// </summary>
21: public ImportAudiences()
22: {
23: SPParamCollection parameters = new SPParamCollection();
24: parameters.Add(new SPParam("ssp", "ssp", false, null, new SPNonEmptyValidator()));
25: parameters.Add(new SPParam("deleteexisting", "delete"));
26: parameters.Add(new SPParam("inputfile", "input", false, null, new SPFileExistsValidator()));
27: parameters.Add(new SPParam("compile", "c"));
28: parameters.Add(new SPParam("mapfile", "map", false, null, new SPDirectoryExistsAndValidFileNameValidator()));
29:
30: StringBuilder sb = new StringBuilder();
31: sb.Append("\r\n\r\nImports all audiences given the provided input file.\r\n\r\nParameters:");
32: sb.Append("\r\n\t-inputfile <file to input results from>");
33: sb.Append("\r\n\t[-deleteexisting <delete existing audiences>]");
34: sb.Append("\r\n\t[-ssp <SSP name>]");
35: sb.Append("\r\n\t[-compile]");
36: sb.Append("\r\n\t[-mapfile <generate a map file to use for search and replace of Audience IDs>]");
37: Init(parameters, sb.ToString());
38: }
39:
40: /// <summary>
41: /// Gets the help message.
42: /// </summary>
43: /// <param name="command">The command.</param>
44: /// <returns></returns>
45: public override string GetHelpMessage(string command)
46: {
47: return HelpMessage;
48: }
49:
50: /// <summary>
51: /// Executes the specified command.
52: /// </summary>
53: /// <param name="command">The command.</param>
54: /// <param name="keyValues">The key values.</param>
55: /// <param name="output">The output.</param>
56: /// <returns></returns>
57: public override int Execute(string command, StringDictionary keyValues, out string output)
58: {
59: output = string.Empty;
60:
61: string inputFile = Params["inputfile"].Value;
62: bool deleteExisting = Params["deleteexisting"].UserTypedIn;
63: bool compile = Params["compile"].UserTypedIn;
64: string mapFile = default(string);
65: if (Params["mapfile"].UserTypedIn)
66: mapFile = Params["mapfile"].Value;
67:
68: string xml = File.ReadAllText(inputFile);
69:
70: Import(xml, Params["ssp"].Value, deleteExisting, compile, mapFile);
71:
72: return OUTPUT_SUCCESS;
73: }
74:
75: /// <summary>
76: /// Imports the specified XML.
77: /// </summary>
78: /// <param name="xml">The XML.</param>
79: /// <param name="sspName">Name of the SSP.</param>
80: /// <param name="deleteExisting">if set to <c>true</c> [delete existing].</param>
81: /// <param name="compile">if set to <c>true</c> [compile].</param>
82: /// <param name="mapFile">The map file.</param>
83: private void Import(string xml, string sspName, bool deleteExisting, bool compile, string mapFile)
84: {
85: ServerContext context;
86: if (string.IsNullOrEmpty(sspName))
87: context = ServerContext.Default;
88: else
89: context = ServerContext.GetContext(sspName);
90:
91: AudienceManager manager = new AudienceManager(context);
92:
93: XmlDocument audiencesDoc = new XmlDocument();
94: audiencesDoc.LoadXml(xml);
95:
96: XmlNodeList audienceElements = audiencesDoc.SelectNodes("//Audience");
97: if (audienceElements == null)
98: throw new ArgumentException("The input file does not contain any audience elements.");
99:
100: StringBuilder sb = new StringBuilder();
101: XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
102: xmlWriter.Formatting = Formatting.Indented;
103: xmlWriter.WriteStartElement("Replacements");
104:
105: Dictionary<Guid, string> ids = new Dictionary<Guid, string>();
106: if (deleteExisting)
107: {
108: Log("Progrss: Deleting existing audiences.");
109: foreach (Audience au in manager.Audiences)
110: ids.Add(au.AudienceID, au.AudienceName);
111:
112: foreach (Guid id in ids.Keys)
113: {
114: if (id == Guid.Empty)
115: continue;
116:
117: string name = manager.Audiences[id].AudienceName;
118: manager.Audiences.Remove(id);
119: }
120: }
121:
122: foreach (XmlElement audienceElement in audienceElements)
123: {
124: string audienceName = audienceElement.GetAttribute("AudienceName");
125: string audienceDesc = audienceElement.GetAttribute("AudienceDescription");
126:
127: Audience audience;
128: bool updatedAudience = false;
129: if (manager.Audiences.AudienceExist(audienceName))
130: {
131: Log("Progress: Updating audience {0}.", audienceName);
132: audience = manager.Audiences[audienceName];
133: audience.AudienceDescription = audienceDesc ?? "";
134: updatedAudience = true;
135: }
136: else
137: {
138: // IMPORTANT: the create method does not do a null check but the methods that load the resultant collection assume not null.
139: Log("Progress: Creating audience {0}.", audienceName);
140: audience = manager.Audiences.Create(audienceName, audienceDesc ?? "");
141: }
142:
143: audience.GroupOperation = (AudienceGroupOperation)Enum.Parse(typeof (AudienceGroupOperation),
144: audienceElement.GetAttribute("GroupOperation"));
145:
146: audience.OwnerAccountName = audienceElement.GetAttribute("OwnerAccountName");
147:
148: audience.Commit();
149:
150: if (updatedAudience && audience.AudienceID != Guid.Empty)
151: {
152: // We've updated an existing audience.
153: xmlWriter.WriteStartElement("Replacement");
154: xmlWriter.WriteElementString("SearchString", (new Guid(audienceElement.GetAttribute("AudienceID")).ToString().ToUpper()));
155: xmlWriter.WriteElementString("ReplaceString", string.Format("(?i:{0})", audience.AudienceID.ToString().ToUpper()));
156: xmlWriter.WriteEndElement(); // Replacement
157: }
158: else if (!updatedAudience && audience.AudienceID != Guid.Empty && ids.ContainsValue(audience.AudienceName))
159: {
160: // We've added a new audience which we just previously deleted.
161: xmlWriter.WriteStartElement("Replacement");
162: foreach (Guid id in ids.Keys)
163: {
164: if (ids[id] == audience.AudienceName)
165: {
166: xmlWriter.WriteElementString("SearchString", id.ToString().ToUpper());
167: break;
168: }
169: }
170: xmlWriter.WriteElementString("ReplaceString", string.Format("(?i:{0})", audience.AudienceID.ToString().ToUpper()));
171: xmlWriter.WriteEndElement(); // Replacement
172: }
173:
174: XmlElement rulesElement = (XmlElement)audienceElement.SelectSingleNode("rules");
175: if (rulesElement == null || rulesElement.ChildNodes.Count == 0)
176: {
177: audience.AudienceRules = new ArrayList();
178: audience.Commit();
179: continue;
180: }
181:
182:
183: string rules = rulesElement.OuterXml;
184: Log("Progress: Adding rules to audience {0}.", audienceName);
185: AddAudienceRule.AddRules(sspName, audienceName, rules, true, compile, false, AddAudienceRule.AppendOp.AND);
186: }
187:
188: xmlWriter.WriteEndElement(); // Replacements
189:
190: if (!string.IsNullOrEmpty(mapFile))
191: {
192: xmlWriter.Flush();
193: File.WriteAllText(mapFile, sb.ToString());
194: }
195: }
196:
197: }
198: }
199: #endif
The help for the command is shown below:
C:\>stsadm -help gl-importaudiences
stsadm -o gl-importaudiences
Imports all audiences given the provided input file.
Parameters:
-inputfile <file to input results from>
[-deleteexisting <delete existing audiences>]
[-ssp <SSP name>]
[-compile]
[-mapfile <generate a map file to use for search and replace of Audience IDs>]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-importaudiences | MOSS 2007 | Released: 4/24/2009
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| inputfile | input | Yes | The path to the input file obtained via the gl-exportaudiences command. | -inputfile c:\audiences.xml
-input c:\audiences.xml |
| deleteexisting | delete | No | If specified then all existing audiences will be deleted prior to importing the audiences. Note that the “All site users” audience will not be deleted or updated. | -deleteexisting
-delete |
| ssp | No | The name of the SSP to import the audiences into. If omitted then the default SSP will be used. | -ssp SSP1 | |
| compile | c | No | If specified then compile the audience after creation/update. | -compile
-c |
| mapfile | map | No | If specified then an XML file will be generated providing the search and replace strings to use in order to update the GUIDs in your site collections. | -mapfile c:\map.xml
-map c:\map.xml |
The following is an example of how to import all audiences contained in the audiences.xml file:
stsadm -o gl-importaudiences -inputfile c:\audiences.xml -ssp SSP1 -mapfile c:\map.xml -compile
The following shows an example output of the map file after running the above command:
<Replacements> <Replacement> <SearchString>98E29BEF-8B1E-4113-BB15-6FAF1E6FB8D0</SearchString> <ReplaceString>(?i:1CA1F37E-A50A-4F84-BDCD-8C1279BADB3E)</ReplaceString> </Replacement> </Replacements>
Exporting Audiences using STSADM
I’d been wanting to build an export and import command for Audiences for quite some time but just haven’t gotten around to it. I’m currently looking for a good sample command to build during a possible talk at the next Best Practices conference so I decided to give this one a whack considering that I already had a good chunk of the code written and just needed to repurpose it. I don’t know if I’ll use this command for the presentation so if anyone has any good ideas of things they’d like to see please let me know and I’ll look into it.
One of the things I was hoping to achieve through this was the ability to do an import and preserve the ID of the Audience so that content targeting Audiences could be migrated between environments – unfortunately I quickly discovered that the ID is set via stored procedures during the creation of the Audience so there was no way for me to intercept the creation process and change the ID without going to the database directly which is something I was not willing to do. I decided that the ability to migrate Audiences between environments, even without being able to set the ID, was still valuable so I went ahead with the creation of the commands.
To do the export I get an instance of the AudienceManager class and then loop through the collection of Audience objects using the Audiences property. I then use an XmlTextWriter to write out all the properties of the Audience and then I loop through the AudienceRuleComponent objects which you can get via the AudienceRules property of the Audience object, once again writing out all the properties using the XmlTextWriter. There’s really not much to it as you can see in the code 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.SharePoint;
13:
14: namespace Lapointe.SharePoint.STSADM.Commands.Audiences
15: {
16: public class ExportAudiences : SPOperation
17: {
18: /// <summary>
19: /// Initializes a new instance of the <see cref="ExportAudiences"/> class.
20: /// </summary>
21: public ExportAudiences()
22: {
23: SPParamCollection parameters = new SPParamCollection();
24: parameters.Add(new SPParam("name", "n", false, null, new SPNonEmptyValidator()));
25: parameters.Add(new SPParam("ssp", "ssp", false, null, new SPNonEmptyValidator()));
26: parameters.Add(new SPParam("explicit", "ex"));
27: parameters.Add(new SPParam("outputfile", "output", false, null, new SPDirectoryExistsAndValidFileNameValidator()));
28:
29: StringBuilder sb = new StringBuilder();
30: sb.Append("\r\n\r\nExports all audiences or a specific audience if a name is provided.\r\n\r\nParameters:");
31: sb.Append("\r\n\t-outputfile <file to output results to>");
32: sb.Append("\r\n\t[-name <audience name>]");
33: sb.Append("\r\n\t[-ssp <SSP name>]");
34: sb.Append("\r\n\t[-explicit (shows field and value attributes for every rule)]");
35: Init(parameters, sb.ToString());
36: }
37:
38: /// <summary>
39: /// Gets the help message.
40: /// </summary>
41: /// <param name="command">The command.</param>
42: /// <returns></returns>
43: public override string GetHelpMessage(string command)
44: {
45: return HelpMessage;
46: }
47:
48: /// <summary>
49: /// Executes the specified command.
50: /// </summary>
51: /// <param name="command">The command.</param>
52: /// <param name="keyValues">The key values.</param>
53: /// <param name="output">The output.</param>
54: /// <returns></returns>
55: public override int Execute(string command, StringDictionary keyValues, out string output)
56: {
57: output = string.Empty;
58:
59: string outputFile = Params["outputfile"].Value;
60: string xml = Export(Params["ssp"].Value, Params["name"].Value, Params["explicit"].UserTypedIn);
61:
62: File.WriteAllText(outputFile, xml);
63:
64: return OUTPUT_SUCCESS;
65: }
66:
67: /// <summary>
68: /// Returns an XML structure containing all the audience details.
69: /// </summary>
70: /// <param name="sspName">Name of the SSP.</param>
71: /// <param name="audienceName">Name of the audience.</param>
72: /// <param name="includeAllAttributes">if set to <c>true</c> [include all attributes].</param>
73: /// <returns></returns>
74: private string Export(string sspName, string audienceName, bool includeAllAttributes)
75: {
76: ServerContext context;
77: if (string.IsNullOrEmpty(sspName))
78: context = ServerContext.Default;
79: else
80: context = ServerContext.GetContext(sspName);
81:
82: AudienceManager manager = new AudienceManager(context);
83:
84: if (!string.IsNullOrEmpty(audienceName) && !manager.Audiences.AudienceExist(audienceName))
85: {
86: throw new SPException("Audience name does not exist");
87: }
88:
89: StringBuilder sb = new StringBuilder();
90: XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
91: xmlWriter.Formatting = Formatting.Indented;
92:
93: xmlWriter.WriteStartElement("Audiences");
94:
95: if (!string.IsNullOrEmpty(audienceName))
96: {
97: Audience audience = manager.Audiences[audienceName];
98: ExportAudience(xmlWriter, audience, includeAllAttributes);
99: }
100: else
101: {
102: foreach (Audience audience in manager.Audiences)
103: ExportAudience(xmlWriter, audience, includeAllAttributes);
104: }
105:
106: xmlWriter.WriteEndElement(); // Audiences
107: xmlWriter.Flush();
108: return sb.ToString();
109: }
110:
111: /// <summary>
112: /// Exports the audience.
113: /// </summary>
114: /// <param name="xmlWriter">The XML writer.</param>
115: /// <param name="audience">The audience.</param>
116: /// <param name="includeAllAttributes">if set to <c>true</c> [include all attributes].</param>
117: private static void ExportAudience(XmlWriter xmlWriter, Audience audience, bool includeAllAttributes)
118: {
119: xmlWriter.WriteStartElement("Audience");
120: xmlWriter.WriteAttributeString("AudienceDescription", audience.AudienceDescription);
121: xmlWriter.WriteAttributeString("AudienceID", audience.AudienceID.ToString());
122: xmlWriter.WriteAttributeString("AudienceName", audience.AudienceName);
123: xmlWriter.WriteAttributeString("AudienceSite", audience.AudienceSite);
124: xmlWriter.WriteAttributeString("CreateTime", audience.CreateTime.ToString());
125: xmlWriter.WriteAttributeString("GroupOperation", audience.GroupOperation.ToString());
126: xmlWriter.WriteAttributeString("LastCompilation", audience.LastCompilation.ToString());
127: xmlWriter.WriteAttributeString("LastError", audience.LastError);
128: xmlWriter.WriteAttributeString("LastPropertyUpdate", audience.LastPropertyUpdate.ToString());
129: xmlWriter.WriteAttributeString("LastRuleUpdate", audience.LastRuleUpdate.ToString());
130: xmlWriter.WriteAttributeString("MemberShipCount", audience.MemberShipCount.ToString());
131: xmlWriter.WriteAttributeString("OwnerAccountName", audience.OwnerAccountName);
132:
133:
134: ArrayList audienceRules = audience.AudienceRules;
135: xmlWriter.WriteStartElement("rules");
136: if (audienceRules != null && audienceRules.Count > 0)
137: {
138: foreach (AudienceRuleComponent rule in audienceRules)
139: {
140: xmlWriter.WriteStartElement("rule");
141: if (includeAllAttributes)
142: {
143: xmlWriter.WriteAttributeString("field", rule.LeftContent);
144: xmlWriter.WriteAttributeString("op", rule.Operator);
145: xmlWriter.WriteAttributeString("value", rule.RightContent);
146: }
147: else
148: {
149: switch (rule.Operator.ToLowerInvariant())
150: {
151: case "=":
152: case ">":
153: case ">=":
154: case "<":
155: case "<=":
156: case "contains":
157: case "<>":
158: case "not contains":
159: xmlWriter.WriteAttributeString("field", rule.LeftContent);
160: xmlWriter.WriteAttributeString("op", rule.Operator);
161: xmlWriter.WriteAttributeString("value", rule.RightContent);
162: break;
163: case "reports under":
164: case "member of":
165: xmlWriter.WriteAttributeString("op", rule.Operator);
166: xmlWriter.WriteAttributeString("value", rule.RightContent);
167: break;
168: case "and":
169: case "or":
170: case "(":
171: case ")":
172: xmlWriter.WriteAttributeString("op", rule.Operator);
173: break;
174: }
175: }
176: xmlWriter.WriteEndElement(); // rule
177: }
178: }
179: xmlWriter.WriteEndElement(); // rules
180: xmlWriter.WriteEndElement(); // Audience
181: }
182: }
183: }
184: #endif
The help for the command is shown below:
C:\>stsadm -help gl-exportaudiences
stsadm -o gl-exportaudiences
Exports all audiences or a specific audience if a name is provided.
Parameters:
-outputfile <file to output results to>
[-name <audience name>]
[-ssp <SSP name>]
[-explicit (shows field and value attributes for every rule)]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-exportaudiences | MOSS 2007 | Released: 4/17/2009
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| outputfile | output | Yes | The path to the output file to save the exported audience details. | -outputfile c:\audiences.xml
-output c:\audiences.xml |
| name | n | No | The name of a specific audience if only one audience is to be exported. | -name HR
-n HR |
| ssp | No | The name of the SSP containing the audiences to export. If omitted then the default SSP will be used. | -ssp SSP1 | |
| explicit | ex | No | If specified then all field and value properties for the rules will be populated (in many cases these properties are redundant and do not add any information and are not necessary for import). | -explicit |
The following is an example of how to export all audiences belonging to SSP1:
stsadm -o gl-exportaudiences -outputfile c:\audiences.xml -ssp SSP1
The following shows an example output of running the above command:
<Audiences> <Audience AudienceDescription="All users who can access the site" AudienceID="00000000-0000-0000-0000-000000000000" AudienceName="All site users" AudienceSite="http://sspadmin/ssp/admin" CreateTime="1/1/0001 12:00:00 AM" GroupOperation="AUDIENCE_NOGROUP_OPERATION" LastCompilation="1/1/0001 12:00:00 AM" LastError="" LastPropertyUpdate="1/1/0001 12:00:00 AM" LastRuleUpdate="1/1/0001 12:00:00 AM" MemberShipCount="0" OwnerAccountName=""> <rules /> </Audience> <Audience AudienceDescription="" AudienceID="a2e62d72-fa52-4772-a5d0-9b950ec7d220" AudienceName="HR" AudienceSite="http://sspadmin/ssp/admin" CreateTime="4/20/2009 8:23:48 PM" GroupOperation="AUDIENCE_OR_OPERATION" LastCompilation="4/20/2009 8:26:22 PM" LastError="" LastPropertyUpdate="4/20/2009 8:26:07 PM" LastRuleUpdate="4/20/2009 8:26:07 PM" MemberShipCount="6" OwnerAccountName="spdev\spadmin"> <rules> <rule op="Member of" value="CN=Human Resources Dept,OU=Users,DC=CompanyName,DC=com" /> <rule op="OR" /> <rule field="Department" op="=" value="Human Resources" /> </rules> </Audience> </Audiences>
Setting the Audience Compilation Schedule via STSADM
In an effort to wrap up my audience related STSADM commands I created a command that allows me to set the audience compilation schedule via STSADM. I had to do some disassembling to figure out how to do this and it turned out that the code was virtually identical to what I had done for the gl-setuserprofileimportschedule command. So it turned out that I was able to create this command by simply coping the code from my other command and then just tweaking a couple lines to load up different class types. I named the command gl-setaudiencecompilationschedule. The downside of this code (and the code it's based off of) is that I had to use reflection to get it done as all the classes are marked internally (no idea why). If anyone knows of a way to do this without all the reflect I'm all ears.
Here's the code - it's ugly, but it works:
1: #if MOSS
2: using System;
3: using System.Collections.Specialized;
4: using System.Reflection;
5: using System.Text;
6: using System.Threading;
7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
8: using Microsoft.Office.Server;
9: using Microsoft.Office.Server.UserProfiles;
10: using Microsoft.SharePoint;
11: using Microsoft.SharePoint.Administration;
12: using Microsoft.SharePoint.StsAdmin;
13: using PropertyInfo=System.Reflection.PropertyInfo;
14: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
15:
16: namespace Lapointe.SharePoint.STSADM.Commands.TimerJob
17: {
18: public class SetAudienceCompilationSchedule : SPOperation
19: {
20: private enum OccurrenceType
21: {
22: daily,
23: weekly,
24: monthly
25: }
26:
27: /// <summary>
28: /// Initializes a new instance of the <see cref="SetAudienceCompilationSchedule"/> class.
29: /// </summary>
30: public SetAudienceCompilationSchedule()
31: {
32: SPParamCollection parameters = new SPParamCollection();
33: parameters.Add(new SPParam("sspname", "ssp", false, null, new SPNonEmptyValidator(), "Please specify the SSP name."));
34: parameters.Add(new SPParam("occurrence", "oc", true, null, new SPRegexValidator("^daily$|^weekly$|^monthly$")));
35: parameters.Add(new SPParam("hour", "hour", true, null, new SPIntRangeValidator(0, 23)));
36: parameters.Add(new SPParam("day", "day", false, null, new SPIntRangeValidator(1, 31)));
37: string regex = "^" + string.Join("$|^", Enum.GetNames(typeof (DayOfWeek))) + "$";
38: parameters.Add(new SPParam("dayofweek", "dayofweek", false, null, new SPRegexValidator(regex.ToLowerInvariant() + "|" + regex)));
39: parameters.Add(new SPParam("enabled", "enabled", false, "true", new SPTrueFalseValidator()));
40: parameters.Add(new SPParam("runjob", "run"));
41:
42: StringBuilder sb = new StringBuilder();
43: sb.Append("\r\n\r\nSets the audience compilation schedule.\r\n\r\nParameters:");
44: sb.Append("\r\n\t[-sspname <SSP name>]");
45: sb.Append("\r\n\t-occurrence <daily|weekly|monthly>");
46: sb.Append("\r\n\t-hour <hour to run (0-23)>");
47: sb.Append("\r\n\t[-day <the day to run if monthly is specified>]");
48: sb.AppendFormat("\r\n\t[-dayofweek <the day of week to run if weekly is specified ({0})>]", string.Join("|", Enum.GetNames(typeof(DayOfWeek))).ToLowerInvariant());
49: sb.Append("\r\n\t[-enabled <true|false> (default is true)]");
50: sb.Append("\r\n\t[-runjob]");
51: Init(parameters, sb.ToString());
52: }
53:
54: #region ISPStsadmCommand Members
55:
56: /// <summary>
57: /// Gets the help message.
58: /// </summary>
59: /// <param name="command">The command.</param>
60: /// <returns></returns>
61: public override string GetHelpMessage(string command)
62: {
63: return HelpMessage;
64: }
65:
66: /// <summary>
67: /// Runs the specified command.
68: /// </summary>
69: /// <param name="command">The command.</param>
70: /// <param name="keyValues">The key values.</param>
71: /// <param name="output">The output.</param>
72: /// <returns></returns>
73: public override int Execute(string command, StringDictionary keyValues, out string output)
74: {
75: output = string.Empty;
76:
77:
78:
79: #region Check Arguments
80:
81: OccurrenceType occurrence = (OccurrenceType)Enum.Parse(typeof(OccurrenceType), Params["occurrence"].Value, true);
82: if (occurrence == OccurrenceType.monthly && !Params["day"].UserTypedIn)
83: {
84: output = "Please specify the day to run the import.";
85: output += GetHelpMessage(command);
86: return (int)ErrorCodes.SyntaxError;
87: }
88: if (occurrence == OccurrenceType.weekly && !Params["dayofweek"].UserTypedIn)
89: {
90: output = "Please specify the day of week to run the import.";
91: output += GetHelpMessage(command);
92: return (int)ErrorCodes.SyntaxError;
93: }
94:
95: #endregion
96:
97: string day = Params["day"].Value;
98: string dayofweek = Params["dayofweek"].Value;
99: string sspname = Params["sspname"].Value;
100: int hour = int.Parse(Params["hour"].Value);
101: bool enabled = bool.Parse(Params["enabled"].Value);
102: bool runJob = Params["runjob"].UserTypedIn;
103: if (!enabled && runJob)
104: throw new SPSyntaxException("The runjob parameter cannot be specified when enabled is set to false.");
105:
106: ServerContext current;
107: if (Params["sspname"].UserTypedIn)
108: current = ServerContext.GetContext(sspname);
109: else
110: current = ServerContext.Default;
111:
112: // What follows is a whole lot of reflection which is required in order to get the SPScheduledJob object.
113: // Problem is that the only way to get the correct instance of this object is to use several internal
114: // classes, methods, and properties - why on earth these were not made public is absolutely beyond me!
115:
116: // The bulk of the reflection is recreating the following which was taken from
117: // Microsoft.SharePoint.Portal.UserProfiles.AdminUI.Sched.InitializeComponent().
118: // Once we have the job objects we can start setting properties.
119: /*
120: private void InitializeComponent()
121: {
122: ServerContext current = ServerContext.Current;
123: UserProfileApplication userProfileApplication = current.UserProfileApplication;
124: try
125: {
126: using (PortalApplication.BeginSecurityContext())
127: {
128: JobSchedulerSharedApplicationCollection applications = new JobSchedulerSharedApplicationCollection(SPFarm.Local.Services.GetValue<JobSchedulerService>(string.Empty));
129: JobSchedulerSharedApplication sharedApplication = (JobSchedulerSharedApplication) applications[current.SharedResourceProvider];
130: ScheduledJobCollection jobs = new ScheduledJobCollection(sharedApplication);
131: this.AudienceCompileScheduler.Job = jobs[userProfileApplication.AudienceCompilationJobId];
132: }
133: }
134: catch (Exception)
135: {
136: throw;
137: }
138: }
139: */
140:
141: // UserProfileApplication userProfileApplication = current.UserProfileApplication;
142: object userProfileApplication = Utilities.GetPropertyValue(current, "UserProfileApplication");
143:
144: // The SSP is locked down so we need to use reflection to get at it.
145: object sharedResourceProvider = Utilities.GetSharedResourceProvider(current);
146:
147: // JobSchedulerService jobSchedulerService = SPFarm.Local.Services.GetValue(typeof(JobSchedulerService));
148: Type jobSchedulerServiceType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerService, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
149:
150:
151: MethodInfo getValue =
152: SPFarm.Local.Services.GetType().GetMethod("GetValue",
153: BindingFlags.NonPublic | BindingFlags.Public |
154: BindingFlags.Instance | BindingFlags.InvokeMethod, null, new Type[] {typeof(Type), typeof(string)}, null);
155:
156: object jobSchedulerService = getValue.Invoke(SPFarm.Local.Services,
157: new object[]
158: {
159: jobSchedulerServiceType, string.Empty
160: });
161:
162:
163: // JobSchedulerSharedApplicationCollection application = new JobSchedulerSharedApplicationCollection(jobSchedulerServiceType);
164: Type jobSchedulerSharedApplicationCollectionType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerSharedApplicationCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
165:
166: ConstructorInfo jobSchedulerSharedApplicationCollectionConstructor =
167: jobSchedulerSharedApplicationCollectionType.GetConstructor(
168: BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
169: null,
170: new Type[] {jobSchedulerService.GetType()}, null);
171: object applications = jobSchedulerSharedApplicationCollectionConstructor.Invoke(new object[] { jobSchedulerService });
172:
173: // JobSchedulerSharedApplication jobSchedulerSharedApplication = applications[sharedResourceProvider];
174: PropertyInfo itemProp = applications.GetType().GetProperty("Item",
175: BindingFlags.NonPublic |
176: BindingFlags.Instance |
177: BindingFlags.InvokeMethod |
178: BindingFlags.GetProperty |
179: BindingFlags.Public);
180: object jobSchedulerSharedApplication = itemProp.GetValue(applications, new object[] { sharedResourceProvider });
181:
182:
183: //ScheduledJobCollection scheduledJobCollection = new ScheduledJobCollection(sharedApplication);
184: Type scheduledJobCollectionType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJobCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
185: ConstructorInfo scheduledJobCollectionConstructor =
186: scheduledJobCollectionType.GetConstructor(
187: BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
188: null,
189: new Type[] {jobSchedulerSharedApplication.GetType()}, null);
190: object scheduledJobCollection = scheduledJobCollectionConstructor.Invoke(new object[] { jobSchedulerSharedApplication });
191:
192:
193: // userProfileApplication.AudienceCompilationJobId
194: Guid audienceCompilationJobId = (Guid)Utilities.GetPropertyValue(userProfileApplication, "AudienceCompilationJobId");
195:
196:
197:
198: // ScheduledJob compilationJob = scheduledJobCollection[audienceCompilationJobId];
199: itemProp = scheduledJobCollection.GetType().GetProperty("Item",
200: BindingFlags.NonPublic |
201: BindingFlags.Instance |
202: BindingFlags.InvokeMethod |
203: BindingFlags.GetProperty |
204: BindingFlags.Public);
205: object compilationJob = itemProp.GetValue(scheduledJobCollection, new object[] { audienceCompilationJobId });
206:
207:
208: PropertyInfo scheduleProp = compilationJob.GetType().GetProperty("Schedule",
209: BindingFlags.FlattenHierarchy |
210: BindingFlags.NonPublic |
211: BindingFlags.Instance |
212: BindingFlags.InvokeMethod |
213: BindingFlags.GetProperty |
214: BindingFlags.Public);
215:
216: MethodInfo update =
217: compilationJob.GetType().GetMethod("Update",
218: BindingFlags.NonPublic |
219: BindingFlags.Public |
220: BindingFlags.Instance |
221: BindingFlags.InvokeMethod |
222: BindingFlags.FlattenHierarchy,
223: null,
224: new Type[] {typeof (bool)}, null);
225:
226: // Woohoo!!! We are finally at a point where we can actually set the schedule - what a pain the @$$ that was!!!
227: SPSchedule schedule;
228:
229: if (occurrence == OccurrenceType.daily)
230: {
231: schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleDaily(hour);
232: }
233: else if (occurrence == OccurrenceType.weekly)
234: {
235: schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleWeekly((DayOfWeek)Enum.Parse(typeof(DayOfWeek), dayofweek, true), hour);
236: }
237: else if (occurrence == OccurrenceType.monthly)
238: {
239: schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleMonthly(int.Parse(day), hour);
240: }
241: else
242: throw new Exception("Unknown occurance type.");
243:
244: Type scheduledJobType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJob, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
245:
246:
247: // fullImportJob.Schedule = schedule;
248: scheduleProp.SetValue(compilationJob, schedule, null);
249:
250: // fullImportJob.Enabled = enabled;
251: Utilities.SetPropertyValue(compilationJob, scheduledJobType, "Disabled", !enabled);
252:
253: // fullImportJob.Update(true);
254: update.Invoke(compilationJob, new object[] { true });
255:
256: if (runJob)
257: {
258: // fullImportJob.Execute();
259: Utilities.ExecuteMethod(compilationJob, "Execute", new Type[] { }, new object[] { });
260: }
261:
262: if (runJob)
263: {
264: // We want to wait until the import is finished before moving on in case we are being run in a batch that requires this to complete before continueing.
265: UserProfileConfigManager manager = new UserProfileConfigManager(current);
266: while (manager.IsImportInProgress())
267: Thread.Sleep(500);
268: }
269:
270:
271: return OUTPUT_SUCCESS;
272: }
273:
274: #endregion
275:
276: }
277: }
278: #endif
The help for the command is shown below:
C:\>stsadm -help gl-setaudiencecompilationschedule
stsadm -o gl-setaudiencecompilationschedule
Sets the audience compilation schedule.
Parameters:
[-sspname <SSP name>]
-occurrence <daily|weekly|monthly>
-hour <hour to run (0-23)>
[-day <the day to run if monthly is specified>]
[-dayofweek <the day of week to run if weekly is specified (sunday|monday|tuesday|wednesday|thursday|friday|saturday)>]
[-enabled <true|false> (default is true)]
[-runjob]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-setaudiencecompilationschedule | MOSS 2007 | Release: 8/14/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| sspname | ssp | No | The name of the SSP that the audiences to compile are associated with. If omitted the default SSP will be used. | -sspname SSP1
-ssp SSP1 |
| occurrence | oc | Yes | Specifies how frequently the compilation should occur. Valid values are "daily", "weekly", and "monthly". | -occurrence daily
-oc monthly |
| hour | Yes | The hour in which to run the compilation. This should be an integer between 0 and 23 (where 0 is 12:00am and 23 is 11:00pm). | -hour 22 | |
| day | No, unless occurrence is monthly | The day of the month to run the compilation job. Valid values are between 1 and 31. | -day 1 | |
| dayofweek | No, unless occurrence is weekly | The day of the week to run the compilation job. Valid values are "sunday", "monday", "tuesday", "wednesday", "thursday", and "saturday". | -dayofweek saturday | |
| enabled | No | "true" to enable the compilation schedule, "false" to disable it. If not specified then the compilation schedule will be enabled. | -enabled true | |
| runjob | run | No | If specified then the compilation job will be immediately executed after setting the schedule. | -runjob
-run |
The following is an example of how to set the compilation schedule to run every Satruday at 10:00pm:
stsadm -o gl-setaudiencecompilationschedule -occurrence weekly -hour 22 -dayofweek saturday -enabled true -runjob
Deleting an Audience via STSADM
Using the commands I've created so far you can now create audiences, add rules, and display those rules all via STSADM. If you're like me and you do a lot of testing of this stuff before you push out the final version then you'll want a way to also easily delete audiences that you've created. Of course you could easily do this via the browser but what if you've added complex rules to your audience using my gl-addaudiencerule command? You'll find that you can't delete it from the browser. To deal with this I've created an additional command called, simply enough, gl-deleteaudience.
One thing I added to the command was an option to delete only the rules and not the audience itself. This allows you to essentially revert the audience back to a pure state so that you can manage it via the browser if you so desire. The code to delete an audience is real simple - essentially one line: manager.Audiences.Remove(audience.AudienceID); - the rest is just error handling and clearing of the rules:
1: /// <summary>
2: /// Deletes the specified audience or all audience rules for the specified audience.
3: /// </summary>
4: /// <param name="sspName">Name of the SSP.</param>
5: /// <param name="audienceName">Name of the audience.</param>
6: /// <param name="deleteRulesOnly">if set to <c>true</c> [delete rules only].</param>
7: private void Delete(string sspName, string audienceName, bool deleteRulesOnly)
8: {
9: ServerContext context;
10: if (string.IsNullOrEmpty(sspName))
11: context = ServerContext.Default;
12: else
13: context = ServerContext.GetContext(sspName);
14:
15: AudienceManager manager = new AudienceManager(context);
16:
17: if (!manager.Audiences.AudienceExist(audienceName))
18: {
19: throw new SPException("Audience name does not exist");
20: }
21:
22: Audience audience = manager.Audiences[audienceName];
23:
24: if (audience.AudienceRules != null && deleteRulesOnly)
25: {
26: audience.AudienceRules = new ArrayList();
27: audience.GroupOperation = AudienceGroupOperation.AUDIENCE_OR_OPERATION;
28: audience.Commit();
29: return;
30: }
31: if (!deleteRulesOnly)
32: manager.Audiences.Remove(audience.AudienceID);
33: }
The help for the command is shown below:
C:\>stsadm -help gl-deleteaudience
stsadm -o gl-deleteaudience
Deletes an audience. To delete only the rules associated with the audience pass in the -rulesonly parameter.
Parameters:
-name <audience name>
[-ssp <SSP name>]
[-rulesonly (delete only the rules)]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-deleteaudience | MOSS 2007 | 8/6/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| name | n | Yes | The name of the audience to delete. | -name "IT"
-n "IT" |
| ssp | No | The name of the SSP that the audience is associated with. If omitted the default SSP will be used. | -ssp SSP1 | |
| -rulesonly | ro | No | If specified then only the audience rules will be deleted and not the audience itself. | -rulesonly
-ro |
The following is an example of how to delete an audience:
stsadm -o gl-deleteaudience -name "IT"
The following is an example of how to delete only the rules associated with an audience
stsadm -o gl-deleteaudience -name "IT" -rulesonly
Displaying Audience Rules via STSADM
Okay, so you've created your audience and used my gl-addaudiencerule command to add some complex rules to the audience. Three months later you've forgotten what those rules are and need to see them again - unfortunately you can't do that via the browser, so what do you do? Simple, you run gl-enumaudiencerules, another command I've created to help manage audiences via STSADM.
This command is really simple so I'll be brief - I simply loop through all the rules associated with an audience and display back an XML structure representing those rules. The nice thing about this is that you can use it to help build your rules to assign using the gl-addaudiencerule command - you'd create all the rules you need via the browser, with no groupings, and then use this command to get the XML and then simply add your grouping elements into the XML - this way you're sure the field names are correct.
Here's a snippet of the code:
1:
2: /// <summary>
3: /// Returns an XML structure containing all the rules associated with the audience.
4: /// </summary>
5: /// <param name="sspName">Name of the SSP.</param>
6: /// <param name="audienceName">Name of the audience.</param>
7: /// <param name="includeAllAttributes">if set to <c>true</c> [include all attributes].</param>
8: /// <returns></returns>
9: private string EnumRules(string sspName, string audienceName, bool includeAllAttributes)
10: {
11: ServerContext context;
12: if (string.IsNullOrEmpty(sspName))
13: context = ServerContext.Default;
14: else
15: context = ServerContext.GetContext(sspName);
16:
17: AudienceManager manager = new AudienceManager(context);
18:
19: if (!manager.Audiences.AudienceExist(audienceName))
20: {
21: throw new SPException("Audience name does not exist");
22: }
23:
24: Audience audience = manager.Audiences[audienceName];
25:
26: ArrayList audienceRules = audience.AudienceRules;
27:
28: if (audienceRules == null || audienceRules.Count == 0)
29: return "The audience contains no rules.";
30:
31: string rulesXml = "<rules>\r\n";
32: foreach (AudienceRuleComponent rule in audienceRules)
33: {
34: if (includeAllAttributes)
35: {
36: rulesXml += string.Format("\t<rule field=\"{1}\" op=\"{0}\" value=\"{2}\" />\r\n", rule.Operator, rule.LeftContent, rule.RightContent);
37: }
38: else
39: {
40: switch (rule.Operator.ToLowerInvariant())
41: {
42: case "=":
43: case ">":
44: case ">=":
45: case "<":
46: case "<=":
47: case "contains":
48: case "<>":
49: case "not contains":
50: rulesXml += string.Format("\t<rule field=\"{1}\" op=\"{0}\" value=\"{2}\" />\r\n", rule.Operator, rule.LeftContent, rule.RightContent);
51: break;
52: case "reports under":
53: case "member of":
54: rulesXml += string.Format("\t<rule op=\"{0}\" value=\"{1}\" />\r\n", rule.Operator, rule.RightContent);
55: break;
56: case "and":
57: case "or":
58: case "(":
59: case ")":
60: rulesXml += string.Format("\t<rule op=\"{0}\" />\r\n", rule.Operator);
61: break;
62: }
63: }
64: }
65: rulesXml += "</rules>";
66: return rulesXml;
67: }
The help for the command is shown below:
C:\>stsadm -help gl-enumaudiencerules
stsadm -o gl-enumaudiencerules
Outputs the rules for an audience.
Parameters:
-name <audience name>
[-ssp <SSP name>]
[-explicit (shows field and value attributes for every rule)]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-enumaudiencerules | MOSS 2007 | 8/6/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| name | n | Yes | The name of the audience | -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 | |
| explicit | ex | No | If specified then all attributes will be returned in the XML - this is only really useful if you wish to see the field names of the special operators, "member of" and "reports under". | -explicit
-ex |
The following is an example of how to display the rules associated with an audience:
stsadm -o gl-enumaudiencerules -name "IT Department"
The following is an example of the XML that would be displayed:
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>
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:
- 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 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>
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;
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;
14:
15: namespace 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 | 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
Creating Audiences via STSADM
The project that I'm currently working has a need to create a ton of audiences representing divisions, departments, locations, etc. We wanted to be able to include the creation of these audiences in our install script so that we easily recreate the audiences in various environments.
To accomplish this task there were two core things I had to figure out - the first was how to create the actual audience, this was the easy part. The second was how to assign rules to that audience - I'll discuss this problem in my next post.
Creating an audience programmatically turned out to be real easy - you basically just get an instance of the AudienceManager class by passing in an SSP context into the constructor and then you use the Audiences property (which is of type AudienceCollection) and call the Create method to create the audience. The Create method returns back an Audience object which you can then use to set additional properties such as the owner (OwnerAccountName) and the group operation (GroupOperation). You then call Commit to save the changes.
The GroupOperation property probably needs some additional explanation. The value it takes is an AudienceGroupOperation enum which consists of the following values:
- AUDIENCE_AND_OPERATION
- AUDIENCE_OR_OPERATION
- AUDIENCE_MIX_OPERATION
- AUDIENCE_NOGROUP_OPERATION
This property defines how the audience rules are handled. If you specify AUDIENCE_AND_OPERATION then every rule specified will have to result in a match for an item to be returned. If you specify AUDIENCE_OR_OPERATION then if any of the rules matched for an item then it will be returned. This is consistent with the behavior you get when creating rules via the browser. If you set the value to AUDIENCE_NOGROUP_OPERATION then you are saying there is no grouping, or more specifically there will be only one rule.
Here's the one that may surprise some people (it did me) - the AUDIENCE_MIX_OPERATION basically says that I want to create a complex grouping of rules - so something like '(Department == "IT" and Reports Under "John Smith") or Hero == true'. That's great right? You can build audiences with complex rules so you can finally get creative - here's the catch: once you do this you can no longer manage those rules via the browser - the only thing you'll be able to do is compile the rule and view memberships - you won't even be able to delete it. But don't worry - I've got you covered - stay tuned for future posts! One more thing - you can't set the GroupOperation property to AUDIENCE_MIX_OPERATION - this value will be returned if your rules are complex but you can't preset the value. I found the best thing to do is to just not set this property at all - but I did choose to expose it in the command I created which I called gl-createaudience.
Here's a snip of the core code that you need to create the actual audience (again, adding rules will be covered in my next post):
1: public static void Create(string sspName, string audienceName, string description, RuleEnum rule, string owner)
2: {
3: ServerContext context;
4: if (string.IsNullOrEmpty(sspName))
5: context = ServerContext.Default;
6: else
7: context = ServerContext.GetContext(sspName);
8:
9: AudienceManager manager = new AudienceManager(context);
10: AudienceCollection auds = manager.Audiences;
11:
12: if (auds.AudienceExist(audienceName))
13: {
14: throw new SPException("Audience name already exists");
15: }
16: Audience audience = auds.Create(audienceName, description??"");// IMPORTANT: the create method does not do a null check but the methods that load the resultant collection assume not null.
17: if (rule == RuleEnum.Any)
18: audience.GroupOperation = AudienceGroupOperation.AUDIENCE_OR_OPERATION;
19: else if (rule == RuleEnum.All)
20: audience.GroupOperation = AudienceGroupOperation.AUDIENCE_AND_OPERATION;
21:
22: if (!string.IsNullOrEmpty(owner))
23: {
24: audience.OwnerAccountName = owner;
25: }
26: else
27: {
28: audience.OwnerAccountName = string.Empty;
29: }
30: audience.Commit();
31: }
The help for the command is shown below:
C:\>stsadm -help gl-createaudience
stsadm -o gl-createaudience
Creates an audience.
Parameters:
-name <audience name>
[-ssp <SSP name>]
[-description <audience description>]
[-owner <DOMAIN\user>]
[-rule <any | all | mix> (default is any)]
[-update (if the audience exists, update it)]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-createaudience | MOSS 2007 | Released: 8/6/2008
Updated: 8/14/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| name | n | Yes | This is the name of the audience. | -name "IT Department"
-n "IT Department" |
| ssp | No | The name of the SSP to create the audience within. If not specified then the default SSP is used. | -ssp SSP1 | |
| description | desc | No | A brief description of the audience. I like to provide a text version of the rules that are applied. This makes it easier to manage via the browser especially when complex rules are used. | -description "All employees who are members of the IT Department: Department==IT or Reports Under==John Smith"
-desc "All employees who are members of the IT Department: Department==IT or Reports Under==John Smith" |
| owner | No | The login name of the user who will be assigned as the owner of the audience. Though not required it's always recommended to identify someone as the person who is responsible for the logic behind the rules. | -owner "domain\user1" | |
| rule | r | No | One of the following values: Any, All, Mix. If Mix is specified it behaves the same as though the parameter were omitted. | -rule Any
-r Any |
| update | u | No | If specified then the existing audience, if found, will be updated. This is useful when using the command in the context of a script that you'd like to be able to run repeatedly without having to delete the audience and therefore have to reset any items using that audience. | -update
-u |
The following is an example of how to create an audience named "IT Department":
stsadm -o gl-createaudience -name "IT Department" -description "All employees of who are members of the IT Department: (Department==IT or Reports Under==John Smith) and IsContractor==false" -rule mix -owner domain\user1
Update 8/14/2008: I've added an -update parameter to allow updating the audience if it already exists.
Programmatically Setting Web Part Audience Targeting
I've been doing some work on my gl-exportlistitem2 and gl-addlistitem commands so that I can support the import of web part pages. I thought I was about done until I discovered that I had an issue with pages and web parts that were using audience targeting. There was actually two issues - one was that when I imported a page to another farm the GUID used to store the audience didn't match up so it would lose its setting for the page. The other issue was that some web parts (specifically anything that is not a V2 web part) do not export the AuthorizationFilter property which is where the audience settings are stored. I find this very odd that they chose to omit this property when exported. Note that for V2 web parts the audience information is exported because it is stored in the IsIncludedFilter property of the web part which has been marked as obsolete (internally this property just references the AuthorizationFilter property).
When I set out to do this I thought it would be pretty easy - just store the name with the audience ID and store the configuration information in a MetaData element that I have which I could then use to set the property during the import. The problem that I ran into was the format of the AuthorizationFilter property - it's just not documented anywhere! If you view the MSDN documentation for the property it says the following:
The Web Part infrastructure does not implement any default behavior for the AuthorizationFilter property. However, the property is provided so that you can assign an arbitrary string value to a custom Web Part. This property can be checked by SPWebPartManager during its AuthorizeWebPart event to determine whether the control can be added to the page.
The thing is - there's nothing arbitrary about this property - it has a very explicit format which if you don't follow will result in the web part failing to process the audience settings - the format of this matches the format of the Audience field of the web part page - if you mess that field up you will get an error when trying to get into the page settings.
So what is the format of the property? First you need to understand that there are three types of audience information that the property is storing: Global Audiences created via the SSP; Distribution Lists; SharePoint Groups. Each of these values is stored slightly differently and the order of their appearance is important. Each item is delimited with two semi-colons: ";;".
The global audience is stored as a GUID and is the only type of item that can be stored without any delimiter information if no other items exist. Multiple items are separated with a comma.
The distribution list is stored as an LDAP string. Multiple items are separated with a new line character ("\n").
The SharePoint groups are stored as a named value (i.e., "Members", "Owners", etc.) and multiple items are separated with a comma.
So putting it all together you would construct a string containing one or more elements of each like so:
1: string[] globalAudienceIDs = new string[] {"e4687e64-c9d8-4860-bbc3-ec036bf9915d"};
2: string[] dlDistinguishedNames = new string[] {"cn=group1,cn=users,dc=spdev,dc=com", "cn=group2,cn=users,dc=spdev,dc=com"};
3: string[] sharePointGroupNames = new string[] {"Demo Members", "Demo Owners"};
4:
5: string result = string.Format("{0};;{1};;{2}",
6: string.Join(",", globalAudienceIDs),
7: string.Join("\n", dlDistinguishedNames),
8: string.Join(",", sharePointGroupNames));
That being said - if you can avoid hard coding this formatting than you should. So how do you avoid it - you use the GetAudienceIDsAsText method of the AudienceManager class. To use this method you would do something like the following:
1: string[] globalAudienceIDs = new string[] {"e4687e64-c9d8-4860-bbc3-ec036bf9915d"};
2: string[] dlDistinguishedNames = new string[] {"cn=group1,cn=users,dc=spdev,dc=com", "cn=group2,cn=users,dc=spdev,dc=com"};
3: string[] sharePointGroupNames = new string[] {"Demo Members", "Demo Owners"};
4:
5: string result = AudienceManager.GetAudienceIDsAsText(globalAudienceIDs, dlDistinguishedNames, sharePointGroupNames);
What's cool is that there is a reciprocal method called GetAudienceIDsFromText which will give you the IDs based on the string.
In my particular case I wasn't able to use these and had to know the format of the string because I needed to supplement the GUIDs with the named value so that I could do an import in a different farm.
Note that this same format is used for the Audience field of a publishing page.