From 0bad57d803ec54fe5009d4f36bde34386156c77e Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 11 Dec 2024 15:16:49 -0500 Subject: [PATCH] Added support for the fn:innermost Metapath function. --- .../library/DefaultFunctionLibrary.java | 1 + .../function/library/FnInnermost.java | 91 +++++++++++++++++++ .../function/library/FnInnermostTest.java | 83 +++++++++++++++++ .../core/testing/AbstractModelBuilder.java | 6 ++ 4 files changed, 181 insertions(+) create mode 100644 core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermost.java create mode 100644 core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermostTest.java 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 fbdccf9bd..50440c01f 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 @@ -105,6 +105,7 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-index-of registerFunction(FnIndexOf.SIGNATURE_TWO_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-innermost + registerFunction(FnInnermost.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-insert-before registerFunction(FnInsertBefore.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-iri-to-uri diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermost.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermost.java new file mode 100644 index 000000000..a93e781aa --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermost.java @@ -0,0 +1,91 @@ +/* + * 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.node.INodeItem; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * /** Implements + * fn:root + * functions. + */ +public final class FnInnermost { + @NonNull + private static final String NAME = "innermost"; + @NonNull + static final IFunction SIGNATURE = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusDependent() + .argument(IArgument.builder() + .name("nodes") + .type(INodeItem.type()) + .zeroOrMore() + .build()) + .returnType(INodeItem.type()) + .returnZeroOrMore() + .functionHandler(FnInnermost::execute) + .build(); + + @SuppressWarnings("unused") + @NonNull + private static ISequence execute(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + ISequence nodes = FunctionUtils.asType(ObjectUtils.requireNonNull(arguments.get(0))); + + return ISequence.of(fnInnermost(nodes.getValue())); + } + + /** + * Get every node within the provided list that is not an ancestor of another + * member of the provided list. + *

+ * The nodes are returned in document order with duplicates eliminated. + *

+ * Based on the XPath 3.1 fn:innermost + * function. + * + * @param arg + * the node items check + * @return the nodes that are not an ancestor of another member of the provided + * list + */ + @NonNull + public static Stream fnInnermost(@NonNull List arg) { + Set ancestors = arg.stream() + .distinct() + .flatMap(INodeItem::ancestor) + .collect(Collectors.toSet()); + + return ObjectUtils.notNull(arg.stream() + .distinct() + .filter(node -> !ancestors.contains(node))); + } + + private FnInnermost() { + // disable construction + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermostTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermostTest.java new file mode 100644 index 000000000..50aa04bf8 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnInnermostTest.java @@ -0,0 +1,83 @@ +/* +/* + * 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.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.ISequence; +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 FnInnermostTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + "(/root/assembly/@assembly-flag,/root/field/@field-flag)", + "innermost((.,/root,/root/assembly,/root/assembly/@assembly-flag,/root/field,/root/field/@field-flag))"), + Arguments.of( + "(/root/assembly,/root/field)", + "innermost((.,/root,/root/assembly,/root/field))"), + Arguments.of( + "(/root/assembly/@assembly-flag,/root/field/@field-flag)", + "innermost((.,/root,/root/assembly,/root/assembly/@assembly-flag,/root/field,/root/field/@field-flag," + + ".,/root,/root/assembly,/root/assembly/@assembly-flag,/root/field,/root/field/@field-flag))")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@NonNull String expectedMetapath, @NonNull String metapath) { + DynamicContext dynamicContext = newDynamicContext(); + INodeItem node = MockedDocumentGenerator.generateDocumentNodeItem(getContext()); + + ISequence expected + = IMetapathExpression.compile(expectedMetapath, dynamicContext.getStaticContext()) + .evaluate(node, dynamicContext); + + ISequence actual = IMetapathExpression.compile(metapath, dynamicContext.getStaticContext()) + .evaluate(node, dynamicContext); + assertEquals(expected, actual); + } + + @Test + void testNotANode() { + DynamicContext dynamicContext = newDynamicContext(); + + MetapathException ex = assertThrows(MetapathException.class, () -> { + IMetapathExpression.compile("innermost('test')", 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)); + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/testing/AbstractModelBuilder.java b/core/src/test/java/gov/nist/secauto/metaschema/core/testing/AbstractModelBuilder.java index a7d974be8..67c2a47cf 100644 --- a/core/src/test/java/gov/nist/secauto/metaschema/core/testing/AbstractModelBuilder.java +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/testing/AbstractModelBuilder.java @@ -131,6 +131,12 @@ protected void applyDefinition(@NonNull IDefinition definition) { applyModelElement(definition); applyNamed(definition); applyAttributable(definition); + getContext().checking(new Expectations() { + { + allowing(definition).getDefinitionQName(); + will(returnValue(IEnhancedQName.of(ObjectUtils.notNull(namespace), ObjectUtils.notNull(name)))); + } + }); } /**