diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/InvalidArgumentFunctionException.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/InvalidArgumentFunctionException.java index 0da7f0249..060259b29 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/InvalidArgumentFunctionException.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/InvalidArgumentFunctionException.java @@ -27,11 +27,41 @@ public class InvalidArgumentFunctionException * Raised when either argument to fn:resolve-uri is not a valid URI/IRI. */ public static final int INVALID_ARGUMENT_TO_RESOLVE_URI = 2; + /** + * err:FORG0003: + * Raised by fn:zero-or-one + * if the supplied value contains more than one item. + * + */ + public static final int INVALID_ARGUMENT_ZERO_OR_ONE = 3; + /** + * err:FORG0005: + * Raised by fn:one-or-more + * if the supplied value is an empty sequence. + */ + public static final int INVALID_ARGUMENT_ONE_OR_MORE = 4; + /** + * err:FORG0005: + * Raised by fn:exactly-one + * if the supplied value is not a singleton sequence. + * + */ + public static final int INVALID_ARGUMENT_EXACTLY_ONE = 5; /** * err:FORG0006: - * Raised by functions such as fn:max, fn:min, fn:avg, fn:sum if the supplied - * sequence contains values inappropriate to this function. + * Raised by functions such as + * fn:max, + * fn:min, + * fn:avg, + * fn:sum if + * the supplied sequence contains values inappropriate to this function. */ public static final int INVALID_ARGUMENT_TYPE = 6; diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/impl/AbstractFunction.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/impl/AbstractFunction.java index ba0a1fc70..a141df433 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/impl/AbstractFunction.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/impl/AbstractFunction.java @@ -237,7 +237,12 @@ public ISequence execute( // toSignature(), convertedArguments.toString(), result.asList().toString())); return result; } catch (MetapathException ex) { - throw new MetapathException(String.format("Unable to execute function '%s'", toSignature()), ex); + // FIXME: avoid throwing a new exception for a function-related exception. Fix + // this after refactoring the exception hierarchy. + throw new MetapathException(String.format("Unable to execute function '%s'. %s", + toSignature(), + ex.getLocalizedMessage()), + ex); } } 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 6501c8426..99eaea54e 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 @@ -76,7 +76,8 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-encode-for-uri // https://www.w3.org/TR/xpath-functions-31/#func-ends-with registerFunction(FnEndsWith.SIGNATURE); - // P2: https://www.w3.org/TR/xpath-functions-31/#func-exactly-one + // https://www.w3.org/TR/xpath-functions-31/#func-exactly-one + registerFunction(FnExactlyOne.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-exists registerFunction(FnExists.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-false @@ -127,7 +128,8 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-not registerFunction(FnNot.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-number - // P2: https://www.w3.org/TR/xpath-functions-31/#func-one-or-more + // https://www.w3.org/TR/xpath-functions-31/#func-one-or-more + registerFunction(FnOneOrMore.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-outermost // https://www.w3.org/TR/xpath-functions-31/#func-parse-ietf-date // https://www.w3.org/TR/xpath-functions-31/#func-path @@ -192,7 +194,8 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-year-from-date // https://www.w3.org/TR/xpath-functions-31/#func-year-from-dateTime // https://www.w3.org/TR/xpath-functions-31/#func-years-from-duration - // P2: https://www.w3.org/TR/xpath-functions-31/#func-zero-or-one + // https://www.w3.org/TR/xpath-functions-31/#func-zero-or-one + registerFunction(FnZeroOrOne.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-array-get registerFunction(ArrayGet.SIGNATURE); diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOne.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOne.java new file mode 100644 index 000000000..3cd9e8616 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOne.java @@ -0,0 +1,83 @@ +/* + * 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.MetapathConstants; +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.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Implements the XPath 3.1 fn:exactly-one + * function. + */ +public final class FnExactlyOne { + private static final String NAME = "exactly-one"; + @NonNull + static final IFunction SIGNATURE = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("arg") + .type(IItem.type()) + .zeroOrMore() + .build()) + .returnType(IItem.type()) + .returnOne() + .functionHandler(FnExactlyOne::execute) + .build(); + + private FnExactlyOne() { + // disable construction + } + + @SuppressWarnings("unused") + @NonNull + private static ISequence execute(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + return fnExactlyOne(ObjectUtils.requireNonNull(arguments.get(0))); + } + + /** + * Check that the provided sequence has exactly one item. + *

+ * Based on the XPath 3.1 fn:exactly-one + * function. + * + * @param sequence + * the sequence to evaluate + * @return the sequence if it has zero or one items + * @throws InvalidArgumentFunctionException + * with the code + * {@link InvalidArgumentFunctionException#INVALID_ARGUMENT_EXACTLY_ONE} + * if the sequence contains less or more than one item + */ + @NonNull + public static ISequence fnExactlyOne(@NonNull ISequence sequence) { + if (sequence.size() != 1) { + throw new InvalidArgumentFunctionException( + InvalidArgumentFunctionException.INVALID_ARGUMENT_EXACTLY_ONE, + String.format("fn:exactly-one called with the sequence '%s' containing a number of items other than one.", + sequence.toSignature())); + } + return sequence; + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMore.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMore.java new file mode 100644 index 000000000..0399ffa31 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMore.java @@ -0,0 +1,83 @@ +/* + * 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.MetapathConstants; +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.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Implements the XPath 3.1 fn:one-or-more + * function. + */ +public final class FnOneOrMore { + private static final String NAME = "one-or-more"; + @NonNull + static final IFunction SIGNATURE = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("arg") + .type(IItem.type()) + .zeroOrMore() + .build()) + .returnType(IItem.type()) + .returnOneOrMore() + .functionHandler(FnOneOrMore::execute) + .build(); + + private FnOneOrMore() { + // disable construction + } + + @SuppressWarnings("unused") + @NonNull + private static ISequence execute(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + return fnOneOrMore(ObjectUtils.requireNonNull(arguments.get(0))); + } + + /** + * Check that the provided sequence has one or more items. + *

+ * Based on the XPath 3.1 fn:one-or-more + * function. + * + * @param sequence + * the sequence to evaluate + * @return the sequence if it has zero or one items + * @throws InvalidArgumentFunctionException + * with the code + * {@link InvalidArgumentFunctionException#INVALID_ARGUMENT_ONE_OR_MORE} + * if the sequence contains no items + */ + @NonNull + public static ISequence fnOneOrMore(@NonNull ISequence sequence) { + if (sequence.size() < 1) { + throw new InvalidArgumentFunctionException( + InvalidArgumentFunctionException.INVALID_ARGUMENT_ONE_OR_MORE, + String.format("fn:one-or-more called with the sequence '%s' containing less than one item.", + sequence.toSignature())); + } + return sequence; + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOne.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOne.java new file mode 100644 index 000000000..ed6dc3519 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOne.java @@ -0,0 +1,83 @@ +/* + * 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.MetapathConstants; +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.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Implements the XPath 3.1 fn:zero-or-one + * function. + */ +public final class FnZeroOrOne { + private static final String NAME = "zero-or-one"; + @NonNull + static final IFunction SIGNATURE = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("arg") + .type(IItem.type()) + .zeroOrMore() + .build()) + .returnType(IItem.type()) + .returnZeroOrOne() + .functionHandler(FnZeroOrOne::execute) + .build(); + + private FnZeroOrOne() { + // disable construction + } + + @SuppressWarnings("unused") + @NonNull + private static ISequence execute(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + return fnZeroOrOne(ObjectUtils.requireNonNull(arguments.get(0))); + } + + /** + * Check that the provided sequence has zero or one items. + *

+ * Based on the XPath 3.1 fn:zero-or-one + * function. + * + * @param sequence + * the sequence to evaluate + * @return the sequence if it has zero or one items + * @throws InvalidArgumentFunctionException + * with the code + * {@link InvalidArgumentFunctionException#INVALID_ARGUMENT_ZERO_OR_ONE} + * if the sequence contains more than one item + */ + @NonNull + public static ISequence fnZeroOrOne(@NonNull ISequence sequence) { + if (sequence.size() > 1) { + throw new InvalidArgumentFunctionException( + InvalidArgumentFunctionException.INVALID_ARGUMENT_ZERO_OR_ONE, + String.format("fn:zero-or-one called with the sequence '%s' containing more than one item.", + sequence.toSignature())); + } + return sequence; + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOneTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOneTest.java new file mode 100644 index 000000000..8b85cb303 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnExactlyOneTest.java @@ -0,0 +1,66 @@ +/* + * 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.integer; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.MetapathException; +import gov.nist.secauto.metaschema.core.metapath.function.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +class FnExactlyOneTest + extends ExpressionTestBase { + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + null, + "exactly-one((10, 20, 30))"), + Arguments.of( + null, + "exactly-one((10, 20))"), + Arguments.of( + sequence(integer(10)), + "exactly-one((10))"), + Arguments.of( + null, + "exactly-one(())")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@Nullable ISequence expected, @NonNull String metapath) { + try { + assertEquals(expected, IMetapathExpression.compile(metapath) + .evaluate(null, newDynamicContext())); + } catch (MetapathException ex) { + // FIXME: After refactoring the exception hierarchy, target the actual exception + Throwable cause = ex.getCause() == null ? ex.getCause() : ex.getCause().getCause(); + assertAll( + () -> assertNull(expected), + () -> assertEquals(InvalidArgumentFunctionException.class, cause.getClass()), + () -> assertEquals( + InvalidArgumentFunctionException.INVALID_ARGUMENT_EXACTLY_ONE, + cause instanceof InvalidArgumentFunctionException + ? ((InvalidArgumentFunctionException) cause).getCode() + : null)); + } + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMoreTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMoreTest.java new file mode 100644 index 000000000..016f0cf5a --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOneOrMoreTest.java @@ -0,0 +1,66 @@ +/* + * 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.integer; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.MetapathException; +import gov.nist.secauto.metaschema.core.metapath.function.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +class FnOneOrMoreTest + extends ExpressionTestBase { + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + sequence(integer(10), integer(20), integer(30)), + "one-or-more((10, 20, 30))"), + Arguments.of( + sequence(integer(10), integer(20)), + "one-or-more((10, 20))"), + Arguments.of( + sequence(integer(10)), + "one-or-more((10))"), + Arguments.of( + null, + "one-or-more(())")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@Nullable ISequence expected, @NonNull String metapath) { + try { + assertEquals(expected, IMetapathExpression.compile(metapath) + .evaluate(null, newDynamicContext())); + } catch (MetapathException ex) { + // FIXME: After refactoring the exception hierarchy, target the actual exception + Throwable cause = ex.getCause() == null ? ex.getCause() : ex.getCause().getCause(); + assertAll( + () -> assertNull(expected), + () -> assertEquals(InvalidArgumentFunctionException.class, cause == null ? null : cause.getClass()), + () -> assertEquals( + InvalidArgumentFunctionException.INVALID_ARGUMENT_ONE_OR_MORE, + cause instanceof InvalidArgumentFunctionException + ? ((InvalidArgumentFunctionException) cause).getCode() + : null)); + } + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOneTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOneTest.java new file mode 100644 index 000000000..048b64420 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnZeroOrOneTest.java @@ -0,0 +1,66 @@ +/* + * 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.integer; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.MetapathException; +import gov.nist.secauto.metaschema.core.metapath.function.InvalidArgumentFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +class FnZeroOrOneTest + extends ExpressionTestBase { + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + null, + "zero-or-one((10, 20, 30))"), + Arguments.of( + null, + "zero-or-one((10, 20))"), + Arguments.of( + sequence(integer(10)), + "zero-or-one((10))"), + Arguments.of( + sequence(), + "zero-or-one(())")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@Nullable ISequence expected, @NonNull String metapath) { + try { + assertEquals(expected, IMetapathExpression.compile(metapath) + .evaluate(null, newDynamicContext())); + } catch (MetapathException ex) { + // FIXME: After refactoring the exception hierarchy, target the actual exception + Throwable cause = ex.getCause() == null ? ex.getCause() : ex.getCause().getCause(); + assertAll( + () -> assertNull(expected), + () -> assertEquals(InvalidArgumentFunctionException.class, cause == null ? null : cause.getClass()), + () -> assertEquals( + InvalidArgumentFunctionException.INVALID_ARGUMENT_ZERO_OR_ONE, + cause instanceof InvalidArgumentFunctionException + ? ((InvalidArgumentFunctionException) cause).getCode() + : null)); + } + } +}