From d36a50d82713d557638d26cd4e875ea7806cc53e Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 11 Dec 2024 14:29:19 -0500 Subject: [PATCH] Added support for the fn:has-children Metapath function. --- .../function/impl/AbstractFunction.java | 3 + .../library/DefaultFunctionLibrary.java | 4 +- .../function/library/FnHasChildren.java | 104 +++++++++++++++++ .../function/library/FnLocalName.java | 2 +- .../metapath/function/library/FnName.java | 2 +- .../function/library/FnNamespaceUri.java | 2 +- .../function/library/FnHasChildrenTest.java | 108 ++++++++++++++++++ 7 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildren.java create mode 100644 core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildrenTest.java 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 93a5b19fa..9ab4a0aab 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 @@ -252,6 +252,9 @@ public ISequence execute( result = executeInternal(convertedArguments, dynamicContext, contextItem); if (callingContext != null) { + // FIXME: ensure the result sequence is list backed, otherwise the stream will + // exhaust on subsequent access + // result.getValue(); // add result to cache dynamicContext.cacheResult(callingContext, result); } 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 a6380352f..fbdccf9bd 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 @@ -92,7 +92,9 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // P2: https://www.w3.org/TR/xpath-functions-31/#func-format-number // P2: https://www.w3.org/TR/xpath-functions-31/#func-format-time // P1: https://www.w3.org/TR/xpath-functions-31/#func-generate-id - // P2: https://www.w3.org/TR/xpath-functions-31/#func-has-children + // https://www.w3.org/TR/xpath-functions-31/#func-has-children + registerFunction(FnHasChildren.SIGNATURE_NO_ARG); + registerFunction(FnHasChildren.SIGNATURE_ONE_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-head registerFunction(FnHead.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-hours-from-dateTime diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildren.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildren.java new file mode 100644 index 000000000..15a30af40 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildren.java @@ -0,0 +1,104 @@ +/* + * 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.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.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBooleanItem; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem; +import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * /** Implements + * fn:root + * functions. + */ +public final class FnHasChildren { + @NonNull + private static final String NAME = "has-children"; + @NonNull + static final IFunction SIGNATURE_NO_ARG = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextDependent() + .focusDependent() + .returnType(IStringItem.type()) + .returnOne() + .functionHandler(FnHasChildren::executeNoArg) + .build(); + @NonNull + static final IFunction SIGNATURE_ONE_ARG = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("arg") + .type(INodeItem.type()) + .zeroOrOne() + .build()) + .returnType(IBooleanItem.type()) + .returnOne() + .functionHandler(FnHasChildren::executeOneArg) + .build(); + + @SuppressWarnings("unused") + @NonNull + private static ISequence executeNoArg(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + + INodeItem arg = FunctionUtils.asType( + // test that the focus is an INodeItem + INodeItem.type().test(ObjectUtils.requireNonNull(focus))); + + return ISequence.of(IBooleanItem.valueOf(fnHasChildren(arg))); + } + + @SuppressWarnings("unused") + @NonNull + private static ISequence executeOneArg(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + INodeItem arg = FunctionUtils.asTypeOrNull(ObjectUtils.requireNonNull(arguments.get(0)).getFirstItem(true)); + + return arg == null ? ISequence.empty() : ISequence.of(IBooleanItem.valueOf(fnHasChildren(arg))); + } + + /** + * Determine if the provided node argument has model item children. + *

+ * Based on the XPath 3.1 fn:has-children + * function. + * + * @param arg + * the node item to check for children + * @return {@code true} if the provided node has model item children, or + * {@code false} otherwise + */ + public static boolean fnHasChildren(@NonNull INodeItem arg) { + return arg.modelItems().findFirst().isPresent(); + } + + private FnHasChildren() { + // disable construction + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalName.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalName.java index 0cd2309b4..a03bdad24 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalName.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalName.java @@ -46,7 +46,7 @@ public final class FnLocalName { .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) .deterministic() .contextIndependent() - .focusDependent() + .focusIndependent() .argument(IArgument.builder() .name("arg") .type(INodeItem.type()) diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnName.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnName.java index 39525180b..ba2f83cdf 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnName.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnName.java @@ -47,7 +47,7 @@ public final class FnName { .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) .deterministic() .contextIndependent() - .focusDependent() + .focusIndependent() .argument(IArgument.builder() .name("arg") .type(INodeItem.type()) diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNamespaceUri.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNamespaceUri.java index d662b77de..2910ff1d5 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNamespaceUri.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNamespaceUri.java @@ -46,7 +46,7 @@ public final class FnNamespaceUri { .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) .deterministic() .contextIndependent() - .focusDependent() + .focusIndependent() .argument(IArgument.builder() .name("arg") .type(INodeItem.type()) diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildrenTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildrenTest.java new file mode 100644 index 000000000..ad89574e0 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnHasChildrenTest.java @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import gov.nist.secauto.metaschema.core.metapath.DynamicContext; +import gov.nist.secauto.metaschema.core.metapath.DynamicMetapathException; +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.library.impl.MockedDocumentGenerator; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem; +import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem; +import gov.nist.secauto.metaschema.core.metapath.type.InvalidTypeMetapathException; +import gov.nist.secauto.metaschema.core.metapath.type.TypeMetapathException; + +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.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +class FnHasChildrenTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + true, + "has-children()"), + Arguments.of( + true, + "has-children(.)"), + Arguments.of( + true, + "has-children(/root)"), + Arguments.of( + false, + "has-children(/root/assembly)"), + Arguments.of( + false, + "has-children(/root/assembly/@assembly-flag)"), + Arguments.of( + false, + "has-children(/root/field)"), + Arguments.of( + false, + "has-children(/root/field/@field-flag)")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(boolean expected, @NonNull String metapath) { + DynamicContext dynamicContext = newDynamicContext(); + + INodeItem node = MockedDocumentGenerator.generateDocumentNodeItem(getContext()); + Boolean result = IMetapathExpression.compile(metapath, dynamicContext.getStaticContext()) + .evaluateAs(node, IMetapathExpression.ResultType.BOOLEAN, dynamicContext); + assertEquals(expected, result); + } + + @Test + void testContextAbsent() { + DynamicContext dynamicContext = newDynamicContext(); + + MetapathException ex = assertThrows(MetapathException.class, () -> { + IMetapathExpression.compile("has-children()", dynamicContext.getStaticContext()) + .evaluateAs(null, IMetapathExpression.ResultType.ITEM, dynamicContext); + }); + Throwable cause = ex.getCause() != null ? ex.getCause().getCause() : null; + + assertAll( + () -> assertEquals(DynamicMetapathException.class, cause == null + ? null + : cause.getClass()), + () -> assertEquals(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT, cause instanceof DynamicMetapathException + ? ((DynamicMetapathException) cause).getCode() + : null)); + } + + @Test + void testNotANode() { + DynamicContext dynamicContext = newDynamicContext(); + + MetapathException ex = assertThrows(MetapathException.class, () -> { + IMetapathExpression.compile("has-children()", dynamicContext.getStaticContext()) + .evaluateAs(IStringItem.valueOf("test"), IMetapathExpression.ResultType.ITEM, dynamicContext); + }); + Throwable cause = ex.getCause() != null ? ex.getCause().getCause() : null; + + assertAll( + () -> assertEquals(InvalidTypeMetapathException.class, cause == null + ? null + : cause.getClass()), + () -> assertEquals(TypeMetapathException.INVALID_TYPE_ERROR, cause instanceof TypeMetapathException + ? ((TypeMetapathException) cause).getCode() + : null)); + } +}