Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Evaluates SPARQL expressions that produce string literals. */
Expand Down Expand Up @@ -209,7 +210,8 @@ private static DatatypeValue replace(ReplaceAst expression, NativeEvaluationCont
throw new QueryTypeErrorException("REPLACE pattern must not match an empty string");
}
try {
String result = compiled.matcher(source.getLabel()).replaceAll(replacement);
String result = compiled.matcher(source.getLabel()).replaceAll(
flags.contains("q") ? Matcher.quoteReplacement(replacement) : replacement);
return stringLike(source, result, context);
} catch (IndexOutOfBoundsException invalidGroup) {
throw new QueryTypeErrorException("Invalid replacement group", invalidGroup);
Expand Down Expand Up @@ -260,30 +262,44 @@ private static DatatypeValue digest(
}
}

@SuppressWarnings("MagicConstant")
private static Pattern compilePattern(String expression, String flags) {
int options = regexOptions(flags);
return Pattern.compile(flags.contains("x") ? removePatternWhitespace(expression) : expression, options);
String processed = expression;
if (flags.contains("q")) {
processed = Pattern.quote(expression);
} else if (flags.contains("x")) {
processed = removePatternWhitespace(expression);
}
return Pattern.compile(processed, options);
}

/** XPath x ignores XML whitespace outside character classes, but does not introduce comments. */
private static String removePatternWhitespace(String expression) {
StringBuilder result = new StringBuilder();
StringBuilder result = new StringBuilder(expression.length());
boolean escaped = false;
int brackets = 0;
for (int index = 0; index < expression.length(); index++) {
char character = expression.charAt(index);
if (brackets == 0 && " \t\r\n".indexOf(character) >= 0) {
continue;
}
if (!escaped) {
if (character == '[') brackets++;
if (character == ']') brackets--;
if (brackets == 0 && " \t\r\n".indexOf(character) >= 0) continue;
if (character == '[') {
brackets++;
} else if (character == ']' && brackets > 0) {
brackets--;
}
}
result.append(character);
escaped = !escaped && character == '\\';
}
return result.toString();
}

