From 21a24cda4720cdbeb41ba3c9346c829c6b1d8c88 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Mon, 15 Jun 2026 21:59:55 +0530 Subject: [PATCH] fix lang() to match whole language subtags, not any prefix isLanguage matched with String.startsWith, so lang('f') matched xml:lang="fr" and lang('e') matched an en-US locale; XPath 1.0 section 4.3 requires the argument to equal the tag or a subtag delimited by '-'. Apply that rule in one NodePointer helper shared by the DOM and JDOM overrides. --- .../commons/jxpath/ri/model/NodePointer.java | 19 +++++++++++++++++-- .../jxpath/ri/model/dom/DOMNodePointer.java | 2 +- .../jxpath/ri/model/jdom/JDOMNodePointer.java | 2 +- .../ri/model/AbstractBeanModelTest.java | 3 +++ .../jxpath/ri/model/AbstractXMLModelTest.java | 2 ++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java index 3a80f287f..67842a83a 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java +++ b/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java @@ -750,12 +750,27 @@ protected boolean isDefaultNamespace(final String prefix) { * Check whether our locale matches the specified language. * * @param lang String language to check - * @return true if the selected locale name starts with the specified prefix lang, case-insensitive. + * @return true if the selected locale name matches lang under the XPath {@code lang()} rules, case-insensitive. */ public boolean isLanguage(final String lang) { final Locale loc = getLocale(); final String name = loc.toString().replace('_', '-'); - return name.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); + return isLanguage(name, lang); + } + + /** + * Tests whether the language tag {@code value} matches {@code lang} under the XPath 1.0 {@code lang()} rules: the comparison is case-insensitive and + * {@code lang} must equal the whole tag or a leading subtag delimited by {@code '-'}. So {@code "en"} matches {@code "en"} and {@code "en-US"} but not + * {@code "english"}, and the bare prefix {@code "e"} matches neither. + * + * @param value the language tag to test, for example an {@code xml:lang} value or a locale name + * @param lang the language being tested for + * @return whether {@code value} is {@code lang} or a sublanguage of it + */ + protected static boolean isLanguage(final String value, final String lang) { + final String name = value.toUpperCase(Locale.ENGLISH); + final String target = lang.toUpperCase(Locale.ENGLISH); + return name.equals(target) || name.startsWith(target + "-"); } /** diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/dom/DOMNodePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/dom/DOMNodePointer.java index 8c2118f3f..207c9d0e8 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/model/dom/DOMNodePointer.java +++ b/src/main/java/org/apache/commons/jxpath/ri/model/dom/DOMNodePointer.java @@ -679,7 +679,7 @@ public boolean isCollection() { @Override public boolean isLanguage(final String lang) { final String current = getLanguage(); - return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); + return current == null ? super.isLanguage(lang) : isLanguage(current, lang); } @Override diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom/JDOMNodePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom/JDOMNodePointer.java index d85f8b7d6..10f12749c 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/model/jdom/JDOMNodePointer.java +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom/JDOMNodePointer.java @@ -696,7 +696,7 @@ public boolean isCollection() { @Override public boolean isLanguage(final String lang) { final String current = getLanguage(); - return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); + return current == null ? super.isLanguage(lang) : isLanguage(current, lang); } @Override diff --git a/src/test/java/org/apache/commons/jxpath/ri/model/AbstractBeanModelTest.java b/src/test/java/org/apache/commons/jxpath/ri/model/AbstractBeanModelTest.java index c11cedbde..771d432f4 100644 --- a/src/test/java/org/apache/commons/jxpath/ri/model/AbstractBeanModelTest.java +++ b/src/test/java/org/apache/commons/jxpath/ri/model/AbstractBeanModelTest.java @@ -78,7 +78,10 @@ void testAttributeLang() { assertXPathValue(context, "@xml:lang", "en-US"); assertXPathValue(context, "count(@xml:*)", Double.valueOf(1)); assertXPathValue(context, "lang('en')", Boolean.TRUE); + assertXPathValue(context, "lang('en-US')", Boolean.TRUE); assertXPathValue(context, "lang('fr')", Boolean.FALSE); + // lang() must match a whole subtag, not any leading prefix + assertXPathValue(context, "lang('e')", Boolean.FALSE); } /** diff --git a/src/test/java/org/apache/commons/jxpath/ri/model/AbstractXMLModelTest.java b/src/test/java/org/apache/commons/jxpath/ri/model/AbstractXMLModelTest.java index 7bd453dbd..8b65cb4b0 100644 --- a/src/test/java/org/apache/commons/jxpath/ri/model/AbstractXMLModelTest.java +++ b/src/test/java/org/apache/commons/jxpath/ri/model/AbstractXMLModelTest.java @@ -360,6 +360,8 @@ void testLang() { assertXPathValue(context, "//product/prix/@xml:lang", "fr"); // lang() used the built-in xml:lang attribute assertXPathValue(context, "//product/prix[lang('fr')]", "934.99"); + // a leading prefix that is not a whole subtag must not match xml:lang="fr" + assertXPathValue(context, "count(//product/prix[lang('f')])", Double.valueOf(0)); // Default language assertXPathValue(context, "//product/price:sale[lang('en')]/saleEnds", "never"); }