If you’ve read my recent post, Fun with Variations, then you know that I’ve been doing a lot of work with variations lately. One of the things that I had to figure out a solution to was how to prevent the hidden Relationships List from getting out of sync with the pages in our publishing site when we migrated content from our authoring farm to our publishing farm. For specifics on what the Relationships List is see the aforementioned post which also touches upon some issues with it.

Breaking this list is extremely easy – let’s say that you have an authoring farm where your content authors create your initial pages and then you would like to migrate those pages to your public publishing farm using the content deployment API (so you could use my gl-exportlistitem and gl-importlistitem commands which use the API or write your own or use Central Admin’s content deployment jobs). In most cases the page will appear to have migrated just fine but you may notice a couple things that aren’t quite right – first, the Variation Label Menu is no longer working – you think to yourself, “well that’s just a navigation thing – maybe if I use the editing page toolbar to update the variations then the navigation will ‘fix itself’?”. So you go into the toolbar and click “Tools -> Update Variation…”. And suddenly you are presented with this very unhelpful error stating that a page already exists at the target location:

So what do you do now? Well, you could delete the page in the other variations but then you’ll lose all history and any translations that you want to keep so that’s not a practical solution. The other option is to try and understand what the failure really is and then fix that. In this case it’s the linking between the Relationships List, the imported page, and the matching pages throughout all the variations. Thus comes the creation of my new command called gl-fixvariationrelationships.

What this command does is for each page in the source variations Pages library it loops through all variations and makes sure that the GroupID field matches and then it looks for an entry in the Relationships List matching the URL of the page and if it doesn’t find an entry then it creates one and if it does find one then it makes sure that the values are correct. It’s important to note that if your setup has page variations that are named differently from one variation to another then you’ll have to fix the issues manually as there’s no way for me to handle this scenario (I’d recommend keeping page names consistent regardless of this issue though – it’s just easier to follow what page goes with what).

