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 50440c01f..e18bdecc6 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 @@ -145,6 +145,7 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // 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 + registerFunction(FnOutermost.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-parse-ietf-date // https://www.w3.org/TR/xpath-functions-31/#func-path registerFunction(FnPath.SIGNATURE_NO_ARG); diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermost.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermost.java new file mode 100644 index 000000000..9cbd009c3 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermost.java @@ -0,0 +1,88 @@ +/* + * 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.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * /** Implements + * fn:root + * functions. + */ +public final class FnOutermost { + @NonNull + private static final String NAME = "outermost"; + @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(FnOutermost::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(fnOutermost(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 fnOutermost(@NonNull List arg) { + Set values = new HashSet<>(arg); + + return ObjectUtils.notNull(arg.stream() + .distinct() + .filter(node -> !node.ancestor().anyMatch(values::contains))); + } + + private FnOutermost() { + // disable construction + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/impl/StreamSequence.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/impl/StreamSequence.java index b5654339a..fa223c7c0 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/impl/StreamSequence.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/impl/StreamSequence.java @@ -60,6 +60,9 @@ public List getValue() { instanceLock.lock(); try { if (list == null) { + if (stream == null) { + throw new IllegalStateException("stream is already consumed"); + } list = stream.collect(Collectors.toUnmodifiableList()); stream = null; } 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 index 75a38d957..17746b27d 100644 --- 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 @@ -68,7 +68,7 @@ void test(@NonNull String expectedValueMetapath, @NonNull String actualValuesMet .evaluate(node, dynamicContext); ISequence alternate = IMetapathExpression.compile("$nodes except $nodes/ancestor::node()", dynamicContext.getStaticContext()) - .evaluate(null, dynamicContext.bindVariableValue(IEnhancedQName.of("nodes"), values)); + .evaluate(null, dynamicContext.subContext().bindVariableValue(IEnhancedQName.of("nodes"), values)); assertEquals(expected, actual); assertEquals(expected, alternate); diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermostTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermostTest.java new file mode 100644 index 000000000..e54963f98 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnOutermostTest.java @@ -0,0 +1,106 @@ +/* +/* + * 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 gov.nist.secauto.metaschema.core.qname.IEnhancedQName; + +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 FnOutermostTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + // only document matches + Arguments.of( + "(.)", + "(.,/root,/root/assembly,/root/assembly/@assembly-flag,/root/field,/root/field/@field-flag)"), + // parents not present + Arguments.of( + "(/root/assembly,/root/field)", + "(/root/assembly,/root/field)"), + // parents not present + Arguments.of( + "(/root/assembly,/root/field/@field-flag)", + "(/root/assembly,/root/assembly/@assembly-flag,/root/field/@field-flag)"), + // duplicates + Arguments.of( + "(.)", + "(/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 expectedValueMetapath, @NonNull String actualValuesMetapath) { + DynamicContext dynamicContext = newDynamicContext(); + INodeItem node = MockedDocumentGenerator.generateDocumentNodeItem(getContext()); + + ISequence expected + = IMetapathExpression.compile(expectedValueMetapath, dynamicContext.getStaticContext()) + .evaluate(node, dynamicContext); + + ISequence actual + = IMetapathExpression.compile("outermost(" + actualValuesMetapath + ")", dynamicContext.getStaticContext()) + .evaluate(node, dynamicContext); + + // Test the expected values against the alternate implementation from the spec + ISequence values + = IMetapathExpression.compile(expectedValueMetapath, dynamicContext.getStaticContext()) + .evaluate(node, dynamicContext); + // ensure the values are list backed + values.getValue(); + + ISequence alternate + = IMetapathExpression + .compile("$nodes[not(ancestor::node() intersect $nodes)]/.", dynamicContext.getStaticContext()) + .evaluate(null, dynamicContext.subContext().bindVariableValue(IEnhancedQName.of("nodes"), values)); + + assertEquals(expected, actual); + assertEquals(expected, alternate); + } + + @Test + void testNotANode() { + DynamicContext dynamicContext = newDynamicContext(); + + MetapathException ex = assertThrows(MetapathException.class, () -> { + IMetapathExpression.compile("outermost('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)); + } +}