Home » 2014 » August

Monthly Archives: August 2014

Advertisements

Clone Items using the Sitecore Item Web API

Yesterday, I had the privilege to present with Ben Lipson and Jamie Michalski, both of Velir, on the Sitecore Item Web API at the New England Sitecore User Group — if you want to see us in action, check out the recording of our presentation!

Plus, my slides are available here!

During my presentation, I demonstrated how easy it is to customize the Sitecore Item API by adding a custom <itemWebApiRequest> pipeline processor, and a custom pipeline to handle a cloning request — for another example on adding a custom <itemWebApiRequest> pipeline processor, and another pipeline to execute a different custom operation, have a look at this post where I show how to publish Items using the Sitecore Item Web API.

For any custom pipeline you build for the Sitecore Item Web API, you must define a Parameter Object that inherits from Sitecore.ItemWebApi.Pipelines.OperationArgs:

using System.Collections.Generic;

using Sitecore.Data.Items;

using Sitecore.ItemWebApi.Pipelines;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class CloneArgs : OperationArgs
    {
        public CloneArgs(Item[] scope)
            : base(scope)
        {
        }

        public IEnumerable<Item> Destinations { get; set; }

        public bool IsRecursive { get; set; }

        public IEnumerable<Item> Clones { get; set; }
    }
}

I added three properties to the class above: a property to hold parent destinations for clones; another indicating whether all descendants should be cloned; and a property to hold a collection of the clones.

I then created a base class for processors of my custom pipeline for cloning:

using Sitecore.ItemWebApi.Pipelines;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public abstract class CloneProcessor : OperationProcessor<CloneArgs>
    {
        protected CloneProcessor()
        {
        }
    }
}

The above class inherits from Sitecore.ItemWebApi.Pipelines.OperationProcessor which is the base class for most Sitecore Item Web API pipelines.

The following class serves as one processor of my custom cloning pipeline:

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class CloneItems : CloneProcessor
    {
        public override void Process(CloneArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Scope, "args.Scope");
            Assert.ArgumentNotNull(args.Destinations, "args.Destinations");
            IList<Item> clones = new List<Item>();
            foreach (Item itemToClone in args.Scope)
            {
                foreach (Item destination in args.Destinations)
                {
                    clones.Add(CloneItem(itemToClone, destination, args.IsRecursive));
                }   
            }

            args.Clones = clones;
        }

        private Item CloneItem(Item item, Item destination, bool isRecursive)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(destination, "destination");
            return item.CloneTo(destination, isRecursive);
        }
    }
}

The class above iterates over all Items in scope — these are the Items being cloned — and clones all to the specified destinations (parent Items of the clones).

I then spun up the following class to serve as another processor in my custom cloning pipeline:

using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Sitecore.ItemWebApi.Pipelines.Read;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class SetResult : CloneProcessor
    {
        public override void Process(CloneArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Clones, "args.Clones");
            if (args.Result == null)
            {
                ReadArgs readArgs = new ReadArgs(args.Clones.ToArray());
                CorePipeline.Run("itemWebApiRead", readArgs);
                args.Result = readArgs.Result;
            }
        }
    }
}

The above class delegates to the <itemWebApiRead> pipeline which retrieves the clones from Sitecore, and stores these in the Parameter Object instance for the custom cloning pipeline.

