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.

Thursday, August 23, 2007

Copy Content Types

This turned out to be a lot more challenging than I was expecting it to be. Duplicating a content type required that I learn a lot about how to programmatically create things like site columns and workflows associations and policies. I also had to do a lot of reverse engineering to figure out seemingly simple things such as how the document information panel and document templates are stored.

In some cases copying elements became as simple as adding xml to a collection or adding an object from the source collection to the target with basically no real logic needed. In other cases I had to painstakingly reconstruct the original element - considering the limited documentation on some of this stuff I'm not convinced it could have been done without Reflector - Microsoft does some rather odd things that I can't seem to find documented anywhere.

I think this command, perhaps more than some of the others, has potential for the most use post deployment. If you've got several site collections and content types that you need duplicated across those site collections then you'll want to check this out - it's a real pain to have to recreate a content type more than once (it's so easy to miss something when you consider all the custom columns, policies, workflows, templates, etc.).  Of course if you built your content types using Features then you wouldn't have this issue but I know that there are many that do not :)

The nice thing about this command is that it can be used to copy all content types from one site collection to another or just a single content type. The code to accomplish this consists of 7 key methods - the primary method, CreateContentType() creates the content type itself based on the source content type. This method then calls out to the remaining six methods to add peripheral items such as workflows and templates. To create a new content type you have to make sure that the parent content type exists first - the CreateContentType method does a recursive call to address the Parent and then once all the parent types exist it will set the basic properties for the new content type.

   1: private static void CreateContentType(string targetUrl, SPContentType sourceCT, SPFieldCollection sourceFields, bool verbose)
   2: {
   3:     // Make sure any parent content types exist - they have to be there before we can create this content type.
   4:     if (availableTargetContentTypes[sourceCT.Parent.Id] == null)
   5:     {
   6:         Log(string.Format("Progress: Parent of content type '{0}' does not exist - creating...", sourceCT.Name));
   7:  
   8:         CreateContentType(targetUrl, sourceCT.Parent, sourceFields, verbose);
   9:  
  10:         // Reset the fields and content types.
  11:         GetAvailableTargetContentTypes(verbose, targetUrl);
  12:     }
  13:  
  14:     Log(string.Format("Progress: Creating content type '{0}'...", sourceCT.Name));
  15:  
  16:     // Create a new content type using information from the source content type.
  17:     SPContentType newCT = new SPContentType(availableTargetContentTypes[sourceCT.Parent.Id], targetContentTypes, sourceCT.Name);
  18:  
  19:     Log(string.Format("Progress: Setting fields for content type '{0}'...", sourceCT.Name));
  20:  
  21:     // Set all the core properties for the content type.
  22:     newCT.Group = sourceCT.Group;
  23:     newCT.Hidden = sourceCT.Hidden;
  24:     newCT.NewDocumentControl = sourceCT.NewDocumentControl;
  25:     newCT.NewFormTemplateName = sourceCT.NewFormTemplateName;
  26:     newCT.NewFormUrl = sourceCT.NewFormUrl;
  27:     newCT.ReadOnly = sourceCT.ReadOnly;
  28:     newCT.RequireClientRenderingOnNew = sourceCT.RequireClientRenderingOnNew;
  29:     newCT.Description = sourceCT.Description;
  30:     newCT.DisplayFormTemplateName = sourceCT.DisplayFormTemplateName;
  31:     newCT.DisplayFormUrl = sourceCT.DisplayFormUrl;
  32:     newCT.EditFormTemplateName = sourceCT.EditFormTemplateName;
  33:     newCT.EditFormUrl = sourceCT.EditFormUrl;
  34:  
  35:     string xml = (string)Utilities.GetFieldValue(newCT, "m_strXmlNonLocalized");
  36:     xml = xml.Replace(string.Format("ID=\"{0}\"", newCT.Id), string.Format("ID=\"{0}\"", sourceCT.Id));
  37:     Utilities.SetFieldValue(newCT, typeof (SPContentType), "m_strXmlNonLocalized", xml);
  38:     Utilities.SetFieldValue(newCT, typeof(SPContentType), "m_id", sourceCT.Id);
  39:  
  40:     Log(string.Format("Progress: Adding content type '{0}' to collection...", sourceCT.Name));
  41:  
  42:     // Add the content type to the content types collection and update all the settings.
  43:     targetContentTypes.Add(newCT);
  44:     newCT.Update();
  45:  
  46:     // Add all the peripheral items
  47:  
  48:     try
  49:     {
  50:         if (copyColumns)
  51:         {
  52:             Log(string.Format("Progress: Adding site columns for content type '{0}'...", sourceCT.Name));
  53:  
  54:             AddSiteColumns(newCT, sourceCT, sourceFields, verbose);
  55:         }
  56:  
  57:         if (copyWorkflows)
  58:         {
  59:             Log(string.Format("Progress: Adding workflow associations for content type '{0}'...", sourceCT.Name));
  60:  
  61:             AddWorkflowAssociations(newCT, sourceCT, verbose);
  62:         }
  63:  
  64:         if (copyDocTemplate)
  65:         {
  66:             Log(string.Format("Progress: Adding document template for content type '{0}'...", sourceCT.Name));
  67:  
  68:             AddDocumentTemplate(newCT, sourceCT);
  69:         }
  70:  
  71:         if (copyDocConversions)
  72:         {
  73:             Log(string.Format("Progress: Adding document conversion settings for content type '{0}'...", sourceCT.Name));
  74:  
  75:             AddDocumentConversionSettings(newCT, sourceCT);
  76:         }
  77:  
  78:         if (copyPolicies)
  79:         {
  80:             Log(string.Format("Progress: Adding information rights policies for content type '{0}'...", sourceCT.Name));
  81:  
  82:             AddInformationRightsPolicies(newCT, sourceCT, verbose);
  83:         }
  84:  
  85:         if (copyDocInfoPanel)
  86:         {
  87:             Log(string.Format("Progress: Adding document information panel for content type '{0}'...", sourceCT.Name));
  88:  
  89:             AddDocumentInfoPanelToContentType(sourceCT, newCT);
  90:         }
  91:     }
  92:     finally
  93:     {
  94:         newCT.ParentWeb.Site.Dispose();
  95:         newCT.ParentWeb.Dispose();
  96:     }
  97: }

