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..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 @@ -26,11 +26,18 @@ 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("['\"&<>]"); + /** 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); + + /** + * 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(); private XQueryImplUtil() { // Util class with static methods only. @@ -50,9 +57,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,22 +71,38 @@ 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. - * 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 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 && 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; } } 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..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 @@ -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 }; } @@ -122,12 +144,30 @@ 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" }, + { "no special chars here 1234", "no special chars here 1234" }, }; } diff --git a/kie-parent/pom.xml b/kie-parent/pom.xml index 0c063540941a..7d2295bf90c9 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