MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. 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.

Sunday, February 17, 2008

Export Content Types

I was recently working on a client project where there was an environment already setup with several custom content types and custom site columns that had been created. We wanted to reverse engineer those content types and columns so that we could create a Feature that could be used to deploy them into the various environments. I poked around a bit and couldn't find anything that would simply give me the CAML that I needed for my Feature. So I decided to create a couple of new commands to help me do this quickly: gl-exportcontenttypes and gl-exportsitecolumns. I'll cover gl-exportsitecolumns in a follow up post. Looking at the code you can see that there's really not much going on. I simply grab the content type(s) to export and get the SchemaXml property. One thing of note is that the SchemaXml property returns back XML that contains the complete field definition - because this won't work in a Feature I replace the <Field /> elements with a corresponding <FieldRef /> element. If the field definitions are needed then you can simply provide the includefielddefinitions parameter. Note that I'm outputting everything in one file but if you intend to use this in a Feature you'll want to break this up into multiple files. For the field definitions I also just output the SchemaXml of the SPField objects associated with the content type (note that there will be some attributes that you'll need to pull when using the resultant output in a Feature). To get the list bindings I loop through all the lists throughout the site collection and add a ContentTypeBindings element for each list that contains a reference to the identified content types.

   1: public class ExportContentTypes : SPOperation
   2: {
   3:     private const string ENCODED_SPACE = "_x0020_";
   4:     /// <summary>
   5:     /// Initializes a new instance of the <see cref="CopyContentTypes"/> class.
   6:     /// </summary>
   7:     public ExportContentTypes()
   8:     {
   9:         SPParamCollection parameters = new SPParamCollection();
  10:         parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the source site collection."));
  11:         parameters.Add(new SPParam("name", "n", false, null, new SPNonEmptyValidator()));
  12:         parameters.Add(new SPParam("group", "g", false, null, new SPNonEmptyValidator()));
  13:         parameters.Add(new SPParam("listname", "list", false, null, new SPNonEmptyValidator()));
  14:         parameters.Add(new SPParam("outputfile", "output", false, null, new SPDirectoryExistsAndValidFileNameValidator()));
  15:         parameters.Add(new SPParam("includelistbindings", "ilb"));
  16:         parameters.Add(new SPParam("includefielddefinitions", "ifd"));
  17:         parameters.Add(new SPParam("excludeparentfields", "epf"));
  18:         parameters.Add(new SPParam("removeencodedspaces", "res"));
  19:  
  20:         StringBuilder sb = new StringBuilder();
  21:         sb.Append("\r\n\r\nExports Content Types to an XML file.\r\n\r\nParameters:");
  22:         sb.Append("\r\n\t-url <url containing the content types>");
  23:         sb.Append("\r\n\t-outputfile <file to output results to>");
  24:         sb.Append("\r\n\t[-name <name of an individual content type to export>]");
  25:         sb.Append("\r\n\t[-group <content type group name to filter results by>]");
  26:         sb.Append("\r\n\t[-listname <name of a list to export content types from>]");
  27:         sb.Append("\r\n\t[-includelistbindings]");
  28:         sb.Append("\r\n\t[-includefielddefinitions]");
  29:         sb.Append("\r\n\t[-excludeparentfields]");
  30:         sb.Append("\r\n\t[-removeencodedspaces (removes '_x0020_' in field names)]");
  31:         Init(parameters, sb.ToString());
  32:     }
  33:  
  34:     #region ISPStsadmCommand Members
  35:  
  36:     /// <summary>
  37:     /// Gets the help message.
  38:     /// </summary>
  39:     /// <param name="command">The command.</param>
  40:     /// <returns></returns>
  41:     public override string GetHelpMessage(string command)
  42:     {
  43:         return HelpMessage;
  44:     }
  45:  
  46:     /// <summary>
  47:     /// Runs the specified command.
  48:     /// </summary>
  49:     /// <param name="command">The command.</param>
  50:     /// <param name="keyValues">The key values.</param>
  51:     /// <param name="output">The output.</param>
  52:     /// <returns></returns>
  53:     public override int Run(string command, StringDictionary keyValues, out string output)
  54:     {
  55:         output = string.Empty;
  56:  
  57:         InitParameters(keyValues);
  58:  
  59:         string url = Params["url"].Value.TrimEnd('/');
  60:         string contentTypeName = null;
  61:         if (Params["name"].UserTypedIn)
  62:             contentTypeName = Params["name"].Value;
  63:         string contentTypeGroup = null;
  64:         if (Params["group"].UserTypedIn)
  65:             contentTypeGroup = Params["group"].Value.ToLowerInvariant();
  66:         if (Params["group"].UserTypedIn && Params["listname"].UserTypedIn)
  67:             throw new SPSyntaxException("The parameters group and listname are incompatible");
  68:         bool excludeParentFields = Params["excludeparentfields"].UserTypedIn;
  69:         bool removeEncodedSpaces = Params["removeencodedspaces"].UserTypedIn;
  70:  
  71:         StringBuilder sb = new StringBuilder();
  72:         XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
  73:         xmlWriter.Formatting = Formatting.Indented;
  74:  
  75:         Dictionary<Guid, SPField> ctFields = new Dictionary<Guid, SPField>();
  76:  
  77:         using (SPSite site = new SPSite(url))
  78:         {
  79:             using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
  80:             {
  81:                 SPContentTypeCollection availableContentTypes;
  82:  
  83:                 if (Params["listname"].UserTypedIn)
  84:                 {
  85:                     SPList list = web.Lists[Params["listname"].Value];
  86:                     availableContentTypes = list.ContentTypes;
  87:                 }
  88:                 else
  89:                 {
  90:                     availableContentTypes = web.AvailableContentTypes;
  91:                 }
  92:                 List<SPContentType> contentTypes = new List<SPContentType>();
  93:                 try
  94:                 {
  95:                     xmlWriter.WriteStartElement("Elements");
  96:                     xmlWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/sharepoint/");
  97:  
  98:                     // Gather up all the content types we want to export out.
  99:                     if (contentTypeName != null)
 100:                     {
 101:                         SPContentType ct = availableContentTypes[contentTypeName];
 102:                         if (ct == null)
 103:                         {
 104:                             output += "The content type specified could not be found.";
 105:                             return 0;
 106:                         }
 107:                         else
 108:                         {
 109:                             contentTypes.Add(ct);
 110:                         }
 111:                     }
 112:                     else
 113:                     {
 114:                         // Loop through all the source content types and create them at the target.
 115:                         foreach (SPContentType ct in availableContentTypes)
 116:                         {
 117:                             if (contentTypeGroup == null || ct.Group.ToLowerInvariant() == contentTypeGroup)
 118:                             {
 119:                                 contentTypes.Add(ct);
 120:                             }
 121:                         }
 122:                     }
 123:  
 124:                     if (Params["includefielddefinitions"].UserTypedIn)
 125:                     {
 126:                         // If we're including field definitions then we want to show them first as they'll need to appear first when using within a Feature
 127:                         foreach (SPContentType ct in contentTypes)
 128:                         {
 129:                             SPContentType parentCT = ct.Parent;
 130:                             foreach (SPField field in ct.Fields)
 131:                             {
 132:                                 // If the parent content type contains the current field and the user wants to exclude parent fields then continue to the next field.
 133:                                 if (parentCT != null && excludeParentFields && parentCT.Fields.ContainsField(field.InternalName))
 134:                                     continue;
 135:  
 136:                                 if (!ctFields.ContainsKey(field.Id))
 137:                                     ctFields.Add(field.Id, field);
 138:                             }
 139:                         }
 140:                         xmlWriter.WriteString("\r\n\r\n");
 141:                         foreach (SPField field in ctFields.Values)
 142:                         {
 143:                             string schema = field.SchemaXml;
 144:                             if (field.InternalName.Contains(ENCODED_SPACE) && removeEncodedSpaces)
 145:                             {
 146:                                 schema = schema.Replace(string.Format("Name=\"{0}\"", field.InternalName),
 147:                                                         string.Format("Name=\"{0}\"", field.InternalName.Replace(ENCODED_SPACE, string.Empty)));
 148:                             }
 149:  
 150:                             xmlWriter.WriteRaw(schema);
 151:                             xmlWriter.WriteString("\r\n");
 152:                         }
 153:                     }
 154:  
 155:                     xmlWriter.WriteString("\r\n");
 156:  
 157:                     foreach (SPContentType ct in contentTypes)
 158:                     {
 159:                         WriteContentTypeXml(ct, excludeParentFields, removeEncodedSpaces, xmlWriter);
 160:                     }
 161:  
 162:                     if (Params["includelistbindings"].UserTypedIn)
 163:                         GetListBindings(web, contentTypes, xmlWriter);
 164:  
 165:                     xmlWriter.WriteEndElement(); // Elements
 166:                 }
 167:                 finally
 168:                 {
 169:                     xmlWriter.Flush();
 170:                     xmlWriter.Close();
 171:                 }
 172:             }
 173:         }
 174:  
 175:         File.WriteAllText(Params["outputfile"].Value, sb.ToString());
 176:         return 1;
 177:     }
 178:  
 179:  
 180:     #endregion
 181:  
 182:  
 183:     /// <summary>
 184:     /// Writes the content type XML.
 185:     /// </summary>
 186:     /// <param name="ct">The ct.</param>
 187:     /// <param name="excludeParentFields">if set to <c>true</c> [exclude parent fields].</param>
 188:     /// <param name="removeEncodedSpaces">if set to <c>true</c> [remove encoded spaces].</param>
 189:     /// <param name="xmlWriter">The XML writer.</param>
 190:     private static void WriteContentTypeXml(SPContentType ct, bool excludeParentFields, bool removeEncodedSpaces, XmlTextWriter xmlWriter)
 191:     {
 192:         SPContentType parentCT = ct.Parent;
 193:         XmlDocument xmlDoc = new XmlDocument();
 194:         xmlDoc.LoadXml(ct.SchemaXml);
 195:         XmlElement fieldRefsElement = xmlDoc.CreateElement("FieldRefs");
 196:         xmlDoc.DocumentElement.AppendChild(fieldRefsElement);
 197:         foreach (XmlElement field in xmlDoc.SelectNodes("//Fields/Field"))
 198:         {
 199:             // If the parent content type contains the current field and the user wants to exclude parent fields then continue to the next field.
 200:             if (parentCT != null && excludeParentFields && parentCT.Fields.ContainsField(field.GetAttribute("Name")))
 201:                 continue;
 202:  
 203:             XmlElement fieldRefElement = xmlDoc.CreateElement("FieldRef");
 204:             fieldRefElement.SetAttribute("ID", field.GetAttribute("ID"));
 205:  
 206:             string name = field.GetAttribute("Name");
 207:             if (name.Contains(ENCODED_SPACE) && removeEncodedSpaces)
 208:                 name = name.Replace(ENCODED_SPACE, string.Empty);
 209:  
 210:             fieldRefElement.SetAttribute("Name", name);
 211:             fieldRefsElement.AppendChild(fieldRefElement);
 212:         }
 213:         xmlDoc.DocumentElement.RemoveChild(xmlDoc.SelectSingleNode("//Fields"));
 214:  
 215:         xmlWriter.WriteString("\r\n");
 216:         xmlWriter.WriteRaw(xmlDoc.OuterXml);
 217:     }
 218:  
 219:     /// <summary>
 220:     /// Gets the list bindings.
 221:     /// </summary>
 222:     /// <param name="web">The web.</param>
 223:     /// <param name="contentTypes">The content types.</param>
 224:     /// <param name="xmlWriter">The XML writer.</param>
 225:     private static void GetListBindings(SPWeb web, List<SPContentType> contentTypes, XmlTextWriter xmlWriter)
 226:     {
 227:         foreach (SPList list in web.Lists)
 228:         {
 229:             foreach (SPContentType listCT in list.ContentTypes)
 230:             {
 231:                 foreach (SPContentType ct in contentTypes)
 232:                 {
 233:                     //if ((listCT.Scope != ct.Scope && listCT.Parent.Id == ct.Id) || listCT.Id == ct.Id)
 234:                     if (listCT.Name == ct.Name && listCT.Group == ct.Group)
 235:                     {
 236:                         xmlWriter.WriteStartElement("ContentTypeBinding");
 237:                         xmlWriter.WriteAttributeString("ContentTypeId", ct.Id.ToString());
 238:                         xmlWriter.WriteAttributeString("ListUrl", list.RootFolder.ServerRelativeUrl);
 239:                         xmlWriter.WriteEndElement(); // ContentTypeBinding
 240:                     }
 241:                 }
 242:             }
 243:         }
 244:  
 245:         foreach (SPWeb subWeb in web.Webs)
 246:         {
 247:             try
 248:             {
 249:                 GetListBindings(subWeb, contentTypes, xmlWriter);
 250:             }
 251:             finally
 252:             {
 253:                 subWeb.Dispose();
 254:             }
 255:         }
 256:     }
 257: }
 258:  
The syntax of the command can be seen below:
C:\>stsadm -help gl-exportcontenttypes

stsadm -o gl-exportcontenttypes


Exports Content Types to an XML file.

Parameters:
        -url <url containing the content types>
        -outputfile <file to output results to>
        [-name <name of an individual content type to export>]
        [-group <content type group name to filter results by>]
        [-listname <name of a list to export content types from>]
        [-includelistbindings]
        [-includefielddefinitions]
        [-excludeparentfields]
        [-removeencodedspaces (removes '_x0020_' in field names)]
Here's an example of how to dump the XMl for all the content types in a particular group:
stsadm -o gl-exportcontenttypes -url "http://intranet" -outputfile c:\contentTypes.xml -group "Custom Content Types" -includelistbindings -includefielddefinitions

21 comments:

Luca said...

Great job Gary!
When do you plan to include importcontenttypes command?

Gary Lapointe said...

I don't currently have any plans to create one but I've given it some thought - it wouldn't take a whole lot of effort. If I do it I'll want to handle workflows and other items associated with the content type which will add a lot of time to it - I just don't have a whole lot of time at the moment :)

John Christian said...

This is really cool, thanks for the hard work. I am somewhat of an SP newbie and I was hoping it wouldn't take much transformation to make the output from this into the correct input for a feature. A lot of it looks right. What do you think it would take? I might could even create some XSLT that would do the trick.

Gary Lapointe said...

John - I use the export content types command for Feature development all the time (I will typically create my site columns and content types via the browser and then use this to export - this is the whole reason I created the command). Typically I only ever have to modify the fields and even then only just a little - there's usually just a couple of attributes that need to be pulled and then if you want to use resource files you have to add all that in.

Anonymous said...

Gary, thanks for creating this, but I'm having a problem deploying it. When I run stsadm.exe -o addsolution -filename Lapointe.SharePoint.STS.Commands.wsp to install it, I'm getting an 'Object reference not set to an instance of an object' error. Any ideas?

Gary Lapointe said...

I've not seen that error when trying to add the solution before. Try pasting the following into a batch file and run that from folder where the WSP is saved:

SET SOLUTION_NAME="Lapointe.SharePoint.STSADM.Commands.wsp"
SET STSADM="C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\bin\stsadm"
%STSADM% -o addsolution -filename %SOLUTION_NAME%
%STSADM% -o deploysolution -local -allowgacdeployment -name %SOLUTION_NAME%


If you still have issues just change the WSP extension to .CAB and put the dll in the GAC and the XML files in the 12\config folder (that's all the WSP is doing - but by using a solution to deploy it will deploy to the whole farm rather than just the one server).

Elhadi said...

Hi Gary

First, thank you for all

Please, can you give us some guidelines on how importing a content type.
For example, is there an equivalent for the "shortcut" SPFiledCollection.AddFieldAsXml() which is used to import site columns ?

Or must we inevitably parse the exported XML, create an SPContentType Object, and affect their properties one by one ?

Gary Lapointe said...

There is a method that will do what you want but it's marked as internal so technically we can't use it. Your best bet is to put it in a feature and deploy the feature - otherwise you'll have to programmatically add all the various artifacts (field links, event receivers, etc.).

Kate Kneafsey said...

I've installed and deployed your solution (which looks great, by the way). But when I try to export a list, I am getting the message for each View page that "A web part or Web form control type could not be found or is not registered as safe. The web part will still be exported". I am logged on as the site admin account.

But when I try to import it to another web application, I get teh message "Warning: Could not find web part -Unknown: #####".

This is just the basic Announcements list that I used to test. Do you know what I am doing wrong?

Thanks, I really need these commands.

Vincent said...

Hi,

Thanx for the great commands.

I've got a problem with the expot of content types, it sets to many values in the XML.

When i install the exported xml i've got errors for the following properties:
Customization
Aggregation
Version
WebId
UserSelectionMode



Maybe you can fix this, in the meanwhile i will have to replace them all by hand.

Gary Lapointe said...

Vincent - use the -featuresafe parameter. It addresses those issues.

Willy said...

Hello Gary, how can i deploy all content types exported to the .xml file?

Gary Lapointe said...

Use the exported XML withing a custom Feature.

Willy said...

Hello again Gary,

Can you give me a guideline to make a feature to install the content types in the XML file?, i dont know what is the correct way...

Regards.

Gary Lapointe said...

This should get you started: http://www.sharepointnutsandbolts.com/2007/04/deploying-content-types-as-feature.html

peace said...

Hi Gary,
We have to move custom content types from one box to another while preserving their GUIDs as there are lot of applications referring to these content types via their GUID.
Does your exportcontenttype preserves the GUID? iF yes, then have u created import content type :))...and if no, then can you suggest me a way to achieve this

Gary Lapointe said...

Right now I don't have an import but if you're within the same farm then you can use the copy command - otherwise your best bet would be to use features.

tripwire said...

Please, please won't you build an import command?? I'm begging you, Gary! :D

Asmodeux said...

I'm in the same situation. We're developing a tool to help us manage our sharepoint environnements and the "import content type" is like the holy grail to us. But we have no clue how to Import them from the Xml exported.

Do you have any hint on how?

Gary Lapointe said...

Your best bet is to turn the exported xml into a feature which can then be deployed and activated selectively.

gbelzile said...

I'm missing many fields from my content types, have you ever experienced that? They appeared in the list of field definitions but not as ref in content types. I did it by hand but I wondered if it happened to someone else.
The tool still saved me a lot of time! Thanks a lot for that!