/** @return a combination of {@link Pattern} flag constants */
private static int regexOptions(String flags) {
if (!flags.chars().allMatch(flag -> "imsx".indexOf(flag) >= 0)) {
if (!flags.chars().allMatch(flag -> "imsxq".indexOf(flag) >= 0)) {
throw new QueryTypeErrorException("Invalid regular expression flags");
}
int options = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/** Resolves SPARQL terms against one immutable query-prologue snapshot. */
public final class SparqlTermResolver {
Expand Down Expand Up @@ -98,10 +99,35 @@ public String unquoteLexical(String lexical) {
return lexical;
}
String unquotedTriple = stripTripleQuotes(lexical);
if (unquotedTriple != null) {
return unquotedTriple;
return processSparqlEscapes(Objects.requireNonNullElseGet(unquotedTriple, () -> stripSingleQuotes(lexical)));
}

static String processSparqlEscapes(String s) {
if (s.indexOf('\\') < 0) {
return s;
}
StringBuilder sb = new StringBuilder(s.length());
int i = 0;
while (i < s.length()) {
if (s.charAt(i) == '\\' && i + 1 < s.length()) {
i++;
switch (s.charAt(i)) {
case 't' -> sb.append('\t');
case 'n' -> sb.append('\n');
case 'r' -> sb.append('\r');
case 'b' -> sb.append('\b');
case 'f' -> sb.append('\f');
case '"' -> sb.append('"');
case '\'' -> sb.append('\'');
case '\\' -> sb.append('\\');
default -> { sb.append('\\'); sb.append(s.charAt(i)); }
}
} else {
sb.append(s.charAt(i));
}
i++;
}
return stripSingleQuotes(lexical);
return sb.toString();
}

private static String stripTripleQuotes(String lexical) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package fr.inria.corese.core.next.query.impl.sparql.bridge;

import fr.inria.corese.core.next.query.Repositories;
import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Unit tests for SPARQL REGEX built-in with XPath flag support.
*
*/
@DisplayName("NativeExpressionEvaluator - XPath REGEX flag support")
class NativeExpressionEvaluatorTest {

private boolean ask(String filter) {
try (var repo = Repositories.create(MemoryStorageManager.builder().build());
var conn = repo.getConnection()) {
return conn.prepareBooleanQuery(
"PREFIX ex: <http://example.com/#> ASK { FILTER(" + filter + ") }").evaluate();
}
}


@Test
@DisplayName("REGEX with i flag matches case-insensitively")
void iFlagMatchesCaseInsensitively() {
assertTrue(ask("REGEX(\"ABCdefGHIjkl\", \"abc\", \"i\")"));
assertTrue(ask("REGEX(\"abcDEFghiJKL\", \"ABC\", \"i\")"));
}

@Test
@DisplayName("REGEX with s flag makes dot match newlines")
void sFlagDotMatchesNewline() {
assertTrue(ask("REGEX(\"a\\nb\", \"a.b\", \"s\")"));
assertFalse(ask("REGEX(\"a\\nb\", \"a.b\")"));
}


@Test
@DisplayName("REGEX with m flag makes ^ and $ match line boundaries")
void mFlagAnchorMatchesLineBoundary() {
assertTrue(ask("REGEX(\"foo\\nbar\", \"^bar$\", \"m\")"));
assertFalse(ask("REGEX(\"foo\\nbar\", \"^bar$\")"));
}


@Test
@DisplayName("REGEX with x flag ignores whitespace in pattern")
void xFlagIgnoresWhitespace() {
assertTrue(ask("REGEX(\"ac\", \" a c \", \"x\")"));
}

@Test
@DisplayName("REGEX with x flag ignores newline and tab in pattern")
void xFlagIgnoresNewlineAndTab() {
assertTrue(ask("REGEX(\"ac\", \" a\\n\\tc \", \"x\")"));
}

@Test
@DisplayName("REGEX with x flag preserves whitespace inside bracket expression")
void xFlagPreservesWhitespaceInsideBrackets() {
assertTrue(ask("REGEX(\"a\\nc\", \" a[\\\\n]c \", \"x\")"));
}

@Test
@DisplayName("REGEX with x flag preserves hashes and whitespace in classes")
void xFlagPreservesHashesAndClassWhitespace() {
assertFalse(ask("REGEX(\"a\", \"a#b\", \"x\")"));
assertTrue(ask("REGEX(\"a#b\", \" a # b \", \"x\")"));
assertTrue(ask("REGEX(\"#\", \"[#]\", \"x\")"));
assertTrue(ask("REGEX(\" \", \"[ a]\", \"x\")"));
assertTrue(ask("REGEX(\"\\n\\t\\r\", \"^[ \\n\\t\\r]+$\", \"x\")"));
assertFalse(ask("REGEX(\"ac\", \"a[ ]c\", \"x\")"));
}

@Test
void xFlagRespectsEscapedBrackets() {
assertTrue(ask("REGEX(\"[a]\", \"\\\\[ a \\\\]\", \"x\")"));
assertTrue(ask("REGEX(\"] \", \"^[\\\\] ]+$\", \"x\")"));
}

@Test
void qFlagOverridesOtherPatternFlags() {
assertTrue(ask("REGEX(\" A#b \", \" a#b \", \"qxims\")"));
assertFalse(ask("REGEX(\"ab\", \" a b \", \"qx\")"));
assertFalse(ask("REGEX(\"a\\nb\", \"a.b\", \"qs\")"));
assertFalse(ask("REGEX(\"a\\nb\", \"^b$\", \"qm\")"));
}

@Test
void qFlagQuotesReplacement() {
assertTrue(ask("REPLACE(\"a/b\", \"/\", \"$0\", \"q\") = \"a$0b\""));
assertTrue(ask("REPLACE(\"a/b\", \"/\", \"$\", \"q\") = \"a$b\""));
assertTrue(ask("REPLACE(\"a/b\", \"/\", \"\\\\\", \"q\") = \"a\\\\b\""));
assertTrue(ask("REPLACE(\"a/b\", \"/\", \"$0\") = \"a/b\""));
}

@Test
void xFlagAlsoAppliesToReplace() {
assertTrue(ask("REPLACE(\"a b\", \"[ ]\", \"-\", \"x\") = \"a-b\""));
assertTrue(ask("REPLACE(\"a#b\", \"#\", \"-\", \"x\") = \"a-b\""));
}

@Test
@DisplayName("REGEX with q flag treats pattern as literal (no metacharacters)")
void qFlagTreatsPatternAsLiteral() {
assertTrue(ask("REGEX(\"a?+*.{}()[]c\", \"a?+*.{}()[]c\", \"q\")"));
assertFalse(ask("REGEX(\"abc\", \"a?+*.{}()[]c\", \"q\")"));
}

@Test
@DisplayName("REGEX with iq flags matches literal pattern case-insensitively")
void iqFlagsMatchLiteralCaseInsensitively() {
assertTrue(ask("REGEX(\"a?+*.{}()[]c\", \"a?+*.{}()[]C\", \"iq\")"));
assertFalse(ask("REGEX(\"abc\", \"a?+*.{}()[]C\", \"iq\")"));
}

@Test
@DisplayName("REGEX with [] expression matches 'b' in character class")
void bracketExpressionMatchesB() {
assertTrue(ask("REGEX(\"abc\", \"a[b\\\\n]c\")"));
}

@Test
@DisplayName("REGEX with [] expression matches newline in character class")
void bracketExpressionMatchesNewline() {
assertTrue(ask("REGEX(\"a\\nc\", \"a[b\\\\n]c\")"));
}

@Test
@DisplayName("REGEX with [] expression does not match unrelated characters")
void bracketExpressionDoesNotMatchOther() {
assertFalse(ask("REGEX(\"adc\", \"a[b\\\\n]c\")"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@

class SparqlTermResolverTest {

@Test
void decodesSparqlEscapesExactlyOnce() {
SparqlTermResolver resolver = new SparqlTermResolver(null);

assertEquals("\t\n\r\b\f\"'\\", resolver.unquoteLexical("\"\\t\\n\\r\\b\\f\\\"\\'\\\\\""));
assertEquals("\\n", resolver.unquoteLexical("\"\\\\n\""));
assertEquals("a\nb", resolver.unquoteLexical("\"\"\"a\\nb\"\"\""));
assertEquals("a\tb", resolver.unquoteLexical("'''a\\tb'''"));
}

@Test
void preservesUnknownEscapesAndTrailingBackslash() {
assertEquals("\\d", SparqlTermResolver.processSparqlEscapes("\\d"));
assertEquals("end\\", SparqlTermResolver.processSparqlEscapes("end\\"));
assertEquals("", SparqlTermResolver.processSparqlEscapes(""));
}

@Test
void resolvesTermsFromItsOwnPrologue() {
QueryPrologueAst prologue = new QueryPrologueAst(
Expand Down
Loading