Encoding XPath Expressions with both single and double quotes

你离开我真会死。 提交于 2019-11-27 04:29:49
Kaleb

Wow, you all sure are making this complicated. Why not just do this?

public static string XpathExpression(string value)
{
    if (!value.Contains("'"))
        return '\'' + value + '\'';

    else if (!value.Contains("\""))
        return '"' + value + '"';

    else
        return "concat('" + value.Replace("'", "',\"'\",'") + "')";
}

.NET Fiddle & test

Though it certainly won't work in all circumstances, here's a way to sidestep the problem:

doc.DocumentElement.SetAttribute("searchName", name);
XmlNode n = doc.SelectNodes("//review[@name=/*/@searchName]");

I was in need of this so I created this solution, for C#.

    /// <summary>
    /// Returns a valid XPath statement to use for searching attribute values regardless of 's or "s
    /// </summary>
    /// <param name="attributeValue">Attribute value to parse</param>
    /// <returns>Parsed attribute value in concat() if needed</returns>
    public static string GetXpathStringForAttributeValue(string attributeValue)
    {
        bool hasApos = attributeValue.Contains("'");
        bool hasQuote = attributeValue.Contains("\"");

        if (!hasApos)
        {
            return "'" + attributeValue + "'";
        }
        if (!hasQuote)
        {
            return "\"" + attributeValue + "\"";
        }

        StringBuilder result = new StringBuilder("concat(");
        StringBuilder currentArgument = new StringBuilder();
        for (int pos = 0; pos < attributeValue.Length; pos++)
        {
            switch (attributeValue[pos])
            {
                case '\'':
                    result.Append('\"');
                    result.Append(currentArgument.ToString());
                    result.Append("'\",");
                    currentArgument.Length = 0;
                    break;
                case '\"':
                    result.Append('\'');
                    result.Append(currentArgument.ToString());
                    result.Append("\"\',");
                    currentArgument.Length = 0;
                    break;
                default:
                    currentArgument.Append(attributeValue[pos]);
                    break;
            }
        }
        if (currentArgument.Length == 0)
        {
            result[result.Length - 1] = ')';
        }
        else
        {
            result.Append("'");
            result.Append(currentArgument.ToString());
            result.Append("')");
        }
        return result.ToString();
    }

This is what I've come up with

public static string EncaseXpathString(string input)
{         
    // If we don't have any " then encase string in "
    if (!input.Contains("\""))
        return String.Format("\"{0}\"", input);

    // If we have some " but no ' then encase in '
    if (!input.Contains("'"))
        return String.Format("'{0}'", input);

    // If we get here we have both " and ' in the string so must use Concat
    StringBuilder sb = new StringBuilder("concat(");           

    // Going to look for " as they are LESS likely than ' in our data so will minimise
    // number of arguments to concat.
    int lastPos = 0;
    int nextPos = input.IndexOf("\"");
    while (nextPos != -1)
    {
        // If this is not the first time through the loop then seperate arguments with ,
        if (lastPos != 0)
            sb.Append(",");

        sb.AppendFormat("\"{0}\",'\"'", input.Substring(lastPos, nextPos - lastPos));
        lastPos = ++nextPos;

        // Find next occurance
        nextPos = input.IndexOf("\"", lastPos);
    }

    sb.Append(")");
    return sb.ToString();
}

Called using something like

