Query your datasource using custom tokens in Sitecore

Before heading into the post I would like to thank Sitecore for giving me the great honor of being awarded as one of the 167 MVPs. I truly enjoy sharing my experiences with people so this means a lot to me. Check out all the skilled MVPs at Sitecore MVPs 2015

Datasource

I would like to show you guys how to do queryable datasources for your renderings. This is nothing new and there are a lot of posts out there describing it very well, so why reinvent the wheel? I found some really nice posts and used most of their code.
Queries in Datasource location on Sitecore Layouts by Thomas Stern aka Dr Stern 🙂
Multiple Datasource Locations using Queries are back in Sitecore 7.2 Update-2 by Sean Holmesby

As Sean pointed out in his post – Since Sitecore 7.2 you can have multiple datasources, that is really cool.

Sitecore 7.2 Update-2 fixes this, and Sitecore’s default code will allow you to define multiple Datasource locations by having them pipe separated in the ‘Datasource Location’ field on a rendering/sublayout

I checked Sean’s and Thomas code and created my pipeline for supporting queryable datasources to the renderings. I started out doing some queries and it worked great but then I needed to to go “above” my site node to find its datasource folder.

Sitecore
–Content
—Site1
—Site2
—Site3
—Site4
—Datasource content
—–Global
—–Site1
——-Some folder
—–Site2
—–Site3
—–Site4

In other words I need to get the name of an element and then add it to the datasource path, something like this:

query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=name of site element])/Some folder

I read all kinds of stuff on what you can do with the query using XPath functions. Here are some good posts you should read:
SITECORE QUERY CHEAT SHEET
Querying Items from Sitecore

Good old Sitecore SDN stuff:
Sitecore Query Syntax
Data Definition Reference – chapter 4:3
Using Sitecore Fast Query

At the end I came to the conclusion that I needed following XPath function:
name(node) – XPath, XQuery, and XSLT Functions, unfortunately this one is not supported in Sitecore’s XPath

So what to do? Why not make a custom token since I already have my own pipeline for the queryable datasource. The custom token should be something like this:

<< node?Field attribute >> 
<<ancestor-or-self::*[@@templatename='Site']?@name>>   

I found this nice old post about accessing special properties of a Sitecore item the same way you get a typical field’s value:
Getting a special field value in Sitecore by Sean Kearney

Here is how the datasource finally will look like:

/sitecore/content/Datasource content/Global/Social sharing|query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=<<ancestor-or-self::*[@@templatename='Site']?@name>>])/Social sharing

Here is the code for the pipeline:

public class MultipleDataSourceLocationsWithQueries
{
    private IDatasourceQueryService _datasourceQueryService; 

    public void Process(GetRenderingDatasourceArgs args)
    {
        Assert.IsNotNull(args, "args");

        this._datasourceQueryService = new DatasourceQueryService(args.ContextItemPath, args.ContentDatabase, args.RenderingItem["Datasource Location"], args.DatasourceRoots);

        if (!this._datasourceQueryService.IsQueryInDataSourceLocation()) 
            return;

        this._datasourceQueryService.ProcessQuerys();
    }
}

Then I created a service for the datasource stuff.

public class DatasourceQueryService : IDatasourceQueryService
{
    private readonly string _contextItemPath;
    private readonly Database _contentDatabase;
    private readonly string _datasourceLocation;
    private readonly List<Item> _datasourceRoots;
    private readonly Regex _regEx;

    public DatasourceQueryService(string contextItemPath, Database contentDatabase, string datasourceLocation, List<Item> datasourceRoots)
    {
        this._contextItemPath = contextItemPath;
        this._contentDatabase = contentDatabase;
        this._datasourceLocation = datasourceLocation;
        this._datasourceRoots = datasourceRoots;
        this._regEx = new Regex(GetRegexPattern());
    }

        
    public bool IsQueryInDataSourceLocation()
    {
        return this._datasourceLocation.Contains(QueryIdentifier);
    }

