Can I programmatically escape dashes in Sitecore queries using FullPath?
Asked Answered
S

4

8

I'm attempting to expand upon a custom Sitecore command to determine if the current item has a parent item matching a certain template id.

I know the query should ideally be as simple as ./ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}'] if the item is the context, or /sitecore/content/home/full/path/to/the-item/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']

Unfortunately the item path includes dashes which need to be escaped like /sitecore/content/home/full/path/to/#the-item#/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}'].

However, ideally I'd like to just use the full path of the item, since it's available as item.Paths.FullPath.

Given an item, what's the best way to write a query containing it's full path, and escaping any dashes that might be contained within?

Scrutineer answered 2/5, 2016 at 20:37 Comment(5)
consider using item.Axes.GetAncestors().Where(...) to keep your code easy to understand and debug.Impacted
@Impacted do you mind creating this as an answer so that I can give you an upvote? I knew there was a way to access ancestors via something LINQ friendly, but I always forget about that dang Axes property.Scrutineer
@Impacted Since it doesn't appear the question has been asked already on SO, per the meta question, I've posted the question you answered as a new question.Scrutineer
@JamesSkemp I was just wondering whether you've had time to test the approaches from mine and Marek's answers? I'm interested to know which approach you ended up using.Goldsberry
Neither, actually. Using LINQ/Axes is generally my preferred way, when I remember that the property exists. Between the two of you I'm not sure which solution is better, so I upvoted you both and will keep an eye on this to accept an answer once more votes have been given.Scrutineer
G
6

I've never seen any Sitecore util class or any other out of the box code which does what you need. It's funny that Sitecore does have a util method which returns false (yes, it does return false) but doesn't have a method which escapes item path so it can be used in query.

You may find code which does what you want (written by Anders Laub) here: https://blog.istern.dk/2014/10/29/escaping-dashes-in-sitecore-queries-datasource-query-update/

And if you're too lazy to click the link, I've copied to code here:

private string EscapeItemNamesWithDashes(string queryPath)
{
  if (!queryPath.Contains("-"))
   return queryPath;

  var strArray = queryPath.Split(new char[] { '/' });
  for (int i = 0; i < strArray.Length; i++)
  {
    if (strArray[i].IndexOf('-') > 0)
    strArray[i] = "#" + strArray[i] + "#";
  }
  return string.Join("/", strArray);
}

If you want to escape also space character, you can try this regex:

string escapedPath = Regex.Replace(myItem.Paths.FullPath, "[^/]*[- ][^/]*", "#$0#");