After creating the content type and saving it the code then adds the other elements according to flags that can be passed in (the default is to copy everything but you can turn off individual items). The first thing I do is add any needed columns. A content type doesn't actually store any details about columns (or fields as they are referred to in code) rather it stores a link to the columns. The columns themselves are part of the Fields collection which you can get from an SPWeb object. So the first step in setting the columns for the content type is to create the columns if they don't already exist and then link that column to the content type:

   1: private static void AddSiteColumns(SPContentType targetCT, SPContentType sourceCT, SPFieldCollection sourceFields, bool verbose)
   2: {
   3:     // Store the field order so that we can reorder after adding all the fields.
   4:     List<string> fields = new List<string>();
   5:     foreach (SPField field in sourceCT.Fields)
   6:     {
   7:         if (!field.Hidden && field.Reorderable)
   8:             fields.Add(field.InternalName);
   9:     }
  10:     // Add any columns associated with the content type.
  11:     foreach (SPFieldLink field in sourceCT.FieldLinks)
  12:     {
  13:         // First we need to see if the column already exists as a Site Column.
  14:         SPField sourceField;
  15:         try
  16:         {
  17:             // First try and find the column via the ID
  18:             sourceField = sourceFields[field.Id];
  19:         }
  20:         catch
  21:         {
  22:             try
  23:             {
  24:                 // Couldn't locate via ID so now try the name
  25:                 sourceField = sourceFields[field.Name];
  26:             }
  27:             catch
  28:             {
  29:                 sourceField = null;
  30:             }
  31:         }
  32:         if (sourceField == null)
  33:         {
  34:             // Couldn't locate by ID or name - it could be due to casing issues between the linked version of the name and actual field
  35:             // (for example, the Contact content type stores the name for email differently: EMail for the field and Email for the link)
  36:             foreach (SPField f in sourceCT.Fields)
  37:             {
  38:                 if (field.Name.ToLowerInvariant() == f.InternalName.ToLowerInvariant())
  39:                 {
  40:                     sourceField = f;
  41:                     break;
  42:                 }
  43:             }
  44:         }
  45:         if (sourceField == null)
  46:         {
  47:             Log(string.Format("WARNING: Unable to add column '{0}' to content type.", field.Name));
  48:             continue;
  49:         }
  50:  
  51:         if (!targetFields.ContainsField(sourceField.InternalName))
  52:         {
  53:             Log(string.Format("Progress: Adding column '{0}' to site columns...", sourceField.InternalName));
  54:  
  55:             // The column does not exist so add the Site Column.
  56:             targetFields.Add(sourceField);
  57:         }
  58:  
  59:         // Now that we know the column exists we can add it to our content type.
  60:         if (targetCT.FieldLinks[sourceField.InternalName] == null) // This should always be true if we're here but I'm keeping it in as a safety check.
  61:         {
  62:             Log(string.Format("Progress: Associating content type with site column '{0}'...", sourceField.InternalName));
  63:             
  64:             // Now add the reference to the site column for this content type.
  65:             try
  66:             {
  67:                 targetCT.FieldLinks.Add(field);
  68:             }
  69:             catch (Exception ex)
  70:             {
  71:                 Log("WARNING: Unable to add field '{0}' to content type: {1}", sourceField.InternalName, ex.Message);
  72:             }
  73:         }
  74:     }
  75:     // Save the fields so that we can reorder them.
  76:     targetCT.Update(true);
  77:  
  78:     // Reorder the fields.
  79:     try
  80:     {
  81:         targetCT.FieldLinks.Reorder(fields.ToArray());
  82:     }
  83:     catch
  84:     {
  85:         Log("WARNING: Unable to set field order.");
  86:     }
  87:     targetCT.Update(true);
  88: }

After adding the columns I add the workflows (note that adding these peripheral items can be done in any order as long as the content type has been saved and therefore belongs to a content type collection). When I first started researching how to do this I thought I was going to have to do a whole lot of work to recreate the workflow (based on what I was seeing in Reflector and how Microsoft was handling the creation of workflows for a content type). On a whim I decided to simply try adding a workflow association object (SPWorkflowAssociation) directly to the targets WorkflowAssociations collection and to my surprise it worked perfectly. Only catch was that I had to make sure I cleared out any existing workflows on the target (the default items that are added behind the scenes when you create a content type):

   1: private static void AddWorkflowAssociations(SPContentType targetCT, SPContentType sourceCT, bool verbose)
   2: {
   3:     // Remove the default workflows - we're going to add from the source.
   4:     while (targetCT.WorkflowAssociations.Count > 0)
   5:     {
   6:         targetCT.RemoveWorkflowAssociation(targetCT.WorkflowAssociations[0]);
   7:     }
   8:  
   9:     // Add workflows.
  10:     foreach (SPWorkflowAssociation wf in sourceCT.WorkflowAssociations)
  11:     {
  12:         Log(string.Format("Progress: Adding workflow '{0}' to content type...", wf.Name));
  13:  
  14:         targetCT.AddWorkflowAssociation(wf);
  15:     }
  16:     targetCT.Update();
  17: }

