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.

Monday, April 14, 2008

Fix Variation Relationships List

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:

image

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

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

18 comments:

Dani LH said...

Hello Gary,
Thanks for your excellent job!,
I've tryied your version of stsadm in my enviroment with the purpouse to fix my variation list 'Relationships list'. But i've a problem, the result of the operation is this:

Progress: Begin processing site collection '/'.
Value does not fall within the expected range.

I had lots of problems with my variations in Moss 2007 and i've to say that i've a large number of sites in my portal.
If you have any idea that can help me... please! and thanks for all your time and work!

if you like to yo can send me and email at: daniel.larrotcha@gmail.com

Gary Lapointe said...

Dani - the error you are seeing is because the code is failing in locating either the "Relationships List" or the "Variation Labels" list. These are two hidden lists that get created when you set up the variations. If you're missing these then something is really wrong with your variation setup. If they are not there you might be able to get them there by re-activating the publishing feature (just guessing here though).

Dani LH said...

Hello Gary,
Thank you for your quick answer.
I've been a bit busy those days and i could not post again...
I check my features, relationships list, variation labels list and all them seem to be activated, created and ok, also containing the items.
Looking at the bbdd all seems to be right like i mentioned before. But i am not sure about one think. In your sample of code i see that you are using the keys "Relationships List", "Variation Labels" and when i checked the table alllists in my database i find on tp_title (column that contains the title of the list) that mines were in spanish and portuguese not in english...
I don't know if the object model programming of sharepoint uses this column like the key of the dictionary collection rootweb.lists...
See your code at:
16: SPList relationshipList = rootWeb.Lists["Relationships List"]; 17: SPList variationLabelsList = rootWeb.Lists["Variation Labels"];

I am gone a do a quicktest to verify this question,
thank you for your time and good work!

Rehman Gul said...

Hello Gary,

It was an awesome post.

I have a few confusions, I hope you would be able to help.

I have the site collection already created. The pages are customized. The site is huge, so have to enable variations on the same site.

1. For customized pages, is it necessary to write customized site definitions and propagate using those definitions. Is it possible in my case.

2.Its a huge site, is there a way to do variation on it step by step i.e. taking a few sites at one time etc.

Thanks for sharing!
mscs111@hotmail.com

pulsar said...

Thanks for that very useful tool. I had to modify the sources a bit in order to run the tool against a Variation-Root other than "/" (in my case "/MyWeb")

You can now provide an optional parameter "-web" to specify the path to your Variation Root Site, for instance:

stsadm -o gl-fixvariationrelationships -url http://myportal -web MySite

You can download the modified class over here:

http://codewut.de/content/fixvariationrelationships
or http://codewut.de/files/FixVariationRelationships.cs

Thanks again!

Tim said...

My Relationships List has both pages and webs as entries. I can get the Page's PublishingVariationGroupID but i cannot find the SPWeb's PublishingVariationGroupID. Any advice in finding that?

thanks

Tim

Gary Lapointe said...

Rehman - I'm not really sure on your first issue. I guess it depends on the type of customizations but if you're only talking about page layout changes or customized as in unghosted then I believe that this shouldn't be an issue and that the files should get propagated as needed. For the second issue, as far as I'm aware, there's no way to do a partial migration. Some of the posts that mine references discuss the performance issues when dealing with large sites. You may want to take a look at the infrastructure update (in a test environment!) and see if it helps to address any of your concerns.

Gary Lapointe said...

Tim - to be honest I'm not sure if there is anything stored for the SPWeb objects. The work I was doing on variations didn't necessitate any sub-sites so I didn't have a need to look into it. I imagine the info would be stored in the objects properties bag though, if at all. Use stsadm to export the site and use nofilecompression and then take a look at the manifest.xml file - if it's stored anywhere you'll find it in there and then you can trace that back to a property or object that you can use.

Rehman Gul said...

Hi Gary,

I am getting the following error on any custom stsadm command that I try to use (like gl-fixpublishingpagespagelayouturl etc):

Value cannot be null.
Parameter name: type

Could you advise what am I doing wrong. Thanks for your help.

Gary Lapointe said...

Rehman - the dll for the custom commands is not GAC'd. Verify that the deployment of the WSP was successful. Worst case change the WSP extension to CAB and manually GAC the dll.

John said...

Gary,

Not sure where to post this, but it may be yet another artifact from setting up variations on my site.

I noticed today that my approval workflow is missing from all my variation sites. I need a way to setup the OOTB approval workflow on all my sites in each of my variations. Do you have an STSADM command for that?

Also, any idea how this could have happened?

Thanks!
John

Gary Lapointe said...

John - I've not seen that particular issue but the environment I was working in didn't have many workflows configured. Unfortunately I don't have any commands to setup/configure/enable workflows - just haven't had the need yet (closest is probably the copycontenttype command which will copy workflow associations).

Thomas said...

Hi Gary,
I've got a german server with a german MOSS. I get also the error message:
"Progress: Begin processing site collection '/'.
Value does not fall within the expected range."


Is it possible, that this solution only works in english webs?

Thanks for the great work!

Thomas

Anonymous said...

Hello Gary,

Great post. Reading it I thought I could find what's wrong with a variation site that I replicated from a working site, and has 2 labels: en-us as the source and Fr-fr as the target. But the Relationships List looks like you say it should, and actually running your code did not set anything not already set. Yet when I created a new page in the source site, the Variation Logs said
"A new page did not get created under Label Fr-fr by the variation system for source page http://moss-sfc-app/sites/vt_tar2/en-us/Pages/8.aspx. Cannot create Variation Publishing Page because the target Publishing Web cannot be determined."
So I tried the variationsfixuptool of stsadm, and to my susprise it did fix the problem.
Now what the tool says it did is
"Assigning source site '/sites/vt_tar2/en-us' target site '/sites/vt_tar2/Fr-fr' to have GroupID '0b5...'".
So I am asking if the guid appearing as GroupID of all the labels has any significance as a guid of some other object in the site? (the guid I had before running the fixuptool was just copied from the original site, so cannot of course be relevant to this site.)

Thanks, Shaul

Gary Lapointe said...

I never got around to getting the fix up tool to work for sites that were screwed up which is why I had some links to tools others built. SP2 is the way to go though.

Somesh Bhalerao said...

Hi Gary,

Good post and development.

I need your help.
I am trying to use following command.

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

But I am getting error.
Progress: Begin processing site collection '/'.
Value does not fall within the expected range.

As per your suggestion I checked my site. Both of the list are avilable.
1] Relationships List
2] Variation Labels

Not able to figure out the cause.
Please suggest the way out.

Osaka said...

Hi..

I have a requirement like,

Whenever the user changes any site (English and Italin in my case) , the other should be updated. Is this possible.
My question is, We need to keep changing Source site in this case and also create a new variations?
How much feasible is this requirement?
Can you please let me know incase any workaround for this?

Thanks in advance.. :)
Prathibha Mendon

Gary Lapointe said...

Osaka - it's not possible using OOTB features. You'll need to implement your own custom logic to do this (basically talking about recreating varaitions and make it considerably more complex - I strongly recommend you push back on this requirement as it will be a nightmare to implement)