    public void ProcessQuerys()
    {
        ListString possibleQueries = new ListString(this._datasourceLocation);
        foreach (string possibleQuery in possibleQueries)
        {
            if (possibleQuery.StartsWith(QueryIdentifier))
            {
                ProcessQuery(possibleQuery);
            }
        }
    }

    private string GetRegexPattern()
    {
        return String.Format(".*{0}(.*){1}.*", SpecialStartItemDelimiter, SpecialEndItemDelimiter);
    }


    private void ProcessQuery(string query)
    {
        //Remove the "query:."
        query = query.Replace(QueryIdentifier, "");

        Item[] datasourceLocations = ResolveDatasourceRootFromQuery(query);

        if (datasourceLocations == null || !datasourceLocations.Any()) 
            return;

        foreach (Item dataSourceLocation in datasourceLocations.Where(dataSourceLocation => !this._datasourceRoots.Exists(item => item.ID.Equals(dataSourceLocation.ID))))
        {
            this._datasourceRoots.Add(dataSourceLocation);
        }
    }

    private Item[] ResolveDatasourceRootFromQuery(string queryPath)
    {
        Match matchedData = this._regEx.Match(queryPath);

        int numberOfMatchedTokens = 1;

        while (matchedData.Success)
        {
            //Just in case - I hate while loops
            if (numberOfMatchedTokens > MaxNumberOfIterations)
                break;
                
            queryPath = GenerateQueryPathForSpecialToken(matchedData, queryPath);
            matchedData = this._regEx.Match(queryPath);

            numberOfMatchedTokens++;
        }

        return GetItemsFromQuery(queryPath);
    }

    private string GenerateQueryPathForSpecialToken(Match matchedData, String queryPath)
    {
        string specialQueryPath = matchedData.Groups[1].ToString();

        string translatedValue = ResolveSpecialItemInQuery(specialQueryPath);

        return queryPath.Replace(
            string.Format("{0}{2}{1}", SpecialStartItemDelimiter, SpecialEndItemDelimiter, specialQueryPath),
            translatedValue);
    }

    private Item[] GetItemsFromQuery(string queryPath)
    {
        try
        {
            return this._contentDatabase.SelectItems(string.Format("{0}/{1}", this._contextItemPath, queryPath));
        }
        catch (Exception ex)
        {
            Log.Error(String.Format(@"Datasource query was not valid:{0}", queryPath), ex, this);
            return null;
        }
    }

    private string ResolveSpecialItemInQuery(string specialQueryPath)
    {
        if (!specialQueryPath.Contains(FieldDelimiter))
            return string.Empty;

        string specialItemPath = specialQueryPath.Split(char.Parse(FieldDelimiter))[0];

        if (string.IsNullOrWhiteSpace(specialItemPath))
            return string.Empty;

        Item[] specialItems = GetItemsFromQuery(specialItemPath);

        if (!specialItems.Any())
            return string.Empty;

        string fieldProperty = specialQueryPath.Split(char.Parse(FieldDelimiter))[1];

        if (string.IsNullOrWhiteSpace(fieldProperty))
            return string.Empty;

        return specialItems.Select(item => item[fieldProperty]).FirstOrDefault();

    }

    private const string QueryIdentifier = "query:.";
    private const string SpecialStartItemDelimiter = "<<";
    private const string SpecialEndItemDelimiter = ">>";
    private const string FieldDelimiter = "?";
    private const Int32 MaxNumberOfIterations = 10;

}

The interface for the service

public interface IDatasourceQueryService
{
    bool IsQueryInDataSourceLocation();

    void ProcessQuerys();
}

In order to make the pipeline work we need to do a patch config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
       <processor type="Sandbox.QueryableDatasource.Pipelines.GetRenderingDatasource.MultipleDataSourceLocationsWithQueries, Sandbox.QueryableDatasource"
                   patch:after="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']" />
        </processor>
      </getRenderingDatasource>
    </pipelines>
  </sitecore>
</configuration>

That’s all for now folks 🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.