XmlNode node = doc.SelectSingleNode("//review[@name=" + EncaseXpathString("Fred's \"Fancy Pizza\"" + "]")

So we get the following results

EncaseXpathString("Pizza Shed") == "'Pizza Shed'";
EncaseXpathString("Bob's pizza") == "\"Bob's Pizza\"";
EncaseXpathString("\"Pizza\" Pam" == "'\"Pizza\" Pam'";
EncaseXpathString("Fred's \"Fancy Pizza\"") == "concat(\"Fred's \",'\"',\"Fancy Pizza\",'\"')";

So it's only using concat when its necessary (both " and ' in string)

The last result show the concat operation is not as short as it could be (see question) but its close enough and anything more optimal would be very complex as you would have to look for matching pairs of " or '.

I've had problems with all solutions so far. One has extra text sections (e.g. '"' or "'") which breaks what you're looking for. One drops all text after the last quote/dblquote which breaks as well.

This is a dumb and quick solution from a dumb vb developer:

Function ParseXpathString(ByVal input As String) As String
    input = Replace(input, "'", Chr(1))
    input = Replace(input, """", Chr(2))
    input = Replace(input, Chr(1), "',""'"",'")
    input = Replace(input, Chr(2), "','""','")
    input = "concat('','" + input + "')"
    Return input
End Function

Usage (same as previous examples):

x.SelectNodes("/path[@attr=" & ParseXpathString(attrvalue) & "]")

I needed to do this in XSLT itself, so came up with the following based on the answers on this page:

<xsl:template name="escape-string">
  <xsl:param name="string"/>
  <xsl:param name="concat" select="true()"/>
  <xsl:variable name="quote">"</xsl:variable>
  <xsl:variable name="apos">'</xsl:variable>
  <xsl:choose>
    <xsl:when test="not(contains($string, $apos))">'<xsl:value-of select="$string"/>'</xsl:when>
    <xsl:when test="not(contains($string, $quote))">"<xsl:value-of select="$string"/>"</xsl:when>
    <xsl:otherwise>
      <xsl:if test="$concat">concat(</xsl:if>
      <xsl:call-template name="escape-string">
        <xsl:with-param name="string" select="substring-before($string, $apos)"/>
        <xsl:with-param name="concat" select="false()"/>
      </xsl:call-template>
      <xsl:text>, "'", </xsl:text>
      <xsl:call-template name="escape-string">
        <xsl:with-param name="string" select="substring-after($string, $apos)"/>
        <xsl:with-param name="concat" select="false()"/>
      </xsl:call-template>
      <xsl:if test="$concat">)</xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Another variation...my concat() part is a little lazy, but at least it uses the whole value.

    /// <summary>
    /// Returns an XPath string literal to use for searching attribute values (wraped in apostrophes, quotes, or as a concat function).
    /// </summary>
    /// <param name="attributeValue">Attribute value to encode and wrap.</param>
    public static string CreateXpathLiteral(string attributeValue)
    {
        if (!attributeValue.Contains("\""))
        {
            // if we don't have any quotes, then wrap string in quotes...
            return string.Format("\"{0}\"", attributeValue);
        }
        else if (!attributeValue.Contains("'"))
        {
            // if we have some quotes, but no apostrophes, then wrap in apostrophes...
            return string.Format("'{0}'", attributeValue);
        }
        else
        {
            // must use concat so the literal in the XPath will find a match...
            return string.Format("concat(\"{0}\")", attributeValue.Replace("\"", "\",'\"',\""));
        }
    }

Join in the fun

public string XPathLiteral(string text) {

    const string APOS = "'";
    const string QUOTE = @"""";

    int pos = 0;

    int posApos;
    int posQuote;

    posQuote = text.IndexOf(QUOTE, pos);

    if (posQuote < 0) {
        return QUOTE + text + QUOTE;
    }//if

    posApos = text.IndexOf(APOS, pos);

    if (posApos < 0) {
        return APOS + text + APOS;
    }//if

    bool containsApos = posApos < posQuote;

    StringBuilder sb = new StringBuilder("concat(", text.Length * 2);

    bool loop = true;
    bool comma = false;

    while (loop) {

        if (posApos < 0) {
            posApos = text.Length;
            loop = false;
        }//if

        if (posQuote < 0) {
            posQuote = text.Length;
            loop = false;
        }//if

        if (comma) {
            sb.Append(",");
        } else {
            comma = true;
        }//if

        if (containsApos) {
            sb.Append(QUOTE);
            sb.Append(text.Substring(pos, posQuote - pos));
            sb.Append(QUOTE);
            pos = posQuote;
            if (loop) posApos = text.IndexOf(APOS, pos + 1);
        } else {
            sb.Append(APOS);
            sb.Append(text.Substring(pos, posApos - pos));
            sb.Append(APOS);
            pos = posApos;
            if (loop) posQuote = text.IndexOf(QUOTE, pos + 1);
        }//if

        // Toggle
        containsApos = !containsApos;

    }//while

    sb.Append(")");

    return sb.ToString();

}//method

URL: http://vaibhavgaikwad.wordpress.com/2007/10/04/handling-apostrophe-single-quote-in-xpath-expressions-in-net/

quote:

  public static string GetXPathString(string input) {
    string[] fragments = input.Split(new char[] { ‘\” });
    string result = “”;
    result += “concat(””;
    for (int i = 0; i < fragments.Length; i++)
    {
      result += “, ‘” + fragments[i] + “‘”;
      if (i < fragments.Length - 1)
      {
        result += “, \”‘\”";
      }
    }
    result += “)”;
    return result;
  }

And here is how you modify the above code so as to use our new function: // remember to remove the single-quotes after = and ]

XmlNode n = doc.SelectSingleNode(“/root/emp[@lname=" + GetXPathString(ln) + "]“);

Or just do like the previous post suggested. make an easy func replacing the ' and " with & apos ; and & quot ;

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!