From 8dac313955aa6693bc024cdbb644c58968db7ec0 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 18 Sep 2024 09:02:56 +0200 Subject: [PATCH] kie-issues#1448: `matches()` function wrongly behaves (#6085) * matches and replace using saxon * saxon usage simplified and modified tests * clean up code as per standards * changes as per suggestions * changes as per suggestions and restored the earlier tests * changes as per suggestions and restored the earlier tests * changes as per suggestions * exceptions handled and changes to util class * wip * wip * wip * wip * wip * CR * CR * CR * CR * Notice updated * CR --------- Co-authored-by: bncriju --- NOTICE | 4 + kie-dmn/kie-dmn-feel/pom.xml | 15 ++ .../runtime/functions/MatchesFunction.java | 67 ++----- .../runtime/functions/ReplaceFunction.java | 21 +- .../org/kie/dmn/feel/util/XQueryImplUtil.java | 62 ++++++ .../dmn/feel/runtime/FEELFunctionsTest.java | 9 +- .../functions/MatchesFunctionTest.java | 188 ++++++++++-------- .../functions/ReplaceFunctionTest.java | 112 +++++++---- .../kie/dmn/feel/util/XQueryImplUtilTest.java | 117 +++++++++++ 9 files changed, 409 insertions(+), 186 deletions(-) create mode 100644 kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java create mode 100644 kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java diff --git a/NOTICE b/NOTICE index 2fe21eeead1..43c4b92efb0 100644 --- a/NOTICE +++ b/NOTICE @@ -14,6 +14,10 @@ This product also includes the following third-party components: Downloaded from: https://lunrjs.com/ License: MIT +* Saxon-HE + Downloaded from: https://www.saxonica.com/ + License: Mozilla Public License 2.0 + * search-ui Downloaded from: https://gitlab.com/antora/antora-lunr-extension License: Mozilla Public License 2.0 diff --git a/kie-dmn/kie-dmn-feel/pom.xml b/kie-dmn/kie-dmn-feel/pom.xml index 51fbef420b7..545d181c628 100644 --- a/kie-dmn/kie-dmn-feel/pom.xml +++ b/kie-dmn/kie-dmn-feel/pom.xml @@ -38,8 +38,19 @@ org.kie.dmn.feel 2 true + 12.5 + + + + net.sf.saxon + Saxon-HE + ${version.net.sf.saxon.Saxon-HE} + + + + @@ -52,6 +63,10 @@ + + net.sf.saxon + Saxon-HE + org.antlr antlr4-runtime diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java index 79ff2a01368..2acb38ad269 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java @@ -18,15 +18,9 @@ */ package org.kie.dmn.feel.runtime.functions; -import java.security.InvalidParameterException; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; - import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity; import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; +import org.kie.dmn.feel.util.XQueryImplUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,58 +38,23 @@ public FEELFnResult invoke(@ParameterName("input") String input, @Param } public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, @ParameterName("flags") String flags) { - try { - return matchFunctionWithFlags(input,pattern,flags); - } catch ( PatternSyntaxException t ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "is invalid and can not be compiled", t ) ); - } catch (InvalidParameterException t ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, t.getMessage(), "cannot be null", t ) ); - } catch (IllegalArgumentException t ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "flags", "contains unknown flags", t ) ); - } catch (Throwable t) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "is invalid and can not be compiled", t ) ); - } - } - - static FEELFnResult matchFunctionWithFlags(String input, String pattern, String flags) { log.debug("Input: {} , Pattern: {}, Flags: {}", input, pattern, flags); + if ( input == null ) { - throw new InvalidParameterException("input"); + return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "input", "cannot be null" ) ); } if ( pattern == null ) { - throw new InvalidParameterException("pattern"); + return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "cannot be null" ) ); } - final String flagsString; - if (flags != null && !flags.isEmpty()) { - checkFlags(flags); - if(!flags.contains("U")){ - flags += "U"; - } - flagsString = String.format("(?%s)", flags); - } else { - flagsString = ""; - } - log.debug("flagsString: {}", flagsString); - String stringToBeMatched = flagsString + pattern; - log.debug("stringToBeMatched: {}", stringToBeMatched); - Pattern p=Pattern.compile(stringToBeMatched); - Matcher m = p.matcher( input ); - boolean matchFound=m.find(); - log.debug("matchFound: {}", matchFound); - return FEELFnResult.ofResult(matchFound); - } - static void checkFlags(String flags) { - Set allowedChars = Set.of('s','i','x','m'); - boolean isValidFlag= flags.chars() - .mapToObj(c -> (char) c) - .allMatch(allowedChars::contains) - && flags.chars() - .mapToObj(c -> (char) c) - .collect(Collectors.toSet()) - .size() == flags.length(); - if(!isValidFlag){ - throw new IllegalArgumentException("Not a valid flag parameter " +flags); - } + try { + return FEELFnResult.ofResult(XQueryImplUtil.executeMatchesFunction(input, pattern, flags)); + } catch (Exception e) { + String errorMessage = String.format("Provided parameters lead to an error. Input: '%s', Pattern: '%s', Flags: '%s'. ", input, pattern, flags); + if (e.getMessage() != null && !e.getMessage().isEmpty()) { + errorMessage += e.getMessage(); + } + return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, errorMessage, e)); + } } } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ReplaceFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ReplaceFunction.java index 723efcbdd36..a7ae497870a 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ReplaceFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ReplaceFunction.java @@ -20,6 +20,7 @@ import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity; import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; +import org.kie.dmn.feel.util.XQueryImplUtil; public class ReplaceFunction extends BaseFEELFunction { @@ -30,12 +31,12 @@ private ReplaceFunction() { super( "replace" ); } - public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, + public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, @ParameterName( "replacement" ) String replacement ) { return invoke(input, pattern, replacement, null); } - public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, + public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, @ParameterName( "replacement" ) String replacement, @ParameterName("flags") String flags) { if ( input == null ) { return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "input", "cannot be null" ) ); @@ -47,14 +48,16 @@ public FEELFnResult invoke(@ParameterName("input") String input, @Parame return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "replacement", "cannot be null" ) ); } - final String flagsString; - if (flags != null && !flags.isEmpty()) { - flagsString = "(?" + flags + ")"; - } else { - flagsString = ""; + try { + return FEELFnResult.ofResult(XQueryImplUtil.executeReplaceFunction(input, pattern, replacement, flags)); + } catch (Exception e) { + String errorMessage = String.format("Provided parameters lead to an error. Input: '%s', Pattern: '%s', Replacement: '%s', Flags: '%s'. ", + input, pattern, replacement, flags); + if (e.getMessage() != null && !e.getMessage().isEmpty()) { + errorMessage += e.getMessage(); + } + return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, errorMessage, e)); } - - return FEELFnResult.ofResult( input.replaceAll( flagsString + pattern, replacement ) ); } } 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 new file mode 100644 index 00000000000..a3ae1c6e53a --- /dev/null +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/XQueryImplUtil.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.dmn.feel.util; + +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.XdmAtomicValue; +import net.sf.saxon.s9api.XdmItem; +import net.sf.saxon.s9api.XQueryCompiler; +import net.sf.saxon.s9api.XQueryEvaluator; +import net.sf.saxon.s9api.XQueryExecutable; +import net.sf.saxon.s9api.SaxonApiException; + +public class XQueryImplUtil { + + public static Boolean executeMatchesFunction(String input, String pattern, String flags) { + flags = flags == null ? "" : flags; + String xQueryExpression = String.format("matches('%s', '%s', '%s')", input, pattern, flags); + return evaluateXQueryExpression(xQueryExpression, Boolean.class); + } + + public static String executeReplaceFunction(String input, String pattern, String replacement, String flags) { + flags = flags == null ? "" : flags; + String xQueryExpression = String.format("replace('%s', '%s', '%s', '%s')", input, pattern, replacement, flags); + return evaluateXQueryExpression(xQueryExpression, String.class); + } + + static T evaluateXQueryExpression(String expression, Class expectedTypeResult) { + try { + Processor processor = new Processor(false); + XQueryCompiler compiler = processor.newXQueryCompiler(); + XQueryExecutable executable = compiler.compile(expression); + XQueryEvaluator queryEvaluator = executable.load(); + XdmItem resultItem = queryEvaluator.evaluateSingle(); + + Object value = switch (expectedTypeResult.getSimpleName()) { + case "Boolean" -> ((XdmAtomicValue) resultItem).getBooleanValue(); + case "String" -> resultItem.getStringValue(); + default -> throw new UnsupportedOperationException("Type " + expectedTypeResult.getSimpleName() + " is not managed."); + }; + + return expectedTypeResult.cast(value); + } catch (SaxonApiException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java index 3707929e3bd..a199e8f1f6f 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java @@ -43,7 +43,6 @@ private static Collection data() { final Object[][] cases = new Object[][] { // constants { "string(1.1)", "1.1" , null}, - { "replace( \" foo bar zed \", \"^(\\s)+|(\\s)+$|\\s+(?=\\s)\", \"\" )", "foo bar zed", null }, { "string(null)", null, null}, { "string(date(\"2016-08-14\"))", "2016-08-14" , null}, { "string(\"Happy %.0fth birthday, Mr %s!\", 38, \"Doe\")", "Happy 38th birthday, Mr Doe!", null}, @@ -85,6 +84,14 @@ private static Collection data() { { "matches(\"one\\ntwo\\nthree\", \"^two$\", \"m\")", Boolean.TRUE , null}, // MULTILINE flag set by "m" { "matches(\"FoO\", \"foo\")", Boolean.FALSE , null}, { "matches(\"FoO\", \"foo\", \"i\")", Boolean.TRUE , null}, // CASE_INSENSITIVE flag set by "i" + { "matches(\"\\u212A\", \"k\", \"i\")", Boolean.TRUE , null}, + { "matches(\"O\", \"[A-Z-[OI]]\", \"i\")", Boolean.FALSE , null}, + { "matches(\"i\", \"[A-Z-[OI]]\", \"i\")", Boolean.FALSE , null}, + { "matches(\"hello world\", \"hello\\ sworld\", \"x\")", Boolean.TRUE , null}, + { "matches(\"abracadabra\", \"bra\", \"p\")", null , FEELEvent.Severity.ERROR}, + { "matches(\"input\", \"pattern\", \" \")", null , FEELEvent.Severity.ERROR}, + { "matches(\"input\", \"pattern\", \"X\")", null , FEELEvent.Severity.ERROR}, + { "matches(\"h\", \"(.)\\2\")", null , FEELEvent.Severity.ERROR}, { "replace(\"banana\",\"a\",\"o\")", "bonono" , null}, { "replace(\"banana\",\"(an)+\", \"**\")", "b**a" , null}, { "replace(\"banana\",\"[aeiouy]\",\"[$0]\")", "b[a]n[a]n[a]" , null}, diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java index b12612bb04a..589f37e3d2d 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java @@ -19,116 +19,146 @@ package org.kie.dmn.feel.runtime.functions; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import java.security.InvalidParameterException; -import java.util.regex.PatternSyntaxException; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; -import static org.mockito.Mockito.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; class MatchesFunctionTest { - @Test - void invokeNull() { - assertThatExceptionOfType(InvalidParameterException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags(null, null, null)); - assertThatExceptionOfType(InvalidParameterException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags(null, "test", null)); - assertThatExceptionOfType(InvalidParameterException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("test", null, null)); + private static final MatchesFunction matchesFunction = MatchesFunction.INSTANCE; + + @ParameterizedTest + @MethodSource("invokeNullTestData") + void invokeNullTest(String input, String pattern) { + FunctionTestUtil.assertResultError(matchesFunction.invoke(input, pattern), InvalidParametersEvent.class); } - @Test - void invokeUnsupportedFlags() { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("foobar", "fo.bar", "g")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "p")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "X")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", " ")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "iU")); + private static Object[][] invokeNullTestData() { + return new Object[][] { + { null, null }, + { null, "test" }, + { "test", null }, + }; } - @Test - void invokeWithoutFlagsMatch() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("test", "test",null), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^fo*b",null), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", ""), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("abracadabra", "bra",null), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("(?xi)[hello world()]", "hello",null), true); + @ParameterizedTest + @MethodSource("invokeUnsupportedFlagsTestData") + void invokeUnsupportedFlagsTest(String input, String pattern, String flags) { + FunctionTestUtil.assertResultError(matchesFunction.invoke(input, pattern, flags), InvalidParametersEvent.class); + } + + private static Object[][] invokeUnsupportedFlagsTestData() { + return new Object[][] { + { "foobar", "fo.bar", "g" }, + { "abracadabra", "bra", "p" }, + { "abracadabra", "bra", "X" }, + { "abracadabra", "bra", "iU" }, + { "abracadabra", "bra", "iU asd" }, + }; } - @Test - void invokeWithoutFlagsNotMatch() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("test", "testt",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^fo*bb",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "fo.bar",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("h", "(.)\3",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("h", "(.)\2",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("input", "\3",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?iU)(?iU)(ab)[|cd]",null), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?x)(?i)hello world","i"), false); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?xi)hello world",null), false); + @ParameterizedTest + @MethodSource("invokeWithoutFlagsMatchTestData") + void invokeWithoutFlagsMatchTest(String input, String pattern, Boolean expectedResult) { + FunctionTestUtil.assertResult(matchesFunction.invoke(input, pattern), expectedResult); } - @Test - void invokeWithFlagDotAll() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "fo.bar", "s"), true); + private static Object[][] invokeWithoutFlagsMatchTestData() { + return new Object[][] { + { "test", "test", true }, + { "foobar", "^fo*b", true }, + { "abracadabra", "bra", true }, + { "abracadabra", "bra", true }, + { "(?xi)[hello world()]", "hello", true } + }; } - @Test - void invokeWithFlagMultiline() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "^bar", "m"), true); + @ParameterizedTest + @MethodSource("invokeNullOrEmptyFlagsMatchTestData") + void invokeNullOrEmptyFlagsMatchTest(String input, String pattern, String flags, Boolean expectedResult) { + FunctionTestUtil.assertResult(matchesFunction.invoke(input, pattern, flags), expectedResult); } - @Test - void invokeWithFlagCaseInsensitive() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^Fo*bar", "i"), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^Fo*bar", "i"), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("\u212A", "k","i"), true); - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("\u212A", "K","i"), true); + private static Object[][] invokeNullOrEmptyFlagsMatchTestData() { + return new Object[][] { + { "test", "test", null, true }, + { "foobar", "^fo*b", null, true }, + { "abracadabra", "bra", null, true }, + { "abracadabra", "bra", "", true }, + { "(?xi)[hello world()]", "hello", null, true } + }; + } + + @ParameterizedTest + @MethodSource("invokeInvalidRegexTestData") + void invokeInvalidRegexTest(String input, String pattern, String flags) { + FunctionTestUtil.assertResultError(matchesFunction.invoke(input, pattern, flags), InvalidParametersEvent.class); + } + + private static Object[][] invokeInvalidRegexTestData() { + return new Object[][] { + { "testString", "(?=\\\\s)", null }, + { "fo\nbar", "(?iU)(?iU)(ab)[|cd]", null }, + { "fo\nbar", "(?x)(?i)hello world", "i" }, + { "fo\nbar", "(?xi)hello world", null }, + }; + } + + @ParameterizedTest + @MethodSource("invokeWithoutFlagsNotMatchTestData") + void invokeWithoutFlagsNotMatchTest(String input, String pattern, String flags, Boolean expectedResult) { + FunctionTestUtil.assertResult(matchesFunction.invoke(input, pattern, flags), expectedResult); + } + + private static Object[][] invokeWithoutFlagsNotMatchTestData() { + return new Object[][] { + { "test", "testt", null, false }, + { "foobar", "^fo*bb", null, false }, + { "h", "(.)\3", null, false }, + { "h", "(.)\2", null, false }, + { "input", "\3", null, false } + }; + } + + @ParameterizedTest + @MethodSource("invokeWithFlagCaseInsensitiveTestData") + void invokeWithoutFlagsPatternTest(String input, String pattern, String flags, Boolean expectedResult) { + FunctionTestUtil.assertResult(matchesFunction.invoke(input, pattern, flags), expectedResult); + } + + private static Object[][] invokeWithFlagCaseInsensitiveTestData() { + return new Object[][] { + { "foobar", "^Fo*bar", "i", true }, + { "foobar", "^Fo*bar", "i", true }, + { "\u212A", "k", "i", true }, + { "\u212A", "K", "i", true } + }; } @Test - void invokeWithFlagComments() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("hello world", "hello"+"\"+ sworld", "x"), false); + void invokeWithFlagDotAll() { + FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "fo.bar", "s"), true); } @Test - void invokeWithAllFlags() { - FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "Fo.^bar", "smi"), true); + void invokeWithFlagMultiline() { + FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "^bar", "m"), true); } @Test - void checkForPatternTest() { - assertThatExceptionOfType(PatternSyntaxException.class).isThrownBy(() -> MatchesFunction.matchFunctionWithFlags("foobar", "(abc|def(ghi", "i")); + void invokeWithFlagComments() { + FunctionTestUtil.assertResult(matchesFunction.invoke("hello world", "hello"+"\"+ sworld", "x"), false); } @Test - void checkFlagsTest() { - assertThatNoException().isThrownBy(() -> MatchesFunction.checkFlags("s")); - assertThatNoException().isThrownBy(() -> MatchesFunction.checkFlags("i")); - assertThatNoException().isThrownBy(() -> MatchesFunction.checkFlags("sx")); - assertThatNoException().isThrownBy(() -> MatchesFunction.checkFlags("six")); - assertThatNoException().isThrownBy(() -> MatchesFunction.checkFlags("sixm")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("a")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("sa")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("siU@")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("siUxU")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("ss")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("siiU")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("si U")); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> MatchesFunction.checkFlags("U")); + void invokeWithAllFlags() { + FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "Fo.^bar", "smi"), true); } @Test - void checkMatchFunctionWithFlagsInvocation() { - MatchesFunction matchesFunctionSpied = spy(MatchesFunction.INSTANCE); - matchesFunctionSpied.invoke("input", "pattern"); - verify(matchesFunctionSpied, times(1)).invoke("input", "pattern", null); - try (MockedStatic matchesFunctionMocked = mockStatic(MatchesFunction.class)) { - matchesFunctionSpied.invoke("input", "pattern"); - matchesFunctionMocked.verify(() -> MatchesFunction.matchFunctionWithFlags("input", "pattern", null)); - matchesFunctionSpied.invoke("input", "pattern", "flags"); - matchesFunctionMocked.verify(() -> MatchesFunction.matchFunctionWithFlags("input", "pattern", "flags")); - } + void checkForPatternTest() { + FunctionTestUtil.assertResultError(matchesFunction.invoke("foobar", "(abc|def(ghi", "i"), InvalidParametersEvent.class); } } \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ReplaceFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ReplaceFunctionTest.java index 06e1bc4da0e..45d1334f358 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ReplaceFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ReplaceFunctionTest.java @@ -19,62 +19,88 @@ package org.kie.dmn.feel.runtime.functions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; class ReplaceFunctionTest { private static final ReplaceFunction replaceFunction = ReplaceFunction.INSTANCE; - @Test - void invokeNull() { - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, null), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", null, null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", "test", null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", null), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", "ttt"), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, "ttt"), InvalidParametersEvent.class); + @ParameterizedTest + @MethodSource("invokeNullTestData") + void invokeNullTest(String input, String pattern, String replacement) { + FunctionTestUtil.assertResultError(replaceFunction.invoke(input, pattern, replacement), InvalidParametersEvent.class); } - @Test - void invokeNullWithFlags() { - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, null, null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", null, null, null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", "test", null, null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", null, null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", "ttt", null), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, "ttt", null), - InvalidParametersEvent.class); - - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, null, "s"), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", null, null, "s"), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", "test", null, "s"), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", null, "s"), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, "test", "ttt", "s"), - InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(replaceFunction.invoke(null, null, "ttt", "s"), - InvalidParametersEvent.class); + private static Object[][] invokeNullTestData() { + return new Object[][] { + { null, null, null }, + { "testString", null, null }, + { "testString", "test", null }, + { null, "test", null }, + { null, "test", "ttt" }, + { null, null, "ttt" } + }; } - @Test - void invokeWithoutFlagsPatternMatches() { - FunctionTestUtil.assertResult(replaceFunction.invoke("testString", "^test", "ttt"), "tttString"); - FunctionTestUtil.assertResult(replaceFunction.invoke("testStringtest", "^test", "ttt"), "tttStringtest"); + @ParameterizedTest + @MethodSource("invokeNullWithFlagsTestData") + void invokeNullWithFlagsTest(String input, String pattern, String replacement, String flags) { + FunctionTestUtil.assertResultError(replaceFunction.invoke(input, pattern, replacement, flags), InvalidParametersEvent.class); + } + + private static Object[][] invokeNullWithFlagsTestData() { + return new Object[][] { + { null, null, null, null }, + { "testString", null, null, null }, + { "testString", "test", null, null }, + { null, "test", null, null }, + { null, "test", "ttt", null }, + { null, null, "ttt", null }, + { null, null, null, "s" }, + { "testString", null, null, "s" }, + { "testString", "test", null, "s" }, + { null, "test", null, "s" }, + { null, "test", "ttt", "s" }, + { null, null, "ttt", "s" }, + }; + } + + @ParameterizedTest + @MethodSource("invokeUnsupportedFlagsTestData") + void invokeUnsupportedFlagsTest(String input, String pattern, String replacement, String flags) { + FunctionTestUtil.assertResultError(replaceFunction.invoke(input, pattern, replacement, flags), InvalidParametersEvent.class); + } + + private static Object[][] invokeUnsupportedFlagsTestData() { + return new Object[][] { + { "testString", "^test", "ttt", "g" }, + { "testString", "^test", "ttt", "p" }, + { "testString", "^test", "ttt", "X" }, + { "testString", "^test", "ttt", "iU" }, + { "testString", "^test", "ttt", "iU asd" }, + }; + } + + @ParameterizedTest + @MethodSource("invokeWithoutFlagsPatternTestData") + void invokeWithoutFlagsPatternTest(String input, String pattern, String replacement, String expectedResult) { + FunctionTestUtil.assertResult(replaceFunction.invoke(input, pattern, replacement), expectedResult); + } + + private static Object[][] invokeWithoutFlagsPatternTestData() { + return new Object[][] { + { "testString", "^test", "ttt", "tttString" }, + { "testStringtest", "^test", "ttt", "tttStringtest" }, + { "testString", "ttest", "ttt", "testString" }, + { "testString", "$test", "ttt", "testString" } + }; } @Test - void invokeWithoutFlagsPatternNotMatches() { - FunctionTestUtil.assertResult(replaceFunction.invoke("testString", "ttest", "ttt"), "testString"); - FunctionTestUtil.assertResult(replaceFunction.invoke("testString", "$test", "ttt"), "testString"); + void invokeInvalidRegExPattern() { + FunctionTestUtil.assertResultError(replaceFunction.invoke("testString", "(?=\\s)", "ttt"), InvalidParametersEvent.class); } @Test 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 new file mode 100644 index 00000000000..6cc71d38a6a --- /dev/null +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/XQueryImplUtilTest.java @@ -0,0 +1,117 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.dmn.feel.util; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class XQueryImplUtilTest { + + @ParameterizedTest + @MethodSource("executeMatchesFunctionTestData") + void executeMatchesFunctionTest(String input, String pattern, String flags, boolean expected) { + assertThat(XQueryImplUtil.executeMatchesFunction(input, pattern, flags)).isEqualTo(expected); + } + + private static Object[][] executeMatchesFunctionTestData() { + return new Object[][] { + { "test", "^test", "i", true }, + { "fo\nbar", "o.b", null, false }, + { "TEST", "test", "i", true }, + }; + } + + @ParameterizedTest + @MethodSource("executeMatchesFunctionInvokingExceptionTestData") + void executeMatchesFunctionInvokingExceptionTest(String input, String pattern, String flags, + String exceptionMessage) { + assertThatThrownBy(() -> XQueryImplUtil.executeMatchesFunction(input, pattern, flags)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining(exceptionMessage); + } + + private static Object[][] executeMatchesFunctionInvokingExceptionTestData() { + return new Object[][] { + { "test", "^test", "g", "Unrecognized flag" }, + { "test", "(?=\\s)", null, "No expression before quantifier" }, + { "test", "(.)\\2", "i", "invalid backreference \\2" }, + }; + } + + @ParameterizedTest + @MethodSource("executeReplaceFunctionTestData") + void executeReplaceFunctionTest(String input, String pattern, String replacement, String flags, String expected) { + assertThat(XQueryImplUtil.executeReplaceFunction(input, pattern, replacement, flags)).isEqualTo(expected); + } + + private static Object[][] executeReplaceFunctionTestData() { + return new Object[][] { + { "testString", "^test", "ttt", "", "tttString" }, + { "fo\nbar", "o.b", "ttt", "s", "ftttar" }, + }; + } + + @ParameterizedTest + @MethodSource("executeReplaceFunctionInvokingExceptionTestData") + void executeReplaceFunctionInvokingExceptionTest(String input, String pattern, String replacement, String flags, + Class expectedException, String exceptionMessage) { + assertThatThrownBy(() -> XQueryImplUtil.executeReplaceFunction(input, pattern, replacement, flags)) + .isInstanceOf(expectedException).hasMessageContaining(exceptionMessage); + } + + private static Object[][] executeReplaceFunctionInvokingExceptionTestData() { + return new Object[][] { + { "fo\nbar", "o.b", "ttt", "g", IllegalArgumentException.class, "Unrecognized flag" }, + { "test", "(?=\\s)", "ttt", null, IllegalArgumentException.class, "No expression before quantifier" }, + { "test", "(.)\\2", "ttt", null, IllegalArgumentException.class, "invalid backreference \\2" }, + }; + } + + @ParameterizedTest + @MethodSource("evaluateXQueryExpressionValidParametersTestData") + void evaluateXQueryExpressionValidParametersTest(String expression, Class returnTypeClass, Object expectedResult) { + assertThat(XQueryImplUtil.evaluateXQueryExpression(expression, returnTypeClass)).isEqualTo(expectedResult); + + } + + private static Object[][] evaluateXQueryExpressionValidParametersTestData() { + return new Object[][] { + { "matches('test', '^test', 'i')", Boolean.class, true }, + { "matches('fo\\nbar', 'o.b', '')", Boolean.class, false }, + { "replace('testString', '^test', 'ttt', '')", String.class, "tttString" } + }; + } + + @ParameterizedTest + @MethodSource("evaluateXQueryExpressionInvalidParametersTestData") + void evaluateXQueryExpressionInvalidParametersTest(String expression, Class returnTypeClass) { + assertThatThrownBy(() -> XQueryImplUtil.evaluateXQueryExpression(expression, returnTypeClass)) + .isInstanceOf(UnsupportedOperationException.class); + } + + private static Object[][] evaluateXQueryExpressionInvalidParametersTestData() { + return new Object[][] { + { "matches('test', '^test', 'i')", Integer.class }, + { "replace('testString', '^test', 'ttt', '')", Double.class }, + }; + } + +} \ No newline at end of file