As I mentioned in my previous post (Replace Field Values) I’m currently trying to solve the issue of broken links throughout the various sites that I’ve upgraded and moved around. The gl-replacefieldvalues command was the first of three that I’ve created. This second command, called gl-applyupgradeareaurlmappings, is very similar to the gl-replacefieldvalues command but it is a bit specialized.

I’ve created this command to address the list items in the special Upgrade Area Url Mappings list (http://[portal]/Lists/98d3057cd9024c27b2007643c1/AllItems.aspx). Instead of taking in a search string and replace string like the other command does I use this list as the source for the search and replace string. The intent is to prevent the user from having to deal with the spsredirect.aspx page when clicking links within a web application.

It’s important to note though that I’ve limited this command to be scoped at the web application level and below and the upgrade list must exist in the web application being targeted. I went back and forth on whether I wanted to allow a farm scope but in the end decided against it. I also considered allowing you to target one application but reference the list from another but the reality of that was that it would be much too difficult to do a reliable replace due to the fact that the URLs in the list are server relative so I simply wouldn’t be able to know which to make absolute and which web app to use for the absolute path.

I haven’t been able to do a significant amount of testing with this but it seems to work fairly well – I was concerned about the order in which the replacements occur which could result in funny things happening but in the test runs that I did the replacements seemed to work just fine (if anyone finds differently please let me know and I’ll see if I can work it out).

Because of this and due to the general nature of making batch content replacements I strongly suggest using the “-test” parameter and verifying all the changes that will be attempted before you execute for real – if you don’t and things get messed up then you’ll have a nice new command to create: revertpreviouscheckins sounds like a good name 🙂

The large bulk of this code is just a series of methods with different loops in them to handle the various scoping capabilities. The core code itself is simply looking at all fields of type string and doing a regular expression replace using the information from the upgrade list. Like the gl-replacefieldvalues command I’ve added the ability to dump all changes to a log file as well as to run the command in a “test” mode where it will show you what it would change but not actually make the change (as mentioned above) – Again, I’d strongly recommend you use this first to verify all the changes that will be made. The core code is shown below:

  1/// <summary>
  2/// Replaces the values.
  3/// </summary>
  4/// <param name="list">The list.</param>
  5/// <param name="settings">The settings.</param>
  6private static void ReplaceValues(SPList list, Settings settings)
  7{
  8    if (list.Title.ToLowerInvariant() == "upgrade area url mapping" || list.DefaultViewUrl.ToLowerInvariant().IndexOf(UPGRADE_AREA_URL_LIST) >= 0)
  9        return; // We don't want to process this list as it's the source of our data.
 10
 11    Log(settings, "Processing List: " + list.DefaultViewUrl);
 12
 13    if (upgradeUrls == null)
 14    {
 15        BuildReplacementUrlsList(settings);
 16    }
 17
 18    // We've got the replacement list built - now we need to check every field of every item.
 19    foreach (SPListItem item in list.Items)
 20    {
 21        if (item.File != null && !Utilities.IsCheckedOutByCurrentUser(item))
 22        {
 23            continue;
 24        }
 25        bool wasCheckedOut = true;
 26        bool modified = false;
 27
 28        foreach (SPField field in list.Fields)
 29        {
 30            if (item[field.Id] == null || field.ReadOnlyField)
 31                continue;
 32
 33            Type fieldType = item[field.Id].GetType();
 34
 35            if (fieldType != typeof(string))
 36                continue; // We're only going to work with strings.
 37
 38            string fieldName = field.Title.ToLowerInvariant();
 39            if (settings.UseInternalFieldName)
 40                fieldName = field.InternalName.ToLowerInvariant();
 41
 42            if (settings.FieldName == null || settings.FieldName.ToLowerInvariant() == fieldName)
 43            {
 44                //  if (item[field.Id].ToString().IndexOf("C4/") >= 0)
 45                //      System.Diagnostics.Debugger.Break();
 46
 47                // We've found a field that we need to consider.  Check all possibilities to see if a match is found.
 48                foreach (ReplacmentSettings rs in upgradeUrls)
 49                {
 50                    bool isMatch = rs.Regex.IsMatch((string)item[field.Id]);
 51
 52                    // if (item[field.Id].ToString().IndexOf("C4/") >= 0 && rs.Regex.ToString().IndexOf("C4/") >= 0)
 53                    //     System.Diagnostics.Debugger.Break();
 54
 55                    if (!isMatch)
 56                        continue;
 57
 58                    string result = rs.Regex.Replace((string)item[field.Id], rs.ReplaceString);
 59
 60                    Log(settings,
 61                    string.Format("Match found: List={0}, Field={1}, Replacement={2} => {3}", item.Url,
 62                    field.Title, item[field.Id], result));
 63
 64                    if (!settings.Test)
 65                    {
 66                        if (item.File != null && item.File.CheckOutStatus == SPFile.SPCheckOutStatus.None)
 67                        {
 68                            item.File.CheckOut();
 69                            wasCheckedOut = false;
 70                        }
 71                        item[field.Id] = result;
 72                        modified = true;
 73                    }
 74                }
 75            }
 76        }
 77        if (modified && !settings.Test)
 78            item.Update();
 79
 80        if (modified && item.File != null)
 81            item.File.CheckIn("Checking in changes to list item due to automated upgrade area url mappings being applied.");
 82        else if (!wasCheckedOut && item.File != null)
 83            item.File.UndoCheckOut();
 84
 85        if (modified && settings.Publish && !wasCheckedOut)
 86            PublishItems.PublishListItem(item, list, new PublishItems.Settings(settings.Quiet, settings.Test, settings.LogFile),
 87                "\"stsadm.exe -o replacefieldvalues\"");
 88    }
 89    Log(settings, "Finished Processing List: " + list.DefaultViewUrl + "\r\n");
 90}
 91 
 92/// <summary>
 93/// Builds the replacement urls list.
 94/// </summary>
 95/// <param name="settings">The settings.</param>
 96private static void BuildReplacementUrlsList(Settings settings)
 97{
 98    // We need to build a local copy of the replacement urls so that we don't have to keep referring back
 99    // to the list for the information.
100    upgradeUrls = new List<ReplacmentSettings>();
101    foreach (SPListItem item in settings.UpgradeList.Items)
102    {
103        if ((bool)item["ShouldRedirect"])
104        {
105            ReplacmentSettings rs = new ReplacmentSettings();
106            rs.ReplaceString = (string)item["V3ServerRelativeUrl"];
107            string str = (string) item["V2ServerRelativeUrl"];
108
109            rs.Regex = new Regex("(?i:" + str + "|" + SPEncode.UrlEncodeAsUrl(str) + ")");
110
111            upgradeUrls.Add(rs);
112        }
113    }
114}

The syntax of the command I created can be seen below.

C:\>stsadm -help gl-applyupgradeareaurlmappings

stsadm -o gl-applyupgradeareaurlmappings

Replaces all occurrences of the V2 server relative URLs with the corresponding V3 server relative URLs as specified in the Upgrade Area Url Mappings list: "http://[portal]/Lists/98d3057cd9024c27b2007643c1/AllItems.aspx"

Parameters:
        -url <url to search>
        -scope <WebApplication | Site | Web | List>
        [-field <field name>]
        [-useinternalfieldname (if not present then the display name will be used)]
        [-quiet]
        [-test]
        [-logfile <log file>]
        [-publish]

Here’s an example of how to replace all occurrences of the V2ServerRelativeUrls with the corresponding V3ServerRelativeUrl:

stsadm -o gl-applyupgradeareaurlmappings -url "http://intranet/" -scope webapplication -logfile "c:\replace.log" -publish

If you wish to filter by field name you can specify either the display name or the internal name. Note that if you specify the internal name you must also add the useinternalfieldname parameter – if you don’t use the internal field name then be aware that you may be updating more than the field you intended as the display name is not always unique (but in most cases it is).

If you don’t want your changes published then don’t specify the publish parameter. However, specifying the publish parameter will not publish items that were previously checked out. If an item can be published and it requires approval then specifying the publish parameter will also cause the item to be approved.