Adding the document templates was another one that I thought was going to be a lot more difficult but in the end turned out pretty easy. Document templates (and custom document information panels) are stored in the resource folder which is located at "http://yourdomain/yoursitecollection/_cts/yourcontenttypename/". The SPContentType object has a ResourceFolder property which contains all the documents located here. To copy the template all I had to do was get a reference to the SPFile object returned by the ResourceFolder.Files collection and then add it to my targets ResourceFolder.Files collection:

   1: private static void AddDocumentTemplate(SPContentType targetCT, SPContentType sourceCT)
   2: {
   3:     if (string.IsNullOrEmpty(sourceCT.DocumentTemplate))
   4:         return;
   5:  
   6:     // Add the document template.
   7:     SPFile sourceFile = null;
   8:     try
   9:     {
  10:         sourceFile = sourceCT.ResourceFolder.Files[sourceCT.DocumentTemplate];
  11:     }
  12:     catch (ArgumentException) {}
  13:     if (sourceFile != null && !string.IsNullOrEmpty(sourceFile.Name))
  14:     {
  15:         SPFile targetFile = targetCT.ResourceFolder.Files.Add(sourceFile.Name, sourceFile.OpenBinary(), true);
  16:         targetCT.DocumentTemplate = targetFile.Name;
  17:         targetCT.Update();
  18:     }
  19:     else
  20:     {
  21:         targetCT.DocumentTemplate = sourceCT.DocumentTemplate;
  22:         targetCT.Update();
  23:     }
  24: }

Adding the document conversion settings turned out to be only marginally more difficult than adding the document template settings. When I first started researching how to do this I was expecting an object for which I could set various properties. Unfortunately (and fortunately as it turned out) I was wrong. Microsoft effectively breaks the pattern here (as well as with the document information panel) by choosing to use an XmlDocument object to store the settings. This threw me for a loop when trying to figure out how to do this but the result was that I could easily copy the settings by simply copying the XML from one object to another.