In order to handle custom requests in the Sitecore Item Web API, you must create a custom <itemWebApiRequest> pipeline processor. I put together the following class to handle my cloning operation:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.Pipelines;
using Sitecore.Text;
using Sitecore.Web;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Clone;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveCloneAction : RequestProcessor
    {
        public override void Process(RequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(RequestMethod, "RequestMethod");
            Assert.ArgumentNotNullOrEmpty(MultipleItemsDelimiter, "MultipleItemsDelimiter");
            if (!ShouldProcessRequest(args))
            {
                return;
            }

            IEnumerable<Item> destinations = GetDestinationItems();
            if (!destinations.Any())
            {
                Logger.Warn("Cannot process clone action: there are no destination items!");
                return;
            }
            
            CloneArgs cloneArgs = new CloneArgs(args.Scope) 
            { 
                Destinations = destinations,
                IsRecursive = DoRecursiveCloning() 
            };
            CorePipeline.Run("itemWebApiClone", cloneArgs);
            args.Result = cloneArgs.Result;
        }

        private bool ShouldProcessRequest(RequestArgs args)
        {
            // Is this the request method we care about?
            if (!AreEqualIgnoreCase(args.Context.HttpContext.Request.HttpMethod, RequestMethod))
            {
                return false;
            }

            // are multiple axes supplied?
            if (WebUtil.GetQueryString("scope").Contains(MultipleItemsDelimiter))
            {
                Logger.Warn("Cannot process clone action: multiple axes detected!");
                return false;
            }

            // are there any items in scope?
            if (!args.Scope.Any())
            {
                Logger.Warn("Cannot process clone action: there are no items in Scope!");
                return false;
            }

            return true;
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
        
        private IEnumerable<Item> GetDestinationItems()
        {
            char delimiter;
            Assert.ArgumentCondition(char.TryParse(MultipleItemsDelimiter, out delimiter), "MultipleItemsDelimiter", "MultipleItemsDelimiter must be a single character!");
            ListString destinations = new ListString(WebUtil.GetQueryString("destinations"), delimiter);
            return (from destination in destinations
                    let destinationItem = GetItem(destination)
                    where destinationItem != null
                    select destinationItem).ToList();
        }

        private Item GetItem(string path)
        {
            try
            {
                return Sitecore.ItemWebApi.Context.Current.Database.Items[path];
            }
            catch (Exception ex)
            {
                Logger.Error(ex);
            }

            return null;
        }

        private bool DoRecursiveCloning()
        {
            bool recursive;
            if (bool.TryParse(WebUtil.GetQueryString("recursive"), out recursive))
            {
                return recursive;
            }
            
            return false;
        }

        private string RequestMethod { get; set; }

        private string MultipleItemsDelimiter { get; set; }
    }
}

The above class ascertains whether it should handle the request: is the RequestMethod passed via configuration equal to the request method detected, and are there any Items in scope? I also built this processor to handle only one axe in order to keep the code simple.

Once the class determines it should handle the request, it grabs all destination Items from the context database — this is Sitecore.ItemWebApi.Context.Current.Database which is populated via the sc_database query string parameter passed via the request.

Further, the class above detects whether the cloning operation is recursive: should we clone all descendants of the Items in scope? This is also passed by a query string parameter.

I then glued everything together using the following Sitecore configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <itemWebApiClone>
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Clone.CloneItems, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Clone.SetResult, Sitecore.Sandbox" />
      </itemWebApiClone>
      <itemWebApiRequest>
        <processor patch:before="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']"
                   type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveCloneAction, Sitecore.Sandbox">
          <RequestMethod>clone</RequestMethod>
          <MultipleItemsDelimiter>|</MultipleItemsDelimiter>
        </processor>
      </itemWebApiRequest>
    </pipelines>
  </sitecore>
</configuration>

Let’s clone the following Sitecore Item with descendants to two folders:

item-to-clone-destinations

In order to make this happen, I spun up the following HTML page using jQuery — no doubt the front-end gurus reading this are cringing when seeing the following code, but I am not much of a front-end developer:

<!DOCTYPE html>
<html lang="en">
	<head>
		<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
		<script src="//cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js"></script>
		<script src="//cdnjs.cloudflare.com/ajax/libs/prettify/r224/prettify.js"></script>
		<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/prettify/r224/prettify.css" />
	</head>
	<body>
		<img width="400" style="display: block; margin-left: auto; margin-right: auto" src="/assets/img/clone-all-the-things.jpg" />
		<input type="button" id="button" value="Clone" style="width:100px;height:50px;font-size: 24px;" />
		<h2 id="confirmation" style="display: none;">Whoa! Something happened!</h2>
		<div id="working" style="display: none;"><img style="display: block; margin-left: auto; margin-right: auto" src="/assets/img/arrow-working.gif" /></div>
		<pre id="responseContainer" class="prettyprint" style="display: none;"><code id="response" class="language-javascript"></code></pre>
		<script type="text/javascript">
		$('#button').click(function() {
			$('#confirmation').hide();
			$('#responseContainer').hide();
			$('#working').show();
			$.ajax({
					type:'clone',
					url: "http://sandbox7/-/item/v1/sitecore/content/Home/Landing Page One?scope=s&destinations=/sitecore/content/Home/Clones|/sitecore/content/Home/Some More Clones&recursive=true&sc_database=master",
					headers:{
						"X-Scitemwebapi-Username":"extranet\\ItemWebAPI",
						"X-Scitemwebapi-Password":"1t3mW3bAP1"}
				}).done(function(response) {
					$('#confirmation').show();
					$('#response').html(JSON.stringify(response, null, 4));
					$('#working').hide();
					$('#responseContainer').show();
				});
		});
		</script>
	</body>
