Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,27 @@
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 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);
Comment thread
yesamer marked this conversation as resolved.

/**
* 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.
Comment thread
yesamer marked this conversation as resolved.
Outdated
* See Saxon s9api Javadoc for {@code XQueryCompiler}.
*/
private static final XQueryCompiler COMPILER = PROCESSOR.newXQueryCompiler();
Comment thread
yesamer marked this conversation as resolved.

private XQueryImplUtil() {
// Util class with static methods only.
Expand All @@ -50,9 +66,7 @@ public static String executeReplaceFunction(String input, String pattern, String

static <T> T evaluateXQueryExpression(String expression, Class<T> expectedTypeResult) {
try {
Processor processor = new Processor(false);
XQueryCompiler compiler = processor.newXQueryCompiler();
XQueryExecutable executable = compiler.compile(expression);
XQueryExecutable executable = COMPILER.compile(expression);
Comment thread
yesamer marked this conversation as resolved.
XQueryEvaluator queryEvaluator = executable.load();
XdmItem resultItem = queryEvaluator.evaluateSingle();

Expand All @@ -66,22 +80,41 @@ static <T> T evaluateXQueryExpression(String expression, Class<T> 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 the five XML special characters (&amp; &quot; &apos; &lt; &gt;) 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
*/
static String escapeXmlCharactersReferencesForXPath(String input) {
Comment thread
yesamer marked this conversation as resolved.
if (input != null && XML_CHARACTER_REFERENCES_PATTERN.matcher(input).find()) {
input = input.contains("&") ? input.replace("&", "&amp;") : input;
input = input.contains("\"") ? input.replace("\"", "&quot;") : input;
input = input.contains("'") ? input.replace("'", "&apos;") : input;
input = input.contains("<") ? input.replace("<", "&lt;") : input;
input = input.contains(">") ? input.replace(">", "&gt;") : input;
if (input == null) {
return null;
}
StringBuilder sb = null;
Comment thread
yesamer marked this conversation as resolved.
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
String replacement = switch (ch) {
case '&' -> "&amp;";
case '"' -> "&quot;";
case '\'' -> "&apos;";
case '<' -> "&lt;";
case '>' -> "&gt;";
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{ "a<b", "a.b", "", true }, // less-than in input
{ "a>b", "a.b", "", true }, // greater-than in input
// XML special characters in pattern
{ "<tag>", "<tag>", "", true }, // angle brackets in pattern
};
}

Expand Down Expand Up @@ -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
};
}

Expand Down Expand Up @@ -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)
{ "&", "&amp;" },
{ "\"", "&quot;" },
{ "'", "&apos;" },
{ "<", "&lt;" },
{ ">", "&gt;" },
// mixed: < and > (no &, no quotes)
{ "List<String>", "List&lt;String&gt;" },
// mixed: " only
{ "\"Mr.Y\"", "&quot;Mr.Y&quot;" },
// mixed: all five present — ' < & > ' (missing " in a multi-char mix)
{ "'<&>'", "&apos;&lt;&amp;&gt;&apos;" },
// mixed: all five chars including " alongside others
{ "a&b\"c'<d>", "a&amp;b&quot;c&apos;&lt;d&gt;" },
// special char first, last, and in the middle
{ "&start", "&amp;start" },
{ "end&", "end&amp;" },
{ "mid&dle", "mid&amp;dle" },
{ "no special chars here 1234", "no special chars here 1234" },
};
}

Expand Down
2 changes: 1 addition & 1 deletion kie-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
<version.net.byte-buddy>1.17.6</version.net.byte-buddy>
<version.net.java.dev.glazedlists>1.8.0</version.net.java.dev.glazedlists>
<version.net.minidev.jsonsmart>2.4.10</version.net.minidev.jsonsmart>
<version.net.sf.saxon.Saxon-HE>12.7</version.net.sf.saxon.Saxon-HE>
<version.net.sf.saxon.Saxon-HE>12.10</version.net.sf.saxon.Saxon-HE>
<version.net.thisptr.jackson-jq>1.0.0-preview.20240207</version.net.thisptr.jackson-jq>
<version.org.antlr>3.5.2</version.org.antlr>
<version.org.antlr.ST4>4.0.7</version.org.antlr.ST4>
Expand Down
Loading