The SPContentType object has an XmlDocuments property which is a collection of documents that contain settings for certain services. There are two documents that we need here - one is named "urn:sharePointPublishingRcaProperties" and the other is named "http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers". The first stores the settings for any document conversions that are set up. The second contains all the converters that are excluded (doesn't really make sense to me to do it this way but whatever). So all I had to do was add these two documents to my target content type:

   1: private static void AddDocumentConversionSettings(SPContentType targetCT, SPContentType sourceCT)
   2: {
   3:     // Add document conversion settings if document converisons are enabled.
   4:     // ParentWeb and Site will be disposed later.
   5:     if (targetCT.ParentWeb.Site.WebApplication.DocumentConversionsEnabled)
   6:     {
   7:         // First, handle the xml that describes what is enabled and each setting
   8:         string sourceDocConversionXml = sourceCT.XmlDocuments["urn:sharePointPublishingRcaProperties"];
   9:         if (sourceDocConversionXml != null)
  10:         {
  11:             XmlDocument sourceDocConversionXmlDoc = new XmlDocument();
  12:             sourceDocConversionXmlDoc.LoadXml(sourceDocConversionXml);
  13:  
  14:             targetCT.XmlDocuments.Delete("urn:sharePointPublishingRcaProperties");
  15:             targetCT.XmlDocuments.Add(sourceDocConversionXmlDoc);
  16:         }
  17:         // Second, handle the xml that describes what is excluded (disabled).
  18:         sourceDocConversionXml =
  19:             sourceCT.XmlDocuments["http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers"];
  20:         if (sourceDocConversionXml != null)
  21:         {
  22:             XmlDocument sourceDocConversionXmlDoc = new XmlDocument();
  23:             sourceDocConversionXmlDoc.LoadXml(sourceDocConversionXml);
  24:  
  25:             targetCT.XmlDocuments.Delete("http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers");
  26:             targetCT.XmlDocuments.Add(sourceDocConversionXmlDoc);
  27:         }
  28:         targetCT.Update();
  29:     }
  30: }

Setting the information rights policies was another one that took me a while - mainly because some of the core classes that Microsoft uses to do this have been obfuscated so I couldn't disassemble them. In the end the solution once again turned out pretty simple - just took me a while to get there. To copy the policies I had to create a PolicyCatalog object which I then use to return a PolicyCollection object. If the policy doesn't already exist in that collection then I export the policy from the source policy (which I got by calling the static GetPolicy() method of the Policy object).

The export method returns an XmlDocument object which I then use add to my target site collection by using the static PolicyCollection.Add() method. Once the policy exist at the site collection level then I can associate it with the content type by calling Policy.CreatePolicy and passing in my Policy object that I got from the source (the method name is a bit confusing as it's not actually creating a policy - it's just assocating a policy with your content type):

   1: private static void AddInformationRightsPolicies(SPContentType targetCT, SPContentType sourceCT, bool verbose)
   2: {
   3: #if MOSS
   4:     // Set information rights policy - must be done after the new content type is added to the collection.
   5:     using (Policy sourcePolicy = Policy.GetPolicy(sourceCT))
   6:     {
   7:         if (sourcePolicy != null)
   8:         {
   9:             PolicyCatalog catalog = new PolicyCatalog(targetCT.ParentWeb.Site);
  10:             PolicyCollection policyList = catalog.PolicyList;
  11:  
  12:             Policy tempPolicy = null;
  13:             try
  14:             {
  15:                 tempPolicy = policyList[sourcePolicy.Id];
  16:                 if (tempPolicy == null)
  17:                 {
  18:                     XmlDocument exportedSourcePolicy = sourcePolicy.Export();
  19:                     try
  20:                     {
  21:                         Log(string.Format("Progress: Adding policy '{0}' to content type...", sourcePolicy.Name));
  22:  
  23:                         PolicyCollection.Add(targetCT.ParentWeb.Site, exportedSourcePolicy.OuterXml);
  24:                     }
  25:                     catch (Exception ex)
  26:                     {
  27:                         if (ex is NullReferenceException || ex is SEHException)
  28:                             throw;
  29:                         // Policy already exists
  30:                         Log("ERROR: An error occured creating the information rights policy: {0}", ex.Message);
  31:                     }
  32:                 }
  33:                 Log(string.Format("Progress: Associating content type with policy '{0}'...", sourcePolicy.Name));
  34:                 // Associate the policy with the content type.
  35:                 Policy.CreatePolicy(targetCT, sourcePolicy);
  36:             }
  37:             finally
  38:             {
  39:                 if (tempPolicy != null)
  40:                     tempPolicy.Dispose();
  41:             }
  42:         }
  43:         targetCT.Update();
  44:     }
  45: #endif
  46: }

The last piece I had to address was the document information panel. This was hands down the most annoying to work on. I ended up grabbing a lot of code from Reflector in order to pull this off - mainly the code necessary to construct the resultant XmlDocument object that stores all the settings as well as the code to read those settings out of the source content type. I couldn't just copy the XML directly as the values needed to be different based on the different URLs of the target and source. The XML of interest can be retrieved using the XmlDocuments property collection and passing in the name http://schemas.microsoft.com/office/2006/metadata/customXsn.

Once you have this you simply have to manipulate the values or construct a new doc as I did (I won't show that code here as it's a bit of a mess considering it's practically a straight copy and paste from Reflector). Beyond setting the XML settings you also have to copy the template file itself. This is actually very similar to what was needed for the document template:

   1: private static void AddDocumentInfoPanelToContentType(SPContentType sourceCT, SPContentType targetCT)
   2: {
   3:     XmlDocument sourceXmlDoc = null;
   4:     string sourceXsnLocation;
   5:     bool isCached;
   6:     bool openByDefault;
   7:     string scope;
   8:  
   9:     // We first need to get the XML which describes the custom information panel.
  10:     string str = sourceCT.XmlDocuments["http://schemas.microsoft.com/office/2006/metadata/customXsn"];
  11:     if (!string.IsNullOrEmpty(str))
  12:     {
  13:         sourceXmlDoc = new XmlDocument();
  14:         sourceXmlDoc.LoadXml(str);
  15:     }
  16:     if (sourceXmlDoc != null)
  17:     {
  18:         // We found settings for a custom information panel so grab those settings for later use.
  19:         XmlNode node;
  20:         string innerText;
  21:         XmlNamespaceManager nsmgr = new XmlNamespaceManager(sourceXmlDoc.NameTable);
  22:         nsmgr.AddNamespace("cust", "http://schemas.microsoft.com/office/2006/metadata/customXsn");
  23:         sourceXsnLocation = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:xsnLocation", nsmgr).InnerText;
  24:         node = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:cached", nsmgr);
  25:         isCached = (node != null) && (node.InnerText == bool.TrueString);
  26:         innerText = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:openByDefault", nsmgr).InnerText;
  27:         openByDefault = !string.IsNullOrEmpty(innerText) && (innerText == bool.TrueString);
  28:     }
  29:     else
  30:         return;
  31:  
  32:     // This should never be null but just in case...
  33:     if (sourceXsnLocation == null)
  34:         return;
  35:  
  36:     // Grab the source file and add it to the target resource folder.
  37:     SPFile sourceFile = null;
  38:     try
  39:     {
  40:         sourceFile = sourceCT.ResourceFolder.Files[sourceXsnLocation];
  41:     }
  42:     catch (ArgumentException)
  43:     {
  44:     }
  45:     if (sourceFile != null)
  46:     {
  47:         SPFile file2 = targetCT.ResourceFolder.Files.Add(targetCT.ParentWeb.Url + "/" + sourceFile.Url, sourceFile.OpenBinary(), true);
  48:  
  49:         // Get the target and scope to use in the xsn for the custom information panel.
  50:         string targetXsnLocation = targetCT.ParentWeb.Url + "/" + file2.Url;
  51:         scope = targetCT.ParentWeb.Site.MakeFullUrl(targetCT.Scope);
  52:  
  53:         XmlDocument targetXmlDoc = BuildCustomInformationPanelXml(targetXsnLocation, isCached, openByDefault, scope);
  54:         // Delete the existing doc so that we can add the new one.
  55:         targetCT.XmlDocuments.Delete("http://schemas.microsoft.com/office/2006/metadata/customXsn");
  56:         targetCT.XmlDocuments.Add(targetXmlDoc);
  57:     }
  58:     targetCT.Update();
  59: }

What's interesting about many pieces of the code above is that they can easily be extracted out and used to copy other elements such as policies and site columns. I expect I'll eventually do just that as I can see the need to have these items copied across site collections (replication is something that I don't even want to think about as it introduces so many issues when dealing with content that is already consuming these elements - perhaps if the issue comes up I'll look into it). The syntax of the command can be seen below:

C:\>stsadm -help gl-copycontenttypes

stsadm -o gl-copycontenttypes

Copies all Content Types from one gallery to another.

Parameters:
        -sourceurl <site collection url containing the source content types>
        -targeturl <site collection url where the content types will be copied to>
        [-sourcename <name of an individual content type to copy - if ommitted all content types are copied if they don't already exist>
        [-noworkflows]
        [-nocolumns]
        [-nodocconversions]
        [-nodocinfopanel]
        [-nopolicies]
        [-nodoctemplate]

Here’s an example of how to copy all the content types from one site collection to another without copying document templates:

stsadm –o gl-copycontenttypes –sourceurl "http://intranet/operations" -targeturl "http://intranet/hr" -nodoctemplate

Update 11/26/2007: I've fixed a bug with copying site columns. I had omitted an Update() call after copying the columns (it worked for me during testing because later calls were calling Update but for certain types of content types this wasn't happening because there was nothing to do in the later calls). I also fixed null argument issue when copying content types that are not based on documents (so no document template is present). Thanks to Chris Rivera for helping me find these.

70 comments:

Anonymous said...

This is awesome!!! you have saved me weeks of work....

Chris Rivera said...

Very nice. I need to copy content types from a subsite to the top level which I assume I can do with your code. Anyway I can get the exe for this? Thanks,
Chris (chrisgoblue@gmail.com)

Gary Lapointe said...

Thanks for the feedback. There's actually no exe for the extensions. You can download the code which contains the dll and xml files in the Package folder (the link is in the right nav bar). You then need to GAC the dll and copy the xml file to C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\CONFIG. Once you've done this you can use the out of the box stsadm executable to run the commands.

Tim said...

Gary - should there be any need to sign the DLL with a strong key before dragging it to the GAC?

Gary Lapointe said...

The dll should already be signed. You can't GAC a dll unless it has been signed.

Tim said...

Gary - sorry for my ignorance, what's the easiest way to sign the DLL? From the downloadable package the DLL won't drag into the GAC.

Tim

Gary Lapointe said...

No worries - the dll is already signed so you don't have to worry about that. If you're having trouble dragging into the GAC then I'd suggest just using gacutil (gacutil -i "Edfinancial.SharePoint.STSADM.Commands.dll". I've found that there are some scenarios where dragging into the GAC doesn't work (for instance, if you drag in using a single explorer instance it won't work - try using two different explorer instances, also make sure that the file is on a local drive and not a network share).

Tim said...

Thx - I'll get back in there and find out what the issue is. I find 99% of my time is spent finding that illusive gremlin and what should be monstrous tasks seem to disolve before my eyes...!

dan a. said...

Gary, great stuff you've created - I can see some of it saving a ton of time. I'm having an issue copying from http://siteA to http://siteB - the command is returning that http://siteA cannot be found.

http://siteA is on a different box from where I am running stsadm, while siteB is local. I'm logged into windows with an account that has full privs on both boxes in terms of sharepoint, the os, and the databases.

Is there a problem that would prohibit stsadm from "finding" siteA when running from a box containing siteB? I'm stuck scratching my head since in theory everything should be fine.

Gary Lapointe said...

There may be a way around it but as far as I understand it you cannot talk cross farms using SPSite (which is the API object that is used to load up the site). One way around it - use import/export to bring SiteA over to SiteB's farm and then copy the content type from there - once done, delete the temp site.

Pierrick said...

First, thanks a lot for your brilliant work. I use your extensions everyday.

Apparently though, this command doesn't replicate the Version attribute of my content types columns (they're at 0 on my target site).
I wouldn't mind, however it generates a "The object has been updated by another user since it was last fetched" error when someone tries to update one of those site columns.
I just have to put the proper Version attribute right in the database and it works well afterwards.

