web-dev-qa-db-fra.com

Caractère spécial dans la requête XPATH

J'utilise ce qui suit XPATH Query pour répertorier l'objet sous un site. ListObject[@Title='SomeValue']. SomeValue est dynamique. Cette requête fonctionne tant que SomeValue n'a pas d'apostrophe ('). Essayé en utilisant la séquence d'échappement également. Ça n'a pas marché.

Qu'est-ce que je fais mal?

41
Prabhu

C'est étonnamment difficile à faire.

Jetez un œil à la recommandation XPath , et vous verrez qu'elle définit un littéral comme:

Literal ::=   '"' [^"]* '"' 
            | "'" [^']* "'"

Autrement dit, les littéraux de chaîne dans les expressions XPath peuvent contenir des apostrophes ou des guillemets doubles, mais pas les deux.

Vous ne pouvez pas utiliser l'évasion pour contourner cela. Un littéral comme celui-ci:

'Some'Value'

correspondra à ce texte XML:

Some'Value

Cela signifie qu'il est possible qu'il y ait un morceau de texte XML auquel vous ne pouvez pas générer un littéral XPath pour correspondre, par exemple:

<Elm att="&quot;&apos"/>

Mais cela ne signifie pas qu'il est impossible de faire correspondre ce texte avec XPath, c'est juste délicat. Dans tous les cas où la valeur que vous essayez de faire correspondre contient à la fois des guillemets simples et doubles, vous pouvez construire une expression qui utilise concat pour produire le texte qui va correspondre:

Elm[@att=concat('"', "'")]

Cela nous amène donc à cela, qui est beaucoup plus compliqué que je ne le souhaiterais:

/// <summary>
/// Produce an XPath literal equal to the value if possible; if not, produce
/// an XPath expression that will match the value.
/// 
/// Note that this function will produce very long XPath expressions if a value
/// contains a long run of double quotes.
/// </summary>
/// <param name="value">The value to match.</param>
/// <returns>If the value contains only single or double quotes, an XPath
/// literal equal to the value.  If it contains both, an XPath expression,
/// using concat(), that evaluates to the value.</returns>
static string XPathLiteral(string value)
{
    // if the value contains only single or double quotes, construct
    // an XPath literal
    if (!value.Contains("\""))
    {
        return "\"" + value + "\"";
    }
    if (!value.Contains("'"))
    {
        return "'" + value + "'";
    }

    // if the value contains both single and double quotes, construct an
    // expression that concatenates all non-double-quote substrings with
    // the quotes, e.g.:
    //
    //    concat("foo", '"', "bar")
    StringBuilder sb = new StringBuilder();
    sb.Append("concat(");
    string[] substrings = value.Split('\"');
    for (int i = 0; i < substrings.Length; i++ )
    {
        bool needComma = (i>0);
        if (substrings[i] != "")
        {
            if (i > 0)
            {
                sb.Append(", ");
            }
            sb.Append("\"");
            sb.Append(substrings[i]);
            sb.Append("\"");
            needComma = true;
        }
        if (i < substrings.Length - 1)
        {
            if (needComma)
            {
                sb.Append(", ");                    
            }
            sb.Append("'\"'");
        }

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

Et oui, je l'ai testé avec tous les boîtiers Edge. Voilà pourquoi la logique est si bêtement complexe:

    foreach (string s in new[]
    {
        "foo",              // no quotes
        "\"foo",            // double quotes only
        "'foo",             // single quotes only
        "'foo\"bar",        // both; double quotes in mid-string
        "'foo\"bar\"baz",   // multiple double quotes in mid-string
        "'foo\"",           // string ends with double quotes
        "'foo\"\"",         // string ends with run of double quotes
        "\"'foo",           // string begins with double quotes
        "\"\"'foo",         // string begins with run of double quotes
        "'foo\"\"bar"       // run of double quotes in mid-string
    })
    {
        Console.Write(s);
        Console.Write(" = ");
        Console.WriteLine(XPathLiteral(s));
        XmlElement Elm = d.CreateElement("test");
        d.DocumentElement.AppendChild(Elm);
        Elm.SetAttribute("value", s);

        string xpath = "/root/test[@value = " + XPathLiteral(s) + "]";
        if (d.SelectSingleNode(xpath) == Elm)
        {
            Console.WriteLine("OK");
        }
        else
        {
            Console.WriteLine("Should have found a match for {0}, and didn't.", s);
        }
    }
    Console.ReadKey();
}
59
Robert Rossney

EDIT: Après une session de tests unitaires lourds et après vérification des normes XPath , j'ai révisé ma fonction comme suit:

public static string ToXPath(string value) {

    const string apostrophe = "'";
    const string quote = "\"";

    if(value.Contains(quote)) {
        if(value.Contains(apostrophe)) {
            throw new XPathException("Illegal XPath string literal.");
        } else {
            return apostrophe + value + apostrophe;
        }
    } else {
        return quote + value + quote;
    }
}

Il semble que XPath n'ait pas du tout de système d'échappement de personnage, il est vraiment très primitif. Évidemment, mon code d'origine ne fonctionnait que par coïncidence. Mes excuses pour avoir trompé quelqu'un!

Réponse originale ci-dessous pour référence seulement - veuillez ignorer

Par sécurité, assurez-vous que toute occurrence des 5 entités XML prédéfinies dans votre chaîne XPath est échappée, par ex.

public static string ToXPath(string value) {
    return "'" + XmlEncode(value) + "'";
}

public static string XmlEncode(string value) {
    StringBuilder text = new StringBuilder(value);
    text.Replace("&", "&amp;");
    text.Replace("'", "&apos;");
    text.Replace(@"""", "&quot;");
    text.Replace("<", "&lt;");
    text.Replace(">", "&gt;");
    return text.ToString();
}

Je l'ai déjà fait auparavant et cela fonctionne bien. Si cela ne fonctionne pas pour vous, il y a peut-être un contexte supplémentaire au problème dont vous devez nous informer.

7
Christian Hayter

J'ai porté la réponse de Robert à Java (testé en 1.6):

/// <summary>
/// Produce an XPath literal equal to the value if possible; if not, produce
/// an XPath expression that will match the value.
///
/// Note that this function will produce very long XPath expressions if a value
/// contains a long run of double quotes.
/// </summary>
/// <param name="value">The value to match.</param>
/// <returns>If the value contains only single or double quotes, an XPath
/// literal equal to the value.  If it contains both, an XPath expression,
/// using concat(), that evaluates to the value.</returns>
public static String XPathLiteral(String value) {
    if(!value.contains("\"") && !value.contains("'")) {
        return "'" + value + "'";
    }
    // if the value contains only single or double quotes, construct
    // an XPath literal
    if (!value.contains("\"")) {
        System.out.println("Doesn't contain Quotes");
        String s = "\"" + value + "\"";
        System.out.println(s);
        return s;
    }
    if (!value.contains("'")) {
        System.out.println("Doesn't contain apostophes");
        String s =  "'" + value + "'";
        System.out.println(s);
        return s;
    }

    // if the value contains both single and double quotes, construct an
    // expression that concatenates all non-double-quote substrings with
    // the quotes, e.g.:
    //
    //    concat("foo", '"', "bar")
    StringBuilder sb = new StringBuilder();
    sb.append("concat(");
    String[] substrings = value.split("\"");
    for (int i = 0; i < substrings.length; i++) {
        boolean needComma = (i > 0);
        if (!substrings[i].equals("")) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append("\"");
            sb.append(substrings[i]);
            sb.append("\"");
            needComma = true;
        }
        if (i < substrings.length - 1) {
            if (needComma) {
                sb.append(", ");
            }
            sb.append("'\"'");
        }
        System.out.println("Step " + i + ": " + sb.toString());
    }
    //This stuff is because Java is being stupid about splitting strings
    if(value.endsWith("\"")) {
        sb.append(", '\"'");
    }
    //The code works if the string ends in a apos
    /*else if(value.endsWith("'")) {
        sb.append(", \"'\"");
    }*/
    sb.append(")");
    String s = sb.toString();
    System.out.println(s);
    return s;
}

J'espère que cela aide quelqu'un!

6
Cody S

De loin, la meilleure approche de ce problème consiste à utiliser les fonctionnalités fournies par votre bibliothèque XPath pour déclarer une variable de niveau XPath que vous pouvez référencer dans l'expression. La valeur de la variable peut alors être n'importe quelle chaîne du langage de programmation Host et n'est pas soumise aux restrictions des littéraux de chaîne XPath. Par exemple, dans Java avec javax.xml.xpath:

XPathFactory xpf = XPathFactory.newInstance();
final Map<String, Object> variables = new HashMap<>();
xpf.setXPathVariableResolver(new XPathVariableResolver() {
  public Object resolveVariable(QName name) {
    return variables.get(name.getLocalPart());
  }
});

XPath xpath = xpf.newXPath();
XPathExpression expr = xpath.compile("ListObject[@Title=$val]");
variables.put("val", someValue);
NodeList nodes = (NodeList)expr.evaluate(someNode, XPathConstants.NODESET);

Pour C # XPathNavigator, vous définiriez un XsltContextpersonnalisé comme décrit dans cet article MSDN (vous n'auriez besoin que du parties liées aux variables de cet exemple, pas les fonctions d'extension).

5
Ian Roberts

La plupart des réponses ici se concentrent sur la façon d'utiliser la manipulation de chaînes pour concocter un XPath qui utilise des délimiteurs de chaînes de manière valide.

Je dirais que la meilleure pratique est de ne pas s'appuyer sur des méthodes aussi compliquées et potentiellement fragiles.

Ce qui suit s'applique à .NET puisque cette question est balisée avec C #. Ian Roberts a fourni ce que je pense être la meilleure solution lorsque vous utilisez XPath en Java.

De nos jours, vous pouvez utiliser Linq-to-Xml pour interroger des documents XML d'une manière qui vous permet d'utiliser directement vos variables dans la requête. Ce n'est pas XPath, mais le but est le même.

Pour l'exemple donné dans OP, vous pouvez interroger les nœuds que vous souhaitez comme ceci:

var value = "Some value with 'apostrophes' and \"quotes\"";

// doc is an instance of XElement or XDocument
IEnumerable<XElement> nodes = 
                      doc.Descendants("ListObject")
                         .Where(lo => (string)lo.Attribute("Title") == value);

ou pour utiliser la syntaxe de compréhension des requêtes:

IEnumerable<XElement> nodes = from lo in doc.Descendants("ListObject")
                              where (string)lo.Attribute("Title") == value
                              select lo;

.NET fournit également un moyen d'utiliser des variables XPath dans vos requêtes XPath. Malheureusement, ce n'est pas facile à faire hors de la boîte, mais avec une simple classe d'aide que je fournis dans cette autre SO , c'est assez facile.

Vous pouvez l'utiliser comme ceci:

var value = "Some value with 'apostrophes' and \"quotes\"";

var variableContext = new VariableContext { { "matchValue", value } };
// ixn is an instance of IXPathNavigable
XPathNodeIterator nodes = ixn.CreateNavigator()
                             .SelectNodes("ListObject[@Title = $matchValue]", 
                                          variableContext);
3
JLRishe

Voici une alternative à l'approche StringBuilder de Robert Rossney, peut-être plus intuitive:

    /// <summary>
    /// Produce an XPath literal equal to the value if possible; if not, produce
    /// an XPath expression that will match the value.
    /// 
    /// Note that this function will produce very long XPath expressions if a value
    /// contains a long run of double quotes.
    /// 
    /// From: http://stackoverflow.com/questions/1341847/special-character-in-xpath-query
    /// </summary>
    /// <param name="value">The value to match.</param>
    /// <returns>If the value contains only single or double quotes, an XPath
    /// literal equal to the value.  If it contains both, an XPath expression,
    /// using concat(), that evaluates to the value.</returns>
    public static string XPathLiteral(string value)
    {
        // If the value contains only single or double quotes, construct
        // an XPath literal
        if (!value.Contains("\""))
            return "\"" + value + "\"";

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

        // If the value contains both single and double quotes, construct an
        // expression that concatenates all non-double-quote substrings with
        // the quotes, e.g.:
        //
        //    concat("foo",'"',"bar")

        List<string> parts = new List<string>();

        // First, put a '"' after each component in the string.
        foreach (var str in value.Split('"'))
        {
            if (!string.IsNullOrEmpty(str))
                parts.Add('"' + str + '"'); // (edited -- thanks Daniel :-)

            parts.Add("'\"'");
        }

        // Then remove the extra '"' after the last component.
        parts.RemoveAt(parts.Count - 1);

        // Finally, put it together into a concat() function call.
        return "concat(" + string.Join(",", parts) + ")";
    }
2
Jonathan Gilbert

Vous pouvez citer une chaîne XPath en utilisant la recherche et le remplacement.

En F #

let quoteString (s : string) =
    if      not (s.Contains "'" ) then sprintf "'%s'"   s
    else if not (s.Contains "\"") then sprintf "\"%s\"" s
    else "concat('" + s.Replace ("'", "', \"'\", '") + "')"

Je ne l'ai pas testé intensivement, mais semble fonctionner.

2
Fortune

Si vous ne prévoyez pas de guillemets doubles dans SomeValue, vous pouvez utiliser des guillemets d'échappement pour spécifier la valeur que vous recherchez dans votre chaîne de recherche XPath.

ListObject[@Title=\"SomeValue\"]
0
48klocs

Vous pouvez résoudre ce problème en utilisant double quotes au lieu de single quotes dans l'expression XPath.

Par exemple:

element.XPathSelectElements(String.Format("//group[@title=\"{0}\"]", "Man's"));
0
Shivanand