Also you should remember that there are some reserved words in Sitecore which should be escaped as well. Those are (copied from http://www.newguid.net/sitecore/2012/escape-characterswords-in-a-sitecore-query/ ):

  • ancestor
  • and
  • child
  • descendant
  • div
  • false
  • following
  • mod
  • or
  • parent
  • preceding
  • self
  • true
  • xor
Geometric answered 2/5, 2016 at 20:46 Comment(5)
This code will surround ancestor::*[...] with hashes, which is probably not what the OP wants.Goldsberry
Hey @DmytroShevchenko the code I copied from Anders is only for escaping the item path, not the query.Geometric
Ah, got it. It'll work fine then. How about item names with white spaces? Are they optional to escape?Goldsberry
Good one. I think they are. Actually this regex should do the trick: string escapedPath = Regex.Replace(myItem.Paths.FullPath, "[^/]*[- ][^/]*", "#$0#"); (copied from sdn.sitecore.net/Forum/ShowPost.aspx?postid=29395 ). Still it does not take into account reserved words in sitecore. Answer updated.Geometric
Why not just escape all parts of the path is you're worried about bad characters or reserved words?Gabriellegabrielli
I
5

Assumptions

Assuming you have a reference to a Sitecore.Data.Items.Item (called item) and you would like to find an ancestor with a given Sitecore.Data.ID (called id), there are a number of ways you can access the ancestor.

Using Linq

In a typical Sitecore setup without any custom libs, I would typically use a bit of Linq so as to avoid encoding issues in XPath.

ancestor =
    item
        .Axes
        .GetAncestors()
        .FirstOrDefault(ancestor => ancestor.TemplateID == id);

Using Closest

I use a bespoke framework for Sitecore development that involves a wide variety of extension methods and customized item generation. To avoid the overhead of accessing all the ancestors before filtering, I would use the Closest extension method:

public static Item Closest(this Item source, Func<Item, bool> test)
{
    Item cur;
    for (cur = source; cur != null; cur = cur.Parent)
    if (test(cur))
        break;
    return cur;
}

Accessing the ancestor would then be:

ancestor =
    item
        .Closest(ancestor => ancestor.TemplateID == id);

(Actually, I typically use code that looks like)

ancestor = (ITemplateNameItem) item.Closest(Is.Type<ITemplateNameItem>);

Using XPath

I usually avoid XPath and only use it as a tool of last resort because it often makes code harder to read, introduces encoding issues such as the one you're faced with in this question, and has a hard limit on the number of items that can be returned.

That said, Sitecore has many tools available for searching with XPath, and in some circumstances it does simplify things.

The trick to fixing item paths that contain spaces is: Don't use item paths.

Instead, you can safely use the item's ID, with no more context necessary because it's an absolute reference to the item. It's also guaranteed to follow a specific format.

var query =
    string.Format(
        "//{0}/ancestor::*[@@templateid='{1}']",
        item.ID.ToString(),
        id.ToString());
/* alternatively
var query =
    string.Format(
        "{0}/ancestor::*[@@templateid='{1}']",
        item.Paths.LongID,
        id.ToString());
*/
ancestor =
    item
        .Database
        .SelectSingleItem(query);

Using Sitecore.Data.Query.Query

As I mentioned previously, Sitecore has many tools available for searching with XPath. One of these tools is Sitecore.Data.Query.Query. The SelectItems and SelectSingleItem methods have additional optional parameters, one of which is a Sitecore.Data.Items.Item as a contextNode.

Passing an item in as the second parameter uses the item as the context for the XPath query.

var query =
    string.Format(
        "./ancestor::*[@@templateid='{0}']",
        id.ToString());
ancestor =
    Sitecore
        .Data
        .Query
        .Query
        .SelectSingleItem(query, item);
Impacted answered 3/5, 2016 at 17:25 Comment(8)
Wow. Not only did you answer the question in a couple different ways, I think you also gave me the answer to the question I should have asked. I guess the only note is that GetAncestors() returns top-down, so if you want the closest ancestor to the item you'll have to order accordingly. I also either forgot or didn't know you could just dump in an item id in a query. Nice.Scrutineer
@JamesSkemp that should have been a separate question though. Now the approved answer does not answer the question you asked.Goldsberry
@DmytroShevchenko, it's essentially an XY Problem, and in the end the important point is: if you have to ask the question, you're probably looking for the wrong solution.Impacted
Hmm. Unfortunately, I think you're right, but this is also the answer to the question that I was trying to ask. My problem was "I'm attempting to expand upon a custom Sitecore command to determine if the current item has a parent item matching a certain template id." I'll see if a question on Meta covers this, and if not ask to see how if I should retroactively change the question, or switch the accepted answer.Scrutineer
@JamesSkemp, in the end it's your call which answer answered your question best. Votes are used so that visitors can decide which answer is best in aggregate. I wouldn't worry about it too much in this case so long as you make sure to upvote good answers that answered your question.Impacted
In this case, the question asked is valid in its own right. It will be useful to others in the future. I would actually remove extra context from the question and only leave the text relevant to escaping Sitecore Queries.Goldsberry
I can understand how relevant the question I ended up asking would be to others, since as far as I know this is only and best place to find an answer to the above question. So I've asked on Meta.Scrutineer
Meta has suggested that I post another question asking what I basically asked in my first paragraph, and accept as an answer what solves my asked question. Since both of the other answers that I've upvoted are, as far as I currently know, equal, I'll be holding off on accepting either until one gets ranked up higher, or after I've tested both options. My apologizes to all for the confusion, but I do greatly appreciate your time and solutions, especially since I genuinely believe these answers to be extremely beneficial.Scrutineer
G
3

There's no public helper method in Sitecore that would escape Sitecore Query paths. You'll need to implement escaping logic by hand.

Code-based approach:

I found some code in Sitecore.Kernel.dll, in the pipeline processor Sitecore.Pipelines.GetLookupSourceItems.ProcessDefaultSource. I had to rework it so that complex selectors containing : and [...] are not escaped:

static string EscapeQueryPath(string queryPath)
{
  string[] strArray = queryPath.Split('/');

  for (int i = 0; i < strArray.Length; ++i)
  {
    string part = strArray[i];

    if ((part.IndexOf(' ') >= 0 || part.IndexOf('-') >= 0)
        && part.IndexOf(':') == -1
        && part.IndexOf('[') == -1
        && !part.StartsWith("#", StringComparison.InvariantCulture)
        && !part.EndsWith("#", StringComparison.InvariantCulture))
    {
      strArray[i] = '#' + part + '#';
    }
  }

  return string.Join("/", strArray);
}

Note that this algorithm:

  • Is idempotent - it won't escape the parts of the path which have already been escaped, so /#item-name#/ will not turn into /##item-name##/.
  • Will escape item names that contain either hyphens or spaces.
  • Takes into account complex selectors (like ancestor::*[...] in your example).

Regex approach:

Here's another approach to escaping. It will have exactly the same results as the code above.

string path = "./my-item/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']";
string result = Regex.Replace(str, "/([^/#\\[\\:]*[- ][^/#\\[\\:]*(?=($|/)))", "/#$1#");
// result: ./#my-item#/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']

This is shorter, but most likely a bit slower.

Goldsberry answered 2/5, 2016 at 21:14 Comment(0)
C
0

I agree with previous answers but there are few more things which must be escaped for the queries - names with words 'or' & 'and' and words starting with numbers. I used code similar to this:

string _path = "/path/to-item/12name and qwe/test"; // your path
string[] pathParts = _path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

string escapedPath = string.Join("/", pathParts.Select(p =>
{
    if (p.Contains("and") || p.Contains("or") || p.Contains("-") || char.IsDigit(p[0]))
    {
        return "#" + p + "#";
    }
    return p;
}));
Cellulitis answered 3/5, 2016 at 7:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.