42 votes

Apostrophe (') dans une requête XPath

J'utilise les éléments suivants XPATH Query pour répertorier l'objet sous un site. ListObject[@Title='SomeValue'] . SomeValue est dynamique. Cette requête fonctionne tant que SomeValue ne comporte pas d'apostrophe ('). J'ai également essayé d'utiliser la séquence d'échappement. Cela n'a pas fonctionné.

Qu'est-ce que je fais de mal ?

0 votes

SomeValue est donc une variable C# ?

0 votes

Oui, c'est une variable C#. "ListObject[@Title='" + SomeValue +"']". Voici comment j'ai écrit l'expression

59voto

Robert Rossney Points 43767

C'est étonnamment difficile à faire.

Jetez un coup d'œil à la Recommandation XPath et vous verrez qu'il définit un littéral comme suit :

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

En d'autres termes, les chaînes de caractères dans les expressions XPath peuvent contenir des apostrophes ou des guillemets doubles, mais pas les deux.

Vous ne pouvez pas utiliser l'échappement pour contourner ce problème. 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 pour lequel vous ne pouvez pas générer un littéral XPath, par exemple :

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

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

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

Cela nous amène à ceci, qui est beaucoup plus compliqué que je ne le voudrais :

/// <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 cas limites. C'est pourquoi la logique est si stupidement 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();
}

10 votes

S'il vous plaît, faites-le. En fait, je n'en ai pas l'utilité moi-même ; je l'ai fait uniquement parce qu'au début, je trouvais le problème intéressant, puis, en creusant, sa difficulté a commencé à m'ennuyer. Mon TDAH est votre gain.

0 votes

Qu'en est-il de " \n " ? Je doute que de nouvelles lignes puissent également causer des problèmes.

0 votes

Non, il est parfaitement possible qu'une chaîne littérale dans XPath contienne un caractère de nouvelle ligne. Le site uniquement La restriction est que les littéraux entre guillemets simples ne peuvent pas contenir de guillemets simples et que les littéraux entre guillemets doubles ne peuvent pas contenir de guillemets doubles.

7voto

Cody S Points 1806

J'ai porté la réponse de Robert en 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 aidera quelqu'un !

6voto

Christian Hayter Points 17999

EDIT : Après une session intensive de tests unitaires, et après avoir vérifié les 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 s'avère que XPath n'a pas du tout de système d'échappement des caractères, c'est assez primitif en fait. Il est évident que mon code original n'a fonctionné que par coïncidence. Je m'excuse d'avoir induit les gens en erreur !

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

Pour plus de 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 et cela fonctionne bien. Si cela ne fonctionne pas pour vous, peut-être y a-t-il un contexte supplémentaire au problème dont vous devez nous faire part.

0 votes

Vous ne devriez même pas avoir à traiter le XML comme une simple chaîne de caractères. Des choses comme l'échappement et le décapage sont abstraites pour vous par les bibliothèques XML intégrées. Vous réinventez la roue ici.

6 votes

Si vous pouviez m'indiquer une classe BCL qui fait abstraction du processus de construction d'une chaîne de requête XPath, je me débarrasserais volontiers de ces fonctions.

0 votes

Par exemple System.Security.SecurityElement.Escape(value) ? (en C#)

5voto

Ian Roberts Points 59836

La meilleure approche de ce problème est de loin d'utiliser les fonctions 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 dans le langage de programmation hôte et n'est pas soumise aux restrictions des littéraux de chaîne XPath. Par exemple, en 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 devez définir un XsltContext comme décrit dans cet article de MSDN (vous n'aurez besoin que des parties relatives aux variables de cet exemple, pas des fonctions d'extension).

0 votes

Ce site es de loin la meilleure approche. +1

2voto

Fortune Points 11

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

En fa#

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é de manière approfondie, mais il semble fonctionner.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X