I probably should have done a better job abstracting this code into separate method calls to make it easier to read but time has been kind of constrained lately and I just needed to get it done. Anyway, here’s the code:

  1/// <summary>
  2/// Processes the specified site.
  3/// </summary>
  4/// <param name="site">The site.</param>
  5/// <param name="verbose">if set to <c>true</c> [verbose].</param>
  6/// <param name="pageName">Name of the page.</param>
  7public static void Process(SPSite site, bool verbose, string pageName)
  8{
  9    m_verbose = verbose;
 10 
 11    using (SPWeb rootWeb = site.RootWeb)
 12    {
 13        Log(string.Format("Begin processing site collection '{0}'.", site.ServerRelativeUrl));
 14 
 15        SPList relationshipList = rootWeb.Lists["Relationships List"];
 16        SPList variationLabelsList = rootWeb.Lists["Variation Labels"];
 17        PublishingWeb sourceLabelWeb = null;
 18        Dictionary<PublishingWeb, bool> labelWebs = new Dictionary<PublishingWeb, bool>();
 19        foreach (SPListItem item in variationLabelsList.Items)
 20        {
 21            Log(string.Format("Getting variation web '{0}'.", item["Label"]));
 22 
 23            SPWeb web = site.OpenWeb(item["Label"].ToString());
 24            if (!PublishingWeb.IsPublishingWeb(web))
 25                continue;
 26 
 27            if ((bool) item["Is Source"])
 28                sourceLabelWeb = PublishingWeb.GetPublishingWeb(web);
 29 
 30            labelWebs.Add(PublishingWeb.GetPublishingWeb(web), (bool) item["Is Source"]);
 31        }
 32        if (sourceLabelWeb == null)
 33            throw new SPException("Unable to identify source label web.");
 34 
 35        Dictionary<PublishingPage, Guid> sourcePages = new Dictionary<PublishingPage, Guid>();
 36 
 37        Log(string.Format("Begin resetting GroupIDs."));
 38 
 39        // First - make sure that all the matching pages have the same group ID
 40        foreach (PublishingPage page in sourceLabelWeb.GetPublishingPages())
 41        {
 42            if (!(pageName == null || pageName.ToLowerInvariant() == page.Name.ToLowerInvariant()))
 43                continue;
 44 
 45            Log(string.Format("Procesing page '{0}'.", page.Url));
 46 
 47            Guid groupID = Guid.Empty;
 48            if (page.Fields.ContainsField("PublishingVariationGroupID"))
 49            {
 50                if (page.ListItem["PublishingVariationGroupID"] + "" != "" &&
 51                    page.ListItem["PublishingVariationGroupID"] + "" != Guid.Empty.ToString())
 52                {
 53                    groupID = new Guid(page.ListItem["PublishingVariationGroupID"] + "");
 54                    Log(
 55                        string.Format("GroupID '{0}' found in variation source '{1}'.", groupID,
 56                                      sourceLabelWeb.Url));
 57                }
 58            }
 59            else
 60            {
 61                Log(string.Format("Unable to locate PublishingVariationGroupID field for page '{0}'", pageName));
 62            }
 63            if (groupID == Guid.Empty)
 64            {
 65                Log(string.Format("GroupID not found in source - begin searching variations."));
 66                // See if we can find a group ID in matching pages within the other variations
 67                foreach (PublishingWeb varWeb in labelWebs.Keys)
 68                {
 69                    if (labelWebs[varWeb])
 70                        continue; // Don't consider the source as we've already done that
 71                    try
 72                    {
 73                        // Get the matching page
 74                        PublishingPage varPage = varWeb.GetPublishingPages()[page.Url];
 75                        if (varPage == null)
 76                            continue;
 77                        if (varPage.Fields.ContainsField("PublishingVariationGroupID"))
 78                        {
 79                            // If the matching page has a group ID then use that
 80                            if (varPage.ListItem["PublishingVariationGroupID"] + "" != "")
 81                                groupID = new Guid(varPage.ListItem["PublishingVariationGroupID"] + "");
 82                        }
 83                    }
 84                    catch (ArgumentException)
 85                    {
 86                    }
 87                    if (groupID.ToString() != Guid.Empty.ToString())
 88                    {
 89                        Log(string.Format("GroupID '{0}' found in variation '{1}'.", groupID, varWeb.Url));
 90                        break;
 91                    }
 92                }
 93            }
 94            if (groupID == Guid.Empty)
 95            {
 96                groupID = Guid.NewGuid();
 97                Log(string.Format("GroupID not found - new GroupID created: '{0}'.", groupID));
 98            }
 99 
100            // Now that we have a groupID reset all pages to use that same groupID
101            Log(string.Format("Begin resetting variations to use new GroupID."));
102            foreach (PublishingWeb varWeb in labelWebs.Keys)
103            {
104                try
105                {
106                    // Get the matching page
107                    PublishingPage varPage = varWeb.GetPublishingPages()[page.Url];
108                    if (varPage == null)
109                        continue;
110                    if (varPage.Fields.ContainsField("PublishingVariationGroupID"))
111                    {
112                        // Set the groupID if it doesn't match what we have
113                        if ((varPage.ListItem["PublishingVariationGroupID"] + "").ToLower() !=
114                            groupID.ToString().ToLower())
115                        {
116                            Log(
117                                string.Format("Assigning GroupID to page '{0}' in variation '{1}'.",
118                                              varPage.Url, varWeb.Url));
119 
120                            varPage.ListItem["PublishingVariationGroupID"] = groupID.ToString();
121                            varPage.ListItem.SystemUpdate();
122                        }
123                    }
124                }
125                catch (ArgumentException)
126                {
127                }
128            }
129            Log(string.Format("Finished resetting variations to use new GroupID."));
130 
131            sourcePages.Add(page, groupID);
132        }
133        // Now that all the pages have been reset to use the same group ID for each variation we can now deal with the relationships list
134        Log(string.Format("Finished resetting GroupIDs.\r\n"));
135        Log(string.Format("Begin processing of Relationships List."));
136 
137        foreach (PublishingPage page in sourcePages.Keys)
138        {
139            foreach (PublishingWeb varWeb in labelWebs.Keys)
140            {
141                Log(string.Format("Processing page '{0}' on variation '{1}'", page.Url, varWeb.Url));
142 
143                SPFieldUrlValue objectID = new SPFieldUrlValue();
144                objectID.Description = varWeb.Web.ServerRelativeUrl + "/" + page.Url;
145                objectID.Url = site.MakeFullUrl(objectID.Description);
146                string groupID = sourcePages[page].ToString();
147 
148                SPListItem relationshipItem = null;
149                foreach (SPListItem item in relationshipList.Items)
150                {
151                    if (item["ObjectID"].ToString().ToLower() == objectID.ToString().ToLower())
152                    {
153                        Log(
154                            string.Format("Found item in relationships list matching ObjectID '{0}'.",
155                                          objectID.Description));
156 
157                        relationshipItem = item;
158                        relationshipItem["Deleted"] = varWeb.GetPublishingPages()[page.Url] == null;
159                        break;
160                    }
161                }
162                if (relationshipItem == null)
163                {
164                    Log(
165                        string.Format(
166                            "Unable to locate item in Relationships List for ObjectID '{0}' - creating a new item.",
167                            objectID.Description));
168                    // We couldn't find a matching item for the variation so we have to create one
169                    relationshipItem =
170                        relationshipList.Items.Add(relationshipList.RootFolder.ServerRelativeUrl,
171                                                   SPFileSystemObjectType.File);
172                    relationshipItem["ObjectID"] = objectID.ToString();
173                    relationshipItem["Deleted"] = varWeb.GetPublishingPages()[page.Url] == null;
174                }
175                relationshipItem["GroupID"] = groupID;
176                relationshipItem.Update();
177 
178                Log(
179                    string.Format("Relationships List item updated - assigning link to page '{0}'.",
180                                  page.Url));
181 
182                // Now that the relationship list item is set the way we need it we have to link the corresponding page to the item.
183                try
184                {
185                    PublishingPage varPage = varWeb.GetPublishingPages()[page.Url];
186                    if (varPage == null)
187                        continue;
188                    string newLinkID = "/" +
189                                       (rootWeb.ServerRelativeUrl + "/" + relationshipItem.Url).Trim('/');
190                    newLinkID = site.MakeFullUrl(newLinkID.Replace(" ", "%20")) + ", " + newLinkID;
191 
192                    varPage.ListItem["PublishingVariationRelationshipLinkFieldID"] = newLinkID;
193                    varPage.ListItem.SystemUpdate();
194                }
195                catch (ArgumentException)
196                {
197                }
198            }
199        }
200        Log(string.Format("Finished processing of Relationships List."));
201 
202        foreach (PublishingWeb web in labelWebs.Keys)
203        {
204            web.Web.Dispose();
205        }
206        Log(string.Format("Finished processing site collection '{0}'.", site.ServerRelativeUrl));
207    }
208}

Using the command is very simple – you just pass in the URL of your site collection and then an optional page name if you only wish to fix a specific page. You can also pass in an optional verbose parameter so that you can see exactly what the command is doing:

C:\>stsadm -help gl-fixvariationrelationships

stsadm -o gl-fixvariationrelationships


Links publishing pages using information in the hidden Relationship List list.

Parameters:
        -url <url>
        [-pagename <name of page to fix (example: "default.aspx")>]
        [-verbose]

Here’s a simple example of running this command against a variation site collection:

stsadm -o gl-fixvariationrelationships -url http://portal -verbose

If you only want to affect one page you could use the following:

stsadm -o gl-fixvariationrelationships -url http://portal -verbose -pagename "default.aspx"

Update 9/2/2008: Tim Dobrinski has a great tool that he’s put together for fixing many issues with the relationships list (including addressing sub-sites which I’m not currently dealing with). You can find details about the tool here: http://www.thesug.org/blogs/lsuslinky/Lists/Posts/Post.aspx?List=ee6ea231%2D5770%2D4c2d%2Da99c%2Dc7c6e5fec1a7&ID=21