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");
}