Do you think you could extract that property and set it in the xml flow ? I checked on the msdn and it seems the SPContentType.Version property is read only :(

Hope it helps, thanks again.

Gary Lapointe said...

Pierrick - is the issue with the SPContentType object or the SPField objects? It's easy enough to set the version to something explicit using reflection but I can't tell from your post which is the issue (I don't really have time to play with it right now in order to figure it out on my own).

javier said...

Gary i have problems with my content types that inherits from another content types: I have content type "a" that inherits from content type "parent" and content type "b" that inherits from the same content type "parent". If i copy the content type "a" all works ok but whe i copy the content type "b" i had an error: "A duplicate name conten "parent" was found...
Thanks

Gary Lapointe said...

Javier - thanks for the info - the problem was that I have no way to directly set the content type ID when I create the content type (not sure why MSFT didn't allow that). So I added a couple of calls using reflection to simply set the content type ID manually so now when the content types get copied it will preserve the original IDs which it was not previously doing. Hate that I had to use reflection for this but it's the only way. I've pushed out a new build - let me konw if you have any issues with it.

benz said...

Thanks Gary - worked great in the test build, but on the production system I'm having problems as some site columns and parent document types already exist resulting in duplicate name errors.

eg I have a parent type "ParentDoc" in both systems, I then develop a "ChildDocA" in the dev environment and want to port over, as "ParentDoc" already exists the process fails. A similar thing happens if site columns already exist in both environments.

Any suggestions on how to work around this?
Thanks,
Ben

Gary Lapointe said...

Ben - sorry for not getting back to you sooner - take a look at my previous comment - I believe I've fixed the issue with parent content types but I'll have to look into the site column issue.

fromonesource said...

Gary,

This utility seems to be very handy. I am having issues off the bat however. Several content types are not being copied from http://dev.loc to http://dev.loc/hr

When I specify the content type using -sourcename, the missing content types will come over but without the information management policy settings. Please help if you can.

Gary Lapointe said...

Can you give me some more info? What is the command you are running? How many content types are there (can you give me the names of the ones that are failing)? What groups are they in? Are you getting any sort of errors (I'm wondering if it's actually just failing on one and therefore not continuing with the others)? Is there anything in the log file? The information rights policy stuff is some of the last pieces to get copied so it's possible it's erroring out and causing the rest to fail (or possibly the doc converter stuff is erroring).

fromonesource said...

Gary,

I have tried this command

stsadm -o gl-copycontenttypes -sourceurl http://dev.site.com -targeturl http://dev.site.com/it

but this will only copy some of the content types, none of the custom ones and none of the custom groups as you can see in screenshots below...

Here are the source content types BEFORE gl-copycontenttypes: http://screencast.com/t/YX2NhU93VSS

Here are the destination content types BEFORE gl-copycontenttypes: http://screencast.com/t/aTQ7VyvcaRA

Here are the destination content types AFTER gl-copycontenttypes: http://screencast.com/t/c6cFW7dkXla

That failed to copy the custom content types such as "Corporate Benefits - Appeals" and I did not get any errors in the command line. "Operation completed successfully". Where should I look for the log file?

Next I tried using the -sourcename parameter:

stsadm -o gl-copycontenttypes -sourceurl http://dev.site.com -targeturl http://dev.site.com/it -sourcename "Corporate Benefits - Appeals"

Here is the specific source content type that I copied (on http://dev.site.com): http://screencast.com/t/bRlT5Caw1

Here is that content type's information management policy (on http://dev.site.com): http://screencast.com/t/HFFCxkBJjj

When I run the command again and specify sourcename, the content type, group and columns copy over but not the information management policy...

Destination content types AFTER sourcename parameter: http://screencast.com/t/KeiibMeq2

Specific content type summary AFTER sourcename parameter: http://screencast.com/t/Q1sjeZEfn

Specific content type information management policy settings: http://screencast.com/t/3HIVmckO

There is one thing odd about this last screenshot which may be an indicator for you. "Use a site collection policy" is selected but disabled when copied over. We are not using site collection policies but rather defining the policies on the individual content types.

I hope this information helped. If you want to talk one on one, this account is my email at gmail. Thanks!

anil said...

as i use the code to copy the content type throughout the site collection,Title column disappers from all the site collection.Is there is any way to restore them

Gary Lapointe said...

That's a new one - are you sure the title column is gone and not just hidden?

Asmodeux said...

I feel ignorant. I'm trying to install the wsp package and can't manage to do it. I installed and deployed the solution: the feature was not there. Then registered the dll in the gac: the feature still not there. I copied the xl files into a new directory in features then tried the installfeature and activate feature... I get "unable to cast object of type "microsoft.SharePoint.Administration.SPSolution" to type "Microsoft.SharePoint.Administration.SPFeatureDefinition"

Do you have a script to install this feature.

Thanks?

Gary Lapointe said...

Run the following stsadm commands to deploy the solution:

stsadm -o addsolution -filename "Lapointe.SharePoint.STSADM.Commands.wsp"
stsadm -o deploysolution -local -allowgacdeployment -name "Lapointe.SharePoint.STSADM.Commands.wsp"
stsadm -o execadmsvcjobs

After running these you should see the all my commands when you do an
"stsadm -help" command. Nothing goes into the Features folder so you won't see anything there.

fromonesource said...

I realized why the policies were not copying. They were defined on the content type. This extension only copies site collection policies.

Is there a way to overwrite an existing content type? If a change is made to a CT in SiteCollectionA and needs to be replicated to SiteCollectionB, how can I overwrite the existing content type in SiteCollectionB?

Gary Lapointe said...

Not easily - take a look at the code for the gl-propagatecontenttype command I created - you'd have to adapt it to work with the content type found in another site collection - it's entirely possible to do you just have to be careful about breaking changes (specifically removal of fields).

philip said...

Hi,

I've tried moving content types from a site on one server to a site on another server with the help of your stsadm extensions.

The message I'm getting is "The web app at "http://server2/xxx" could not be found. Verify that you have typed the URL correctly. If the URL should be serving existing content, the system admin may need to add a new request URL mapping to the intended application"

Any ideas appreciated. Obviously both servers are on the domain and I can browse to sites on both. The old server was used as dev and the new one is live.

I've tried using a site template to copy everything across but as you know the site columns and content types are not copied.

Gary Lapointe said...

Philip - unfortunately the copy command only works within the context of a single farm - this is true with all of my commands (and the out of box commands as well).

Anonymous said...

I really like the idea of these extensions and couldn't wait to give them a try. I might misunderstand what is meant to happen though. The content types are copied but not the site columns which belonged to the content type in the source site. I tried using the list copy extension to get the columns in place first as well in a separate try but that didn't help.

Sorry, I'm anonymous right now (Jonathan), but will sign up right away.

Gary Lapointe said...

Jonathan - I'm not sure why the site columns aren't being copied -they should be getting copied (at least they were during all my tests that I did). You can try the gl-exportsitecolumns/gl-importsitecolumns commands to move the columns over first.

Daniel Adler said...

Great post! love your work.

Charles said...

Ran into an error in AddDocumentTemplate(...) -

It appears that the line
SPFile sourceFile = sourceCT.ResourceFolder.Files[sourceCT.DocumentTemplate];

will throw an exception if the code executes this far and the file doesn't actually exist.

This is like a number of the collections in the SharePoint OM - instead of returning null, they throw an Exception.

Given that the next line in the code checks for this variable to be null, I got around it by declaring the SPFile first, setting it null, and then assigning it that value in a small try-catch block. Not sure if this problem is specific to my environment or if others might encounter it, but I figured I'd pass it along.

Anonymous said...

Thank you kindly! It worked great :)

Gary Lapointe said...

Charles - this issue has been fixed - thanks for the feedback.

Steve P said...

I just wanted to let you know that we developed a graphical tool that amongst other things allows you to drag-and-drop content types between sites, sub-sites and even servers. MetaVis Architect is currently in free beta. You can download it at www.metavistech.com/beta

dirq said...

Thank you so much. You probably saved me a week of work (and maybe my job) :) Awesome! Yeah yeah!

dirq said...

It seems that there is a memory leak somewhere. We need to copy over around 1,200 fields and 1,200 content types - while we expected this to take some time we didn't expect it to use 700M of RAM. It just kept eating up more and more memory as the process went on. We had to eventually kill it. Just wondering if you had similar problems copying lots of fields and content types and if you'd know where to look to plug the leak. It probably wasn't designed to handle this kind of load...

Otherwise, thank you again for all of your hard work. Even after finding the leaks you have saved us a ton of time! :)

Gary Lapointe said...

I've made a couple of changes which should address the memory leak issue. The updated build will go out today. That being said - why on earth do you have 1200 content types? I think you have a bigger design issue here that needs to be solved.

fromonesource said...

Gary,
In our case we are using Content Types for record retention. We have a few hundred record retention policies and a CT for each policy. Would you suggest that we do not use CTs for this?

dirq said...

Gary –
Essentially, we are in the design stages of a project and the management team needs to have a large number of field-sets available to the user creating a site. We have a organizational hierarchy that everything resides in (Site > Department > Practice Group > Area of Law, etc.) and each level has some fields that inherit down to another level. So the site may have “Matter Name” and the department may have a field called “Department Chair” and then the Area of Law would have all of the fields above it.

Currently there is a site that holds all of the default content-types and fields and when a new site is created those objects need to be available – and thus, the massive copy operation.

We have gone over the requirements and are still planning on the same architecture of many levels of content types but are now interested in just bringing over the used content fields as-needed. There will be some extensive synchronization routines between the new site and the default site, but I think that it is feasible. Your code has definitely set us on the right track. Thank you.

Larry said...

Gary,
I have noticed that if my Source content type contains a Field Type of "User" or "Choice" that these fields were not created in the Target copy of the content type. Are there known limitations with various field types?

Gary Lapointe said...

Field types shouldn't matter - I know I've tested using Choice fields (can't remember about user fields) but it should work - I'll have to dig into it some - thanks for the info.

Ryan said...

Thanks for such a great set of tools!

One thing I have noticed while using the gl-copycontenttypes is that it does not appear to correctly copy the Document Template value when set to the URL of an existing document template.

All the best, Ryan

Gary Lapointe said...

Thanks for the info. I've got a fix for that and will try to get it published this week sometime.

Montanan Missing her Mountains said...

What is the issue if you get a dozen "the field is not accessible" errors when running cmd for specific customized content type?

tripwire said...

Hi Gary, you muyst really love me by now but I have the same error as Pierrick above and am unable to modify any site columns. :)

The object has been updated by another user since it was last fetched. at Microsoft.SharePoint.SPField.UpdateSchemaXmlInWeb(String strXml, Boolean bToggleSealed, Boolean bRichText, String strFmla, List`1 fldTitles, List`1 fids)
at Microsoft.SharePoint.SPField.UpdateInWeb(Boolean bToggleSealed)
at Microsoft.SharePoint.SPField.UpdateCore(Boolean bToggleSealed)
at Microsoft.SharePoint.SPField.Update()
at Microsoft.SharePoint.SPFieldMultiChoice.Update()
at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.UpdateFieldWithPropertiesFromXml(SPField fld, String strXml)
at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.ProcessWeb()
at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.OnLoad(EventArgs e)
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

I found a reference to version info being the cause if it's any help. If I knew how to change this myself I would. :\

http://www.sharepointblogs.com/abdrasin/archive/2007/10/31/site-column-update-error-the-object-has-been-updated-by-another-user-since-it-was-last-fetched.aspx

Many thanks as always.

Gary Lapointe said...

Unfortunately I don't have enough info to replicate this - if you have some repeatable steps that I can follow to repro then I'll see what I can do.

tripwire said...

I didn't do anything special, just copied the content types. I resolved the problem by deleting the affected columns and recreating them.

Anonymous said...

hello, excellent toll tnx
i have a problem that after the copy of contenttypes, the contenttypes exists, but all the columes are not included.what can i do?

Lorella(lgmoss@gmx.at) said...

tnx, very nice. but i have problem after copy of content types i can not see the colums. it is possible to copy also the colums?

Gary Lapointe said...

I'm not sure what's going on - I'd need more info such as how the content types and fields are defined.

Anonymous said...

Great work! How about upgrading already copied content types? Like changing the type of a column or another setting on your "master" site collection types and then propagating the changes. The code for that would be something similar wouldn't it? Do you know if anyone has looked into that?

/Fredrik

Gary Lapointe said...

The code would be a hybrid of the copy and the propagate commands. I haven't looked into it because the better way to solve this problem is to use features.

Anonymous said...

I'm late to this post, but thank you so much. You saved me hours of work. Your instructions worked perfectly

Anonymous said...

Thank you for this command. I was able to deploy the solution but when I tried to use the actual copy command I received an error, "Object Reference is not set to an instance of an object". I confirmed that I'm the farm administrator. (I logged onto the server as such.) Any other ideas? This would be a huge asset as we're building our site. ThanX.

MatjaĆŸ said...

I have a problem with content types that i wouldn't be able to resolve. I have already generated document libraries with files with metadata in them. The lists were created with content type document and list modification. Now i want to use content type for those libraries because it is imposible to manage individualy. Is there a way to change content type that library uses and stil keep meta data of the documents.

i tried creating new content type with same colums that library has but i couldn't copy metadata to new content type columns.

Gary Lapointe said...

Create your new content type and then associate it with the library. You'll then need to modify the content type for each item to have it point to the new content type - as long as you're using site columns for the existing content type and the new one then you shouldn't have to migrate the data - if not then you'll have to copy the old field values to the new field values.

tripwire said...

I was in a similar boat with content types being modified and new columns being added at the list level. I've since learnt to lock them down.

Provided the column names match (internal!) you'll find that when you add the column to your content type - and then associate that with the list item - the metadata will be present.

You can do this quickly using Datasheet view.

Call it a feature, but all custom metadata actually DOES get stored with the document as well. Check custom properties.

Now if only someone could provide an easy way to REMOVE unwanted content types I would be very happy!

I've turned off versioning, disabled check-out, published major versions, deleted all items - the works! But due to previous major versions using the old CT I'm unable to delete it from the list or site collection.

One for you, Gary. :)

