Recommended Reading/Basics before you dive into this blog post: User Profiles and Audience Targeting.
What is this utility?
This is a command line utility that will allow you to export and import user profiles between two MOSS installations that cannot talk with each other, OR, do not have BDC (say you had a cheapo version of MOSS). You could export data from (SAP, or other sources) to XML, and use that XML to suck in data into your SSP in MOSS.
How to use this utility?
The utility comes with online help. Just type in "PI -help" at commandline to get the help screen. The help screen reproduced below for your pleasure -
=================================================
Profile Import Utility
=================================================
Usage: PI -mapping <mapfile.xml> -inputs <inputfile.xml> -url <MOSS_Web_URL>
*********** Required Parameters are: ***********
-mappingfile:
This Parameter points to an XML file that specifies mapping between your XML elements and MOSS properties.
For instance, PostalCode in your SAP system may mean ZipCode in your MOSS system, the mapping would thus look like this -
<Mapping InputField='PostalCode' MOSSField='ZipCode'/>
-inputs The file taht contains your data
-url URL of the MOSS 2007 web.
*********** XML File structures are: ***********
Mapping File:
<Mappings>
<Mapping InputField=.. MOSSField=../>
...
</Mappings>
Inputs File:
<Inputs>
<Input>
<AccountName>MOSS2007\administrator</AccountName>
<Title>Some JobTitle</Title>
... other properties ...
</Input>
... other inputs ...
</Inputs>
The inputs file can have properties identified by elements that mean something to your MOSS web.
Both inputs are validated via Embedded Schemas (the schemas are available in the source code).
*********** Sample usage: ***********
PI -mapping mapfile.xml -inputs inputsfile.xml -url http://moss2007
Code Explanation
The code accepts input as command line parameters, and passes them to a business object called ProgramInputs as shown below:
static void Main(string[] args)
{
ProgramInputs inputs = new ProgramInputs(args);
The ProgramInputs business object has 4 properties - Boolean "IsValid", String "SSPUrl", string "MappingFile" and a String "InputsFile". It performs various validations, such as if it is able to find the specified files, the specified URL, and if everything works - it decides that IsValid = true, i.e. inputs are valid.
Once we know that the inputs are valid, we can act upon them. This is done using the following code
if (inputs.IsValid)
{ ...
}
else
{
Trace.WriteLine("Invalid inputs specified", true);
Trace.PrintHelp();
}
In the actual code, one validation we need to do, is validate the XML Files, and see if they are in a valid acceptable form. As you can see, I do that using simple utility functions I have written, so my code becomes really simple to read -
if (
Utilities.IsXmlValid(inputs.InputsFile, "Winsmarts.PI.Common.InputsFileSchema.xsd") &
Utilities.IsXmlValid(inputs.MappingFile, "Winsmarts.PI.Common.MappingFileSchema.xsd")
)
{
The schemas are embedded in my dll. Suggested further reading: Working with Embedded Resources, and Validating an XML file with an XSD Schema.
Here are the schemas I am using -
InputsFileSchema -
<?xml version="1.0" encoding="utf-8"?>
<xs:schema
id="InputsFileSchema"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Inputs">
<xs:complexType>
<xs:sequence>
<xs:element name="Input" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
MappingFileSchema -
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema
id="MappingFileSchema"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Mappings">
<xs:complexType>
<xs:sequence>
<xs:element name="Mapping" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Mapping">
<xs:complexType>
<xs:attribute name="InputField" use="required"/>
<xs:attribute name="MOSSField" use="required"/>
<xs:attribute name="DateFormat" use="optional"/>
</xs:complexType>
</xs:element>
</xs:schema>
Once I know all my inputs are valid, I can then start working on the actual import process. The first thing I do is to import the Mappings into a custom business object. This business object is a dictionary of "Mapping" object instances. In setting up this business object, I can do all kinds of magic - calculations, validations etc. Here is the structure of the business object -
And setting this up is abstracted into a single line of code -
Mappings mappings = new Mappings(inputs.MappingFile);
(Single line of code, because all the rest of the crap I dont' want to see is encapsulated inside the business object).
The next thing I do is, read up the inputs, and setup the necessary counters -
// Begin the import
XmlDocument inputDoc = new XmlDocument();
inputDoc.Load(inputs.InputsFile);
XmlNodeList inputNodes = inputDoc.SelectNodes("./Inputs/Input");
int successes = 0;
int errors = 0;
Then in order to begin the import, I get a hold of the appropriate SharePoint API objects
using (SPSite site = new SPSite(inputs.SSPURL))
{
ServerContext context = ServerContext.GetContext(site);
UserProfileManager mngr = new UserProfileManager(context);
Mapping currentMapping = null;
foreach (XmlNode inputNode in inputNodes)
{
string accountName = inputNode.SelectSingleNode("./" + mappings.AccountField).InnerText;
UserProfile profile = mngr.GetUserProfile(accountName);
try
{
foreach (XmlNode dataNode in inputNode.ChildNodes)
{
// ... this is where more code will come
}
profile.Commit();
successes++;
}
catch (Exception ex)
{
errors++;
Trace.WriteLine("Error Importing:" + accountName + ". Error Details:\n" + ex.ToString(), true);
}
}
Trace.WriteLine("Import Finished, total errors: " + errors + " and total successes: " + successes);
}
As you can see, above I iterate over every node, i.e. every single input specified, I then get a hold of the appropriate profile in SharePoint, and then for each specified value under the input, I do some more stuff). This "some more stuff", is basically setting appropriate values while taking mappings into consideration. This in C# goes right here you see the "... this is where more code will come" comment, and it looks like this -
currentMapping = mappings[dataNode.Name];
if (currentMapping.MOSSField != "AccountName")
{
if (currentMapping.IsDate)
{
if (dataNode.InnerText.Length != 0)
profile[currentMapping.MOSSField].Value =
DateTime.ParseExact(dataNode.InnerText, currentMapping.DateFormat, null);
}
else
{
profile[currentMapping.MOSSField].Value = dataNode.InnerText;
}
}
And then finally, once all the profile information I wished to set has been set appropriately, I simply call profile.Commit() to make the changes permanent, and keep a count of errors and successes.
Pretty simple really. :-). You can now export user profile information in your organization from any system to XML, and from that XML suck it into any MOSS installation.
SHWEET !! :-)
You can find the full code here -
http://www.codeplex.com/MOSSProfileImport/