From bb936a069f7a70caba849ed26f21d7d5bdfbfd3e Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Tue, 4 Aug 2026 18:28:06 +0200 Subject: [PATCH 1/8] Improve replace and matches functions performances --- .../org/kie/dmn/feel/util/XQueryImplUtil.java | 18 +++++++++++---- .../kie/dmn/feel/util/XQueryImplUtilTest.java | 22 +++++++++++++++++++ kie-parent/pom.xml | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index e617ae77c1a3..bdd9a59aa225 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -32,6 +32,18 @@ public class XQueryImplUtil { private static final Pattern XML_CHARACTER_REFERENCES_PATTERN = Pattern.compile("['\"&<>]"); + /** + * Single Saxon Processor instance. Processor is thread-safe and expensive to construct + * (it initialises the Saxon Configuration and performs a license check). One instance + * per JVM is the Saxon-recommended pattern. + */ + private static final Processor PROCESSOR = new Processor(false); + + /** + * Single XQueryCompiler instance. XQueryCompiler is thread-safe and reusable. + */ + private static final XQueryCompiler COMPILER = PROCESSOR.newXQueryCompiler(); + private XQueryImplUtil() { // Util class with static methods only. } @@ -50,9 +62,7 @@ public static String executeReplaceFunction(String input, String pattern, String static T evaluateXQueryExpression(String expression, Class expectedTypeResult) { try { - Processor processor = new Processor(false); - XQueryCompiler compiler = processor.newXQueryCompiler(); - XQueryExecutable executable = compiler.compile(expression); + XQueryExecutable executable = COMPILER.compile(expression); XQueryEvaluator queryEvaluator = executable.load(); XdmItem resultItem = queryEvaluator.evaluateSingle(); @@ -66,7 +76,7 @@ static T evaluateXQueryExpression(String expression, Class expectedTypeRe } catch (SaxonApiException e) { throw new IllegalArgumentException(e); } - } + } /** * It replaces all the XML Character References (&, ", ', <, >) in a given input string with their "escaping" characters. diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java index c196d33588a7..e4a9cdcbc388 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java @@ -37,6 +37,18 @@ private static Object[][] executeMatchesFunctionTestData() { { "test", "^test", "i", true }, { "fo\nbar", "o.b", null, false }, { "TEST", "test", "i", true }, + // flags + { "test", "^test", "", true }, // flags = "" explicitly + { "FO\nBAR", "fo.bar", "si", true }, // dotall + case-insensitive + { "hello\nworld", "^WORLD", "mi", true }, // multiline + case-insensitive + // XML special characters in input — exercises the escape→embed→evaluate pipeline + { "it's", "it.s", "", true }, // single quote in input + { "say \"hi\"", "say .hi.", "", true }, // double quote in input + { "a&b", "a.b", "", true }, // ampersand in input + { "ab", "a.b", "", true }, // greater-than in input + // XML special characters in pattern + { "", "", "", true }, // angle brackets in pattern }; } @@ -66,6 +78,16 @@ private static Object[][] executeReplaceFunctionTestData() { return new Object[][] { { "testString", "^test", "ttt", "", "tttString" }, { "fo\nbar", "o.b", "ttt", "s", "ftttar" }, + // flags + { "FO\nBAR", "fo.bar", "X", "si", "X" }, // dotall + case-insensitive + // XML special characters in input — exercises the escape→embed→evaluate pipeline + { "a&b", "a.b", "X", "", "X" }, // ampersand in input + { "it's", "it.s", "X", "", "X" }, // single quote in input + // XML special characters in replacement + { "hello", "hello", "a&b", "", "a&b" }, // ampersand in replacement + { "hello", "hello", "it's", "", "it's" }, // single quote in replacement + // backreference in replacement + { "hello", "(h)", "$1$1", "", "hhello" }, // $1 backreference }; } diff --git a/kie-parent/pom.xml b/kie-parent/pom.xml index 8bda366f695b..27f56c820e9d 100644 --- a/kie-parent/pom.xml +++ b/kie-parent/pom.xml @@ -205,7 +205,7 @@ 1.17.6 1.8.0 2.4.10 - 12.7 + 12.10 1.0.0-preview.20240207 3.5.2 4.0.7 From 8d24688d3e9a914cd35e8d51e2b2cdf531b068d1 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Tue, 4 Aug 2026 18:39:09 +0200 Subject: [PATCH 2/8] Additional improvements --- .../org/kie/dmn/feel/util/XQueryImplUtil.java | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index bdd9a59aa225..98015c5d9952 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -26,12 +26,8 @@ import net.sf.saxon.s9api.XQueryExecutable; import net.sf.saxon.s9api.SaxonApiException; -import java.util.regex.Pattern; - public class XQueryImplUtil { - private static final Pattern XML_CHARACTER_REFERENCES_PATTERN = Pattern.compile("['\"&<>]"); - /** * Single Saxon Processor instance. Processor is thread-safe and expensive to construct * (it initialises the Saxon Configuration and performs a license check). One instance @@ -79,19 +75,38 @@ static T evaluateXQueryExpression(String expression, Class expectedTypeRe } /** - * It replaces all the XML Character References (&, ", ', <, >) in a given input string with their "escaping" characters. - * This is required to run XPath functions containing XML Character References. - * @param input A string input representing one of the parameter of managed functions - * @return A sanitized string + * Escapes the five XML special characters (& " ' < >) in a single + * pass over the string, so they are safe to embed as XQuery string literals. + * Returns {@code null} unchanged; returns the original reference when no escaping is needed. + * + * @param input A string parameter of a managed XQuery function + * @return The escaped string, or the original if no special characters were present */ static String escapeXmlCharactersReferencesForXPath(String input) { - if (input != null && XML_CHARACTER_REFERENCES_PATTERN.matcher(input).find()) { - input = input.contains("&") ? input.replace("&", "&") : input; - input = input.contains("\"") ? input.replace("\"", """) : input; - input = input.contains("'") ? input.replace("'", "'") : input; - input = input.contains("<") ? input.replace("<", "<") : input; - input = input.contains(">") ? input.replace(">", ">") : input; + if (input == null) { + return null; + } + StringBuilder sb = null; + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + String replacement = switch (ch) { + case '&' -> "&"; + case '"' -> """; + case '\'' -> "'"; + case '<' -> "<"; + case '>' -> ">"; + default -> null; + }; + if (replacement != null) { + if (sb == null) { + sb = new StringBuilder(input.length() + 16); + sb.append(input, 0, i); + } + sb.append(replacement); + } else if (sb != null) { + sb.append(ch); + } } - return input; + return sb != null ? sb.toString() : input; } } From 8d8c2fa2c988b269d75fe60958dd5e89ca32152d Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Tue, 4 Aug 2026 19:57:51 +0200 Subject: [PATCH 3/8] Doc changes --- .../main/java/org/kie/dmn/feel/util/XQueryImplUtil.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index 98015c5d9952..f9956d2d20ca 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -36,7 +36,12 @@ public class XQueryImplUtil { private static final Processor PROCESSOR = new Processor(false); /** - * Single XQueryCompiler instance. XQueryCompiler is thread-safe and reusable. + * Single XQueryCompiler instance. XQueryCompiler is reusable and may in principle + * be used concurrently in multiple threads. In practice, concurrent compilations share + * the same ErrorReporter, making it difficult to associate error messages with specific + * compilations. Since errors here are immediately wrapped and re-thrown as + * {@link IllegalArgumentException}, this is not a concern. + * See Saxon s9api Javadoc for {@code XQueryCompiler}. */ private static final XQueryCompiler COMPILER = PROCESSOR.newXQueryCompiler(); From 483ecf99a34963b8c1ccc65dc6f4e556eccdf518 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Tue, 4 Aug 2026 20:16:26 +0200 Subject: [PATCH 4/8] MInor --- .../src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index f9956d2d20ca..1dbc3094f3f9 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -81,7 +81,7 @@ static T evaluateXQueryExpression(String expression, Class expectedTypeRe /** * Escapes the five XML special characters (& " ' < >) in a single - * pass over the string, so they are safe to embed as XQuery string literals. + * pass over the string, so they are safe to embed as XPath string literals. * Returns {@code null} unchanged; returns the original reference when no escaping is needed. * * @param input A string parameter of a managed XQuery function From bb174b99bf5b71dff00156c29727e7c18d4ebb93 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Tue, 4 Aug 2026 20:17:52 +0200 Subject: [PATCH 5/8] Minor --- .../src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index 1dbc3094f3f9..a6ad9ed18d65 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -84,7 +84,7 @@ static T evaluateXQueryExpression(String expression, Class expectedTypeRe * pass over the string, so they are safe to embed as XPath string literals. * Returns {@code null} unchanged; returns the original reference when no escaping is needed. * - * @param input A string parameter of a managed XQuery function + * @param input A string parameter of a managed XPath function * @return The escaped string, or the original if no special characters were present */ static String escapeXmlCharactersReferencesForXPath(String input) { From 8f8c3c11d0d67cb004b6776665d5c3c925a15911 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 5 Aug 2026 08:15:39 +0200 Subject: [PATCH 6/8] Tests added. --- .../kie/dmn/feel/util/XQueryImplUtilTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java index e4a9cdcbc388..fc726f08bf6d 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java @@ -144,13 +144,40 @@ void escapeXmlCharactersReferencesForXPathTest(String expression, String expecte private static Object[][] escapeXmlCharactersReferencesForXPathTestData() { return new Object[][] { + // null / empty { null, null }, { "", "" }, + // no special chars — original reference must be returned unchanged { "lolASD", "lolASD" }, + // each of the five special characters in isolation (single-char string) + { "&", "&" }, + { "\"", """ }, + { "'", "'" }, + { "<", "<" }, + { ">", ">" }, + // mixed: < and > (no &, no quotes) { "List", "List<String>" }, + // mixed: " only { "\"Mr.Y\"", ""Mr.Y"" }, + // mixed: all five present — ' < & > ' (missing " in a multi-char mix) { "'<&>'", "'<&>'" }, + // mixed: all five chars including " alongside others + { "a&b\"c'", "a&b"c'<d>" }, + // special char first, last, and in the middle + { "&start", "&start" }, + { "end&", "end&" }, + { "mid&dle", "mid&dle" }, }; } + /** + * Verifies the Javadoc guarantee: when no special characters are present, + * the original String reference is returned (no allocation). + */ + @org.junit.jupiter.api.Test + void escapeXmlCharactersReferencesForXPathReturnsSameReferenceWhenNoEscapingNeeded() { + String input = "no special chars here 1234"; + assertThat(XQueryImplUtil.escapeXmlCharactersReferencesForXPath(input)).isSameAs(input); + } + } \ No newline at end of file From 9f14755d34fe7b2e6cb711dda1b10491f2155fc1 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 5 Aug 2026 09:00:16 +0200 Subject: [PATCH 7/8] minor --- .../java/org/kie/dmn/feel/util/XQueryImplUtil.java | 9 ++++++--- .../org/kie/dmn/feel/util/XQueryImplUtilTest.java | 11 +---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index a6ad9ed18d65..9f7c956253c4 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -29,9 +29,12 @@ public class XQueryImplUtil { /** - * Single Saxon Processor instance. Processor is thread-safe and expensive to construct - * (it initialises the Saxon Configuration and performs a license check). One instance - * per JVM is the Saxon-recommended pattern. + * Single Saxon Processor instance shared across all calls. A {@code Processor} is thread-safe + * and expensive to construct: it initialises the Saxon {@code Configuration} and owns shared + * resources such as the Saxon NamePool. Saxon recommends creating it once and reusing it; + * nothing can be shared between separate {@code Processor} instances. + * Note: {@code new Processor(false)} does not perform a license check — it unconditionally + * creates a plain Home Edition configuration. */ private static final Processor PROCESSOR = new Processor(false); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java index fc726f08bf6d..c968a7da2622 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java @@ -167,17 +167,8 @@ private static Object[][] escapeXmlCharactersReferencesForXPathTestData() { { "&start", "&start" }, { "end&", "end&" }, { "mid&dle", "mid&dle" }, + { "no special chars here 1234", "no special chars here 1234" }, }; } - /** - * Verifies the Javadoc guarantee: when no special characters are present, - * the original String reference is returned (no allocation). - */ - @org.junit.jupiter.api.Test - void escapeXmlCharactersReferencesForXPathReturnsSameReferenceWhenNoEscapingNeeded() { - String input = "no special chars here 1234"; - assertThat(XQueryImplUtil.escapeXmlCharactersReferencesForXPath(input)).isSameAs(input); - } - } \ No newline at end of file From d5748589e09adc894dc086fc88e8ccac6385fb90 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Thu, 6 Aug 2026 10:15:03 +0200 Subject: [PATCH 8/8] Change Request --- .../org/kie/dmn/feel/util/XQueryImplUtil.java | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java index 9f7c956253c4..f8d28507f185 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -28,23 +28,14 @@ public class XQueryImplUtil { - /** - * Single Saxon Processor instance shared across all calls. A {@code Processor} is thread-safe - * and expensive to construct: it initialises the Saxon {@code Configuration} and owns shared - * resources such as the Saxon NamePool. Saxon recommends creating it once and reusing it; - * nothing can be shared between separate {@code Processor} instances. - * Note: {@code new Processor(false)} does not perform a license check — it unconditionally - * creates a plain Home Edition configuration. - */ + /** Shared across all calls. Thread-safe; expensive to construct. One instance per JVM is sufficient. + * See Saxon s9api {@link net.sf.saxon.s9api.Processor} Javadoc. */ private static final Processor PROCESSOR = new Processor(false); /** - * Single XQueryCompiler instance. XQueryCompiler is reusable and may in principle - * be used concurrently in multiple threads. In practice, concurrent compilations share - * the same ErrorReporter, making it difficult to associate error messages with specific - * compilations. Since errors here are immediately wrapped and re-thrown as - * {@link IllegalArgumentException}, this is not a concern. - * See Saxon s9api Javadoc for {@code XQueryCompiler}. + * Shared across all calls. Concurrent use is permitted, but error messages may not be + * attributed to the correct thread under concurrent error conditions. + * See Saxon s9api {@link net.sf.saxon.s9api.XQueryCompiler} Javadoc. */ private static final XQueryCompiler COMPILER = PROCESSOR.newXQueryCompiler(); @@ -83,12 +74,9 @@ static T evaluateXQueryExpression(String expression, Class expectedTypeRe } /** - * Escapes the five XML special characters (& " ' < >) in a single - * pass over the string, so they are safe to embed as XPath string literals. - * Returns {@code null} unchanged; returns the original reference when no escaping is needed. - * - * @param input A string parameter of a managed XPath function - * @return The escaped string, or the original if no special characters were present + * Escapes XML special characters ({@code & " ' < >}) so the value is safe to embed + * as an XPath string literal. Returns {@code null} unchanged; returns the original + * reference if no escaping is needed. */ static String escapeXmlCharactersReferencesForXPath(String input) { if (input == null) {