Anonymous said...

You mentioned that you had created code using reflection that set a Content Type's ID programatically. Can you post this code? I couldn't find it in the source code on codeplex...

Gary Lapointe said...

Here's the code snippet (you'll need to pull the relevant utility methods):
string xml = (string)Utilities.GetFieldValue(newCT, "m_strXmlNonLocalized");
xml = xml.Replace(string.Format("ID=\"{0}\"", newCT.Id), string.Format("ID=\"{0}\"", sourceCT.Id));
Utilities.SetFieldValue(newCT, typeof (SPContentType), "m_strXmlNonLocalized", xml);
Utilities.SetFieldValue(newCT, typeof(SPContentType), "m_id", sourceCT.Id);

pramod said...

Hi Gary,

Can you please put me in to right direction?

I am struggling to get this right. All i am trying to do is that copying content typr from site A to site B

stsadm -o gl-copycontenttypes -sourceurl "http://sitea" -targeturl "http://siteb" -sourcename xyz document

It keep saying that, Command line error. Dont know where I am doing wrong

Any help would be much appreciated.

pramod said...

Hi Gary,

I managed to copy content across all site collection. How can make it available to all document libraries?

Now I can find the content type in Site settings / site content types

casy said...

Hi Gary,

Cannot get it to work, all I get is 'a syntax error' message,