</html>

Plus, please pardon the hard-coded Sitecore credentials — I know you would never store a username and password in front-end code, right? 😉

The above HTML page looks like this on initial load:

clone-items-html-page-no-data

I then clicked the ‘Clone’ button, and saw the following:

cloned-items-html-page

As you can see, the target Item with descendants were cloned to the destination folders set in the jQuery above:

items-cloned-sitecore

If you have any thoughts on this, or have other ideas around customizing the Sitecore Item Web API, please share in a comment.

Advertisements

Restart the Sitecore Server Using a Custom FileWatcher

For a few months now, I’ve been contemplating potential uses for a custom Sitecore.IO.FileWatcher — this lives in Sitecore.Kernel.dll, and defines abstract methods to handle changes to files on the file system within your Sitecore web application — and finally came up with something: how about a FileWatcher that restarts the Sitecore server when a certain file is uploaded to a specific directory?

You might be thinking “why would I ever want use such a thing?” Well, suppose you need to restart the Sitecore server on one of your Content Delivery Servers immediately, but you do not have direct access to it, and the person who does has left for the week. What do you do?

The following FileWatcher might be one option for the scenario above (another option might be to make frantic phone calls to get the server restarted):

using System;

using Sitecore.Diagnostics;
using Sitecore.Install;
using Sitecore.IO;

namespace Sitecore.Sandbox.IO
{
    public class RestartServerWatcher : FileWatcher
    {
        public RestartServerWatcher()
            : base("watchers/restartServer")
        {
        }

        protected override void Created(string fullPath)
        {
            try
            {
                Log.Info(string.Format("Restart server file detected: {0}. Restarting the server.", fullPath), this);
                FileUtil.Delete(fullPath);
                Installer.RestartServer();
            }
            catch (Exception exception)
            {
                Log.Error("Error in RestartServerWatcher", exception, typeof(RestartServerWatcher));
            }
        }
        
        protected override void Deleted(string filePath)
        {
            return;
        }

        protected override void Renamed(string filePath, string oldFilePath)
        {
            return;
        }
    }
}

All of the magic occurs in the Created() method above — we do not care if the file is renamed or deleted. If the file is detected, the code in the Created() method logs information to the Sitecore log, deletes the file, and then initiates a Sitecore server restart.

I created the following patch configuration file for the RestartServerWatcher class above:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <watchers>
      <restartServer>
        <folder>/restart</folder>
        <filter>restart-server.txt</filter>
      </restartServer>
    </watchers>
  </sitecore>
</configuration>

Since FileWatchers are HttpModules, I had to register the RestartServerWatcher in the <system.webServer> section of my Web.config (this configuration element lives outside of the <sitecore> configuration element, and cannot be mapped via a Sitecore patch configuration file):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- Lots of stuff up here -->
<system.webServer>
	<!-- Some stuff here -->
	<add type="Sitecore.Sandbox.IO.RestartServerWatcher, Sitecore.Sandbox" name="SitecoreRestartServerWatcher"/>
</system.webServer>
<!-- More stuff down here -->
</configuration>

For testing, I uploaded my target file into the target location via the Sitecore File Explorer to trigger a Sitecore server restart:

file-explorer-wizard-upload

I then opened up my Sitecore log, and saw the following entries:

restart-server-log-file

If you have any thoughts on this, or have other ideas for custom FileWatchers, please share in a comment.