From 976189aa4057eacd8fc5a1a0609ec6027d5417d3 Mon Sep 17 00:00:00 2001 From: marvinw Date: Mon, 17 Aug 2026 13:30:02 +0200 Subject: [PATCH 1/2] OAK-12364: XPath queries fail to parse when path names contain invalid XML characters --- .../java/org/apache/jackrabbit/oak/Oak.java | 8 ++++ .../oak/query/QueryEngineSettings.java | 12 ++++++ .../oak/query/xpath/XPathToSQL2Converter.java | 6 ++- .../jackrabbit/oak/query/XPathTest.java | 14 +++++++ .../query/index/TraversingIndexQueryTest.java | 9 ++++ .../oak/query/xpath/XPathToSQL2Test.java | 42 +++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java index 407e208384e..68e8ac7c8de 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java @@ -589,6 +589,10 @@ public Oak with(@NotNull Whiteboard whiteboard) { LOG.info("Registered ignore limit in index selection feature: " + QueryEngineSettings.FT_IGNORE_LIMIT_IN_INDEX_SELECTION); closer.register(ignoreLimitInIndexSelection); queryEngineSettings.setIgnoreLimitInIndexSelectionFeature(ignoreLimitInIndexSelection); + Feature xmlNameCharsInPath = newFeature(QueryEngineSettings.FT_XML_NAME_CHARS_IN_PATH, whiteboard); + LOG.info("Registered XML name characters in path feature: " + QueryEngineSettings.FT_XML_NAME_CHARS_IN_PATH); + closer.register(xmlNameCharsInPath); + queryEngineSettings.setXmlNameCharsInPathFeature(xmlNameCharsInPath); } return this; @@ -1009,6 +1013,10 @@ public void setIgnoreLimitInIndexSelectionFeature(@Nullable Feature feature) { settings.setIgnoreLimitInIndexSelectionFeature(feature); } + public void setXmlNameCharsInPathFeature(@Nullable Feature feature) { + settings.setXmlNameCharsInPathFeature(feature); + } + @Override public void setQueryValidatorPattern(String key, String pattern, String comment, boolean failQuery) { settings.getQueryValidator().setPattern(key, pattern, comment, failQuery); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java index 93d6eebda7f..25be5f5caea 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java @@ -67,6 +67,8 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit public static final String FT_IGNORE_LIMIT_IN_INDEX_SELECTION = "FT_OAK-12057"; + public static final String FT_XML_NAME_CHARS_IN_PATH = "FT_OAK-12364"; + public static final int DEFAULT_PREFETCH_COUNT = Integer.getInteger(OAK_QUERY_PREFETCH_COUNT, -1); public static final String OAK_QUERY_FAIL_TRAVERSAL = "oak.queryFailTraversal"; @@ -125,6 +127,7 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit private Feature sortUnionQueryLegacyModeFeature; private Feature optimizeXPathUnion; private Feature ignoreLimitInIndexSelectionFeature; + private Feature xmlNameCharsInPathFeature; private String autoOptionsMappingJson = "{}"; private QueryOptions.AutomaticQueryOptionsMapping autoOptionsMapping = new QueryOptions.AutomaticQueryOptionsMapping(autoOptionsMappingJson); @@ -257,6 +260,15 @@ public boolean isIgnoreLimitInIndexSelection() { return ignoreLimitInIndexSelectionFeature == null || ignoreLimitInIndexSelectionFeature.isEnabled(); } + public void setXmlNameCharsInPathFeature(@Nullable Feature feature) { + this.xmlNameCharsInPathFeature = feature; + } + + public boolean isXmlNameCharsInPathEnabled() { + // enabled if the feature toggle is not used + return xmlNameCharsInPathFeature == null || xmlNameCharsInPathFeature.isEnabled(); + } + public String getStrictPathRestriction() { return strictPathRestriction.name(); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java index 514d4772629..3383e596977 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java @@ -28,11 +28,12 @@ import org.apache.jackrabbit.oak.query.QueryOptions.Traversal; import org.apache.jackrabbit.oak.query.xpath.Statement.UnionStatement; import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.XMLChar; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * This class can can convert a XPATH query to a SQL2 query. + * This class can convert a XPATH query to a SQL2 query. */ public class XPathToSQL2Converter { @@ -1006,6 +1007,9 @@ private void initialize(String query) throws ParseException { } else { if (Character.isJavaIdentifierPart(c)) { type = CHAR_NAME; + } else if ((settings == null || settings.isXmlNameCharsInPathEnabled()) && XMLChar.isName(c)) { + // accept XML name characters that ISO9075 leaves unencoded, so they are not split off the name + type = CHAR_NAME; } } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java index c7e80a3e97d..d520b545da5 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java @@ -325,6 +325,20 @@ public void union() throws ParseException { "/* xpath: /jcr:root/content// element(*, nt:folder) order by @jcr:score descending */"); } + @Test + public void xmlNameCharsInPathAreConverted() throws ParseException { + verify("/jcr:root/content/m·d/element(*, nt:base)", + "select [jcr:path], [jcr:score], * " + + "from [nt:base] as a " + + "where ischildnode(a, '/content/m·d') " + + "/* xpath: /jcr:root/content/m·d/element(*, nt:base) */"); + verify("/jcr:root/content/m·d", + "select [jcr:path], [jcr:score], * " + + "from [nt:base] as a " + + "where issamenode(a, '/content/m·d') " + + "/* xpath: /jcr:root/content/m·d */"); + } + private void verify(String xpath, String expectedSql2) throws ParseException { String sql2 = new XPathToSQL2Converter().convert(xpath); sql2 = formatSQL(sql2); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java index 07f17b34199..e3253e4b159 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java @@ -500,6 +500,15 @@ public void testMultipleRelativeProperties() throws Exception { assertQuery("//*[*/@d < 3]", "xpath", Arrays.asList("/content")); } + @Test + public void testXmlNameCharsInPathAreQueryable() throws Exception { + root.getTree("/").addChild("content").addChild("m·d").addChild("child"); + root.commit(); + + assertQuery("/jcr:root/content/m·d/element(*, nt:base)", "xpath", + List.of("/content/m·d/child")); + } + @Test public void testLowercaseOnArrays() throws Exception { // OAK-1829 diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java index 33334e954ec..937d0e8a6d2 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java @@ -17,6 +17,8 @@ package org.apache.jackrabbit.oak.query.xpath; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.text.ParseException; @@ -142,6 +144,46 @@ public void testOrWithFunctionCallFeatureEnabled() throws ParseException { "select ... where contains(*, 'test') or [type] = 'page' "); } + @Test + public void testXmlNameCharsInPathFeatureNotSet() throws ParseException { + String sql2 = new XPathToSQL2Converter(new QueryEngineSettings()) + .convert("/jcr:root/a/m·d/element(*, nt:base)"); + assertTrue(sql2.contains("'/a/m·d'")); + } + + @Test + public void testXmlNameCharsInPathFeatureEnabled() throws ParseException { + QueryEngineSettings settings = new QueryEngineSettings(); + settings.setXmlNameCharsInPathFeature(createFeature(true)); + String sql2 = new XPathToSQL2Converter(settings) + .convert("/jcr:root/a/m·d/element(*, nt:base)"); + assertTrue(sql2.contains("'/a/m·d'")); + } + + @Test + public void testXmlNameCharsInPathFeatureDisabled() { + QueryEngineSettings settings = new QueryEngineSettings(); + settings.setXmlNameCharsInPathFeature(createFeature(false)); + try { + new XPathToSQL2Converter(settings) + .convert("/jcr:root/a/m·d/element(*, nt:base)"); + fail("expected ParseException"); + } catch (ParseException expected) { + // expected + } + } + + @Test + public void testGenuineSpaceInPathStillFails() { + try { + new XPathToSQL2Converter(new QueryEngineSettings()) + .convert("/jcr:root/a/m d/element(*, nt:base)"); + fail("expected ParseException"); + } catch (ParseException expected) { + // expected + } + } + /** * Helper method to create a Feature mock with the specified enabled state. */ From 281c35f77f0a55b4cce977edb71a56b70ab373ac Mon Sep 17 00:00:00 2001 From: marvinw Date: Thu, 20 Aug 2026 09:36:02 +0200 Subject: [PATCH 2/2] OAK-12364: use \u notation for the middle dot in tests for readability --- .../org/apache/jackrabbit/oak/query/XPathTest.java | 12 ++++++------ .../oak/query/index/TraversingIndexQueryTest.java | 6 +++--- .../jackrabbit/oak/query/xpath/XPathToSQL2Test.java | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java index d520b545da5..5399cf3cd38 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/XPathTest.java @@ -327,16 +327,16 @@ public void union() throws ParseException { @Test public void xmlNameCharsInPathAreConverted() throws ParseException { - verify("/jcr:root/content/m·d/element(*, nt:base)", + verify("/jcr:root/content/m\u00b7d/element(*, nt:base)", "select [jcr:path], [jcr:score], * " + "from [nt:base] as a " + - "where ischildnode(a, '/content/m·d') " + - "/* xpath: /jcr:root/content/m·d/element(*, nt:base) */"); - verify("/jcr:root/content/m·d", + "where ischildnode(a, '/content/m\u00b7d') " + + "/* xpath: /jcr:root/content/m\u00b7d/element(*, nt:base) */"); + verify("/jcr:root/content/m\u00b7d", "select [jcr:path], [jcr:score], * " + "from [nt:base] as a " + - "where issamenode(a, '/content/m·d') " + - "/* xpath: /jcr:root/content/m·d */"); + "where issamenode(a, '/content/m\u00b7d') " + + "/* xpath: /jcr:root/content/m\u00b7d */"); } private void verify(String xpath, String expectedSql2) throws ParseException { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java index e3253e4b159..5433a813eb6 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java @@ -502,11 +502,11 @@ public void testMultipleRelativeProperties() throws Exception { @Test public void testXmlNameCharsInPathAreQueryable() throws Exception { - root.getTree("/").addChild("content").addChild("m·d").addChild("child"); + root.getTree("/").addChild("content").addChild("m\u00b7d").addChild("child"); root.commit(); - assertQuery("/jcr:root/content/m·d/element(*, nt:base)", "xpath", - List.of("/content/m·d/child")); + assertQuery("/jcr:root/content/m\u00b7d/element(*, nt:base)", "xpath", + List.of("/content/m\u00b7d/child")); } @Test diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java index 937d0e8a6d2..644c2b68cdd 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java @@ -147,8 +147,8 @@ public void testOrWithFunctionCallFeatureEnabled() throws ParseException { @Test public void testXmlNameCharsInPathFeatureNotSet() throws ParseException { String sql2 = new XPathToSQL2Converter(new QueryEngineSettings()) - .convert("/jcr:root/a/m·d/element(*, nt:base)"); - assertTrue(sql2.contains("'/a/m·d'")); + .convert("/jcr:root/a/m\u00b7d/element(*, nt:base)"); + assertTrue(sql2.contains("'/a/m\u00b7d'")); } @Test @@ -156,8 +156,8 @@ public void testXmlNameCharsInPathFeatureEnabled() throws ParseException { QueryEngineSettings settings = new QueryEngineSettings(); settings.setXmlNameCharsInPathFeature(createFeature(true)); String sql2 = new XPathToSQL2Converter(settings) - .convert("/jcr:root/a/m·d/element(*, nt:base)"); - assertTrue(sql2.contains("'/a/m·d'")); + .convert("/jcr:root/a/m\u00b7d/element(*, nt:base)"); + assertTrue(sql2.contains("'/a/m\u00b7d'")); } @Test @@ -166,7 +166,7 @@ public void testXmlNameCharsInPathFeatureDisabled() { settings.setXmlNameCharsInPathFeature(createFeature(false)); try { new XPathToSQL2Converter(settings) - .convert("/jcr:root/a/m·d/element(*, nt:base)"); + .convert("/jcr:root/a/m\u00b7d/element(*, nt:base)"); fail("expected ParseException"); } catch (ParseException expected) { // expected