stsadm -o gl-copycontenttypes -sourceurl "http://sitea/subs/sita" -targeturl "http://siteb/subs/siteb"

MOSS2007 win 2008 64bit

What am I doing wrong ?
Casey

casy said...

Hi Gary,

Ok can copy the CT now, but as with other posts no site columns,

Even created a new site col and new CT but no joy, any ideas.
Kerry

stsadm -o gl-copycontenttypes -sourceurl "http://xxx/sites/aaa" -targeturl "http://yyy/sites/bbb
/Home" -sourcename "TestCT"

casy said...

Gary,
Some More details.
It seams the content type is copied but has none of the custom site columns.
I used the export import site columns routine to copy the site cols to the target site, OK
With a fresh site colection and a fresh content type,columns present (or not) when copied I always get the errors like below.

Kerry

A duplicate name "FileLeafRef" was found.
A duplicate name "ContentType" was found.

casy said...

Gary,
OK installed service pack 2 and started with two fresh site collections and every thing worked as on the box.
The voyage continues...

cws_chan said...

How about the columns field "A" is lookup type? Because "A" cannot get the information list for another site collection.

Jay said...

Gary, in your reply to dirg March 5th 2009 you mention a fix to a memory leak.
Do you remember what that fix was?

Gary Lapointe said...

Jay - I wasn't disposing a couple of items such as the Policy objects. Looks like the code in my post was not updated with those changes so I'm going to take a look at that now and see if I can update it.

Anonymous said...

I have the same issue come up that Pierrick was having while copying content type columns. He says he just had to change the Version attributes and it worked. What version attributes is he talking about and where woudl i change them?

tripwire said...

@Anonymous: He's referring to changes made directly to the SharePoint content database. This is not supported, nor recommended. I have never found version attributes for columns in the DB, only contenttypes.

I have suffered from this issue every time I try to use the command but Gary does not appear able to replicate it. I have to assume it only affects a minority. Or perhaps others just aren't trying to modify the CT after copying it to a new collection.