diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java index f3ef8e49e..292569450 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java @@ -101,7 +101,7 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-iri-to-uri // P1: https://www.w3.org/TR/xpath-functions-31/#func-last // P1: https://www.w3.org/TR/xpath-functions-31/#func-lower-case - // P1: https://www.w3.org/TR/xpath-functions-31/#func-matches + // https://www.w3.org/TR/xpath-functions-31/#func-matches registerFunction(FnMatches.SIGNATURE_TWO_ARG); registerFunction(FnMatches.SIGNATURE_THREE_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-max @@ -164,7 +164,10 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-timezone-from-date // https://www.w3.org/TR/xpath-functions-31/#func-timezone-from-dateTime // https://www.w3.org/TR/xpath-functions-31/#func-timezone-from-time - // P1: https://www.w3.org/TR/xpath-functions-31/#func-tokenize + // https://www.w3.org/TR/xpath-functions-31/#func-tokenize + registerFunction(FnTokenize.SIGNATURE_ONE_ARG); + registerFunction(FnTokenize.SIGNATURE_TWO_ARG); + registerFunction(FnTokenize.SIGNATURE_THREE_ARG); // P1: https://www.w3.org/TR/xpath-functions-31/#func-translate // https://www.w3.org/TR/xpath-functions-31/#func-true registerFunction(FnTrue.SIGNATURE); diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatches.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatches.java index 8851faecd..b8fa6896e 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatches.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatches.java @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ package gov.nist.secauto.metaschema.core.metapath.function.library; @@ -75,28 +79,24 @@ public final class FnMatches { .functionHandler(FnMatches::executeThreeArg) .build(); - @SuppressWarnings("unused") @NonNull private static ISequence executeTwoArg( - @NonNull IFunction function, + @SuppressWarnings("unused") @NonNull IFunction function, @NonNull List> arguments, - @NonNull DynamicContext dynamicContext, - IItem focus) { + @SuppressWarnings("unused") @NonNull DynamicContext dynamicContext, + @SuppressWarnings("unused") IItem focus) { IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true)); IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true))); return execute(input, pattern, IStringItem.valueOf("")); } - @SuppressWarnings("unused") - @NonNull private static ISequence executeThreeArg( - @NonNull IFunction function, + @SuppressWarnings("unused") @NonNull IFunction function, @NonNull List> arguments, - @NonNull DynamicContext dynamicContext, - IItem focus) { - + @SuppressWarnings("unused") @NonNull DynamicContext dynamicContext, + @SuppressWarnings("unused") IItem focus) { IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true)); IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true))); IStringItem flags = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(2).getFirstItem(true))); @@ -104,37 +104,16 @@ private static ISequence executeThreeArg( return execute(input, pattern, flags); } - @SuppressWarnings("PMD.OnlyOneReturn") @NonNull private static ISequence execute( @Nullable IStringItem input, @NonNull IStringItem pattern, @NonNull IStringItem flags) { - if (input == null) { - return ISequence.empty(); - } - - return ISequence.of(fnMatches(input, pattern, flags)); - } - - /** - * Implements fn:matches. - * - * @param input - * the string to match against - * @param pattern - * the regular expression to use for matching - * @param flags - * matching options - * @return {@link IBooleanItem#TRUE} if the pattern matches or - * {@link IBooleanItem#FALSE} otherwise - */ - public static IBooleanItem fnMatches( - @NonNull IStringItem input, - @NonNull IStringItem pattern, - @NonNull IStringItem flags) { - return IBooleanItem.valueOf(fnMatches(input.asString(), pattern.asString(), flags.asString())); + return input == null + ? ISequence.empty() + : ISequence.of( + IBooleanItem.valueOf( + fnMatches(input.asString(), pattern.asString(), flags.asString()))); } /** @@ -154,9 +133,15 @@ public static boolean fnMatches(@NonNull String input, @NonNull String pattern, return Pattern.compile(pattern, RegexUtil.parseFlags(flags)) .matcher(input).find(); } catch (PatternSyntaxException ex) { - throw new RegularExpressionMetapathException(RegularExpressionMetapathException.INVALID_EXPRESSION, ex); + throw new RegularExpressionMetapathException( + RegularExpressionMetapathException.INVALID_EXPRESSION, + "Invalid regular expression pattern: '" + pattern + "'", + ex); } catch (IllegalArgumentException ex) { - throw new RegularExpressionMetapathException(RegularExpressionMetapathException.INVALID_FLAG, ex); + throw new RegularExpressionMetapathException( + RegularExpressionMetapathException.INVALID_FLAG, + "Invalid regular expression flags: '" + flags + "'", + ex); } } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenize.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenize.java new file mode 100644 index 000000000..6256030e6 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenize.java @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import gov.nist.secauto.metaschema.core.metapath.DynamicContext; +import gov.nist.secauto.metaschema.core.metapath.ISequence; +import gov.nist.secauto.metaschema.core.metapath.MetapathConstants; +import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils; +import gov.nist.secauto.metaschema.core.metapath.function.IArgument; +import gov.nist.secauto.metaschema.core.metapath.function.IFunction; +import gov.nist.secauto.metaschema.core.metapath.function.regex.RegexUtil; +import gov.nist.secauto.metaschema.core.metapath.function.regex.RegularExpressionMetapathException; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem; +import gov.nist.secauto.metaschema.core.util.CollectionUtil; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Implements fn:tokenize. + */ +public final class FnTokenize { + @NonNull + static final IFunction SIGNATURE_ONE_ARG = IFunction.builder() + .name("tokenize") + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("input") + .type(IStringItem.class) + .zeroOrOne() + .build()) + .returnType(IStringItem.class) + .returnZeroOrMore() + .functionHandler(FnTokenize::executeOneArg) + .build(); + @NonNull + static final IFunction SIGNATURE_TWO_ARG = IFunction.builder() + .name("tokenize") + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("input") + .type(IStringItem.class) + .zeroOrOne() + .build()) + .argument(IArgument.builder() + .name("pattern") + .type(IStringItem.class) + .one() + .build()) + .returnType(IStringItem.class) + .returnZeroOrMore() + .functionHandler(FnTokenize::executeTwoArg) + .build(); + + @NonNull + static final IFunction SIGNATURE_THREE_ARG = IFunction.builder() + .name("tokenize") + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("input") + .type(IStringItem.class) + .zeroOrOne() + .build()) + .argument(IArgument.builder() + .name("pattern") + .type(IStringItem.class) + .one() + .build()) + .argument(IArgument.builder() + .name("flags") + .type(IStringItem.class) + .one() + .build()) + .returnType(IStringItem.class) + .returnZeroOrMore() + .functionHandler(FnTokenize::executeThreeArg) + .build(); + + @NonNull + private static ISequence executeOneArg( + @SuppressWarnings("unused") @NonNull IFunction function, + @NonNull List> arguments, + @SuppressWarnings("unused") @NonNull DynamicContext dynamicContext, + @SuppressWarnings("unused") IItem focus) { + IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true)); + + return input == null + ? ISequence.empty() + : ISequence.of(ObjectUtils.notNull( + fnTokenize(input.normalizeSpace().asString(), " ", "").stream() + .map(IStringItem::valueOf))); + } + + @NonNull + private static ISequence executeTwoArg( + @SuppressWarnings("unused") @NonNull IFunction function, + @NonNull List> arguments, + @SuppressWarnings("unused") @NonNull DynamicContext dynamicContext, + @SuppressWarnings("unused") IItem focus) { + IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true)); + IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true))); + + return execute(input, pattern, IStringItem.valueOf("")); + } + + @NonNull + private static ISequence executeThreeArg( + @SuppressWarnings("unused") @NonNull IFunction function, + @NonNull List> arguments, + @SuppressWarnings("unused") @NonNull DynamicContext dynamicContext, + @SuppressWarnings("unused") IItem focus) { + + IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true)); + IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true))); + IStringItem flags = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(2).getFirstItem(true))); + + return execute(input, pattern, flags); + } + + @SuppressWarnings("PMD.OnlyOneReturn") + @NonNull + private static ISequence execute( + @Nullable IStringItem input, + @NonNull IStringItem pattern, + @NonNull IStringItem flags) { + return input == null + ? ISequence.empty() + : fnTokenize(input, pattern, flags); + } + + /** + * Implements fn:tokenize. + * + * @param input + * the string to tokenize + * @param pattern + * the regular expression to use for identifying token boundaries + * @param flags + * matching options + * @return the sequence of tokens + */ + @NonNull + public static ISequence fnTokenize( + @NonNull IStringItem input, + @NonNull IStringItem pattern, + @NonNull IStringItem flags) { + return ISequence.of(ObjectUtils.notNull( + fnTokenize(input.asString(), pattern.asString(), flags.asString()).stream() + .map(IStringItem::valueOf))); + } + + /** + * Implements fn:tokenize. + * + * @param input + * the string to match against + * @param pattern + * the regular expression to use for matching + * @param flags + * matching options + * @return the stream of tokens + */ + @SuppressWarnings({ "PMD.OnlyOneReturn", "PMD.CyclomaticComplexity" }) + @NonNull + public static List fnTokenize(@NonNull String input, @NonNull String pattern, @NonNull String flags) { + if (input.isEmpty()) { + return CollectionUtil.emptyList(); + } + + try { + Matcher matcher = Pattern.compile(pattern, RegexUtil.parseFlags(flags)).matcher(input); + + int lastPosition = 0; + int length = input.length(); + + List result = new LinkedList<>(); + while (matcher.find()) { + String group = matcher.group(); + if (group.isEmpty()) { + throw new RegularExpressionMetapathException(RegularExpressionMetapathException.MATCHES_ZERO_LENGTH_STRING, + String.format("Pattern '%s' will match a zero-length string.", pattern)); + } + + int start = matcher.start(); + if (start == 0) { + result.add(""); + } else { + result.add(input.substring(lastPosition, start)); + } + + lastPosition = matcher.end(); + } + + if (lastPosition == length) { + result.add(""); + } else { + result.add(input.substring(lastPosition, length)); + } + + return result; + } catch (PatternSyntaxException ex) { + throw new RegularExpressionMetapathException(RegularExpressionMetapathException.INVALID_EXPRESSION, ex); + } catch (IllegalArgumentException ex) { + throw new RegularExpressionMetapathException(RegularExpressionMetapathException.INVALID_FLAG, ex); + } + } + + private FnTokenize() { + // disable construction + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegexUtil.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegexUtil.java index 47ebec2b7..204773075 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegexUtil.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegexUtil.java @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ package gov.nist.secauto.metaschema.core.metapath.function.regex; @@ -18,12 +22,12 @@ public final class RegexUtil { * @return the bitmask */ public static int parseFlags(@NonNull String flags) { - return flags.chars() + return flags.codePoints() .map(i -> characterToFlag((char) i)) .reduce(0, (mask, flag) -> mask | flag); } - private static int characterToFlag(Character ch) { + private static int characterToFlag(char ch) { int retval; switch (ch) { case 's': diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegularExpressionMetapathException.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegularExpressionMetapathException.java index d245c8e71..db981e975 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegularExpressionMetapathException.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/regex/RegularExpressionMetapathException.java @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ package gov.nist.secauto.metaschema.core.metapath.function.regex; diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/AbstractStringItem.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/AbstractStringItem.java index 58b25f117..75f2d20a0 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/AbstractStringItem.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/AbstractStringItem.java @@ -8,11 +8,17 @@ import gov.nist.secauto.metaschema.core.metapath.impl.AbstractStringMapKey; import gov.nist.secauto.metaschema.core.metapath.item.function.IMapKey; +import java.util.regex.Pattern; + import edu.umd.cs.findbugs.annotations.NonNull; public abstract class AbstractStringItem extends AbstractAnyAtomicItem implements IStringItem { + private static final String WHITESPACE_SEGMENT = "[ \t\r\n]"; + private static final Pattern TRIM_END = Pattern.compile(WHITESPACE_SEGMENT + "++$"); + private static final Pattern TRIM_START = Pattern.compile("^" + WHITESPACE_SEGMENT + "++"); + private static final Pattern TRIM_MIDDLE = Pattern.compile(WHITESPACE_SEGMENT + "{2,}"); /** * Construct a new string item with the provided {@code value}. @@ -54,4 +60,15 @@ public IStringItem getKey() { return AbstractStringItem.this; } } + + @Override + public IStringItem normalizeSpace() { + String value = asString(); + value = TRIM_START.matcher(value).replaceFirst(""); + value = TRIM_MIDDLE.matcher(value).replaceAll(" "); + value = TRIM_END.matcher(value).replaceFirst(""); + + return IStringItem.valueOf(value); + } + } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/IStringItem.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/IStringItem.java index 5e505fc6f..0089229f0 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/IStringItem.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/IStringItem.java @@ -64,4 +64,7 @@ default int compareTo(@NonNull IStringItem other) { default int compareTo(IAnyAtomicItem other) { return compareTo(other.asStringItem()); } + + @NonNull + IStringItem normalizeSpace(); } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/UuidItemImpl.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/UuidItemImpl.java index d8b16189e..69dbdf8b7 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/UuidItemImpl.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/atomic/UuidItemImpl.java @@ -54,6 +54,12 @@ public boolean equals(Object obj) { || (obj instanceof IStringItem && compareTo((IStringItem) obj) == 0); } + @Override + public IStringItem normalizeSpace() { + // noop + return this; + } + private final class MapKey implements IMapKey { @Override public IUuidItem getKey() { diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatchesTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatchesTest.java index 6e888f971..e196f249d 100644 --- a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatchesTest.java +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnMatchesTest.java @@ -100,7 +100,11 @@ void testInvalidPattern() { ISequence.empty(), List.of(sequence(string("input")), sequence(string("pattern[")))); } catch (MetapathException ex) { - throw ex.getCause(); + Throwable cause = ex.getCause(); + if (cause != null) { + throw cause; + } + throw ex; } }); assertEquals(RegularExpressionMetapathException.INVALID_EXPRESSION, throwable.getCode()); @@ -117,7 +121,11 @@ void testInvalidFlag() { ISequence.empty(), List.of(sequence(string("input")), sequence(string("pattern")), sequence(string("dsm")))); } catch (MetapathException ex) { - throw ex.getCause(); + Throwable cause = ex.getCause(); + if (cause != null) { + throw cause; + } + throw ex; } }); assertEquals(RegularExpressionMetapathException.INVALID_FLAG, throwable.getCode()); diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenizeTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenizeTest.java new file mode 100644 index 000000000..3ccbd27f8 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnTokenizeTest.java @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.string; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.ISequence; +import gov.nist.secauto.metaschema.core.metapath.MetapathException; +import gov.nist.secauto.metaschema.core.metapath.MetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.function.regex.RegularExpressionMetapathException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +class FnTokenizeTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + sequence(string("red"), string("green"), string("blue")), + "tokenize(\" red green blue \")"), + Arguments.of( + sequence(string("The"), string("cat"), string("sat"), string("on"), string("the"), string("mat")), + "tokenize(\"The cat sat on the mat\", \"\\s+\")"), + Arguments.of( + sequence(string(""), string("red"), string("green"), string("blue"), string("")), + "tokenize(\" red green blue \", \"\\s+\")"), + Arguments.of( + sequence(string("1"), string("15"), string("24"), string("50")), + "tokenize(\"1, 15, 24, 50\", \",\\s*\")"), + Arguments.of( + sequence(string("1"), string("15"), string(""), string("24"), string("50"), string("")), + "tokenize(\"1,15,,24,50,\", \",\")"), + Arguments.of( + sequence(string("Some unparsed"), string("HTML"), string("text")), + "tokenize(\"Some unparsed
HTML
text\", \"\\s*
\\s*\", \"i\")")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@NonNull ISequence expected, @NonNull String metapath) { + assertEquals(expected, MetapathExpression.compile(metapath) + .evaluateAs(null, MetapathExpression.ResultType.SEQUENCE, + newDynamicContext())); + } + + @Test + void testMatchZeroLengthString() { + RegularExpressionMetapathException throwable = assertThrows(RegularExpressionMetapathException.class, + () -> { + try { + FunctionTestBase.executeFunction( + FnTokenize.SIGNATURE_TWO_ARG, + newDynamicContext(), + ISequence.empty(), + List.of(sequence(string("abba")), sequence(string(".?")))); + } catch (MetapathException ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + throw cause; + } + throw ex; + } + }); + assertEquals(RegularExpressionMetapathException.MATCHES_ZERO_LENGTH_STRING, throwable.getCode()); + } + + @Test + void testInvalidPattern() { + RegularExpressionMetapathException throwable = assertThrows(RegularExpressionMetapathException.class, + () -> { + try { + FunctionTestBase.executeFunction( + FnTokenize.SIGNATURE_TWO_ARG, + newDynamicContext(), + ISequence.empty(), + List.of(sequence(string("input")), sequence(string("pattern[")))); + } catch (MetapathException ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + throw cause; + } + throw ex; + } + }); + assertEquals(RegularExpressionMetapathException.INVALID_EXPRESSION, throwable.getCode()); + } + + @Test + void testInvalidFlag() { + RegularExpressionMetapathException throwable = assertThrows(RegularExpressionMetapathException.class, + () -> { + try { + FunctionTestBase.executeFunction( + FnTokenize.SIGNATURE_THREE_ARG, + newDynamicContext(), + ISequence.empty(), + List.of(sequence(string("input")), sequence(string("pattern")), sequence(string("dsm")))); + } catch (MetapathException ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + throw cause; + } + throw ex; + } + }); + assertEquals(RegularExpressionMetapathException.INVALID_FLAG, throwable.getCode()); + } +}