Skip to content

Commit

Permalink
Added support for the fn:has-children Metapath function.
Browse files Browse the repository at this point in the history
  • Loading branch information
david-waltermire committed Dec 11, 2024
1 parent 8f88271 commit d36a50d
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* <a href= "https://www.w3.org/TR/xpath-functions-31/#func-root">fn:root</a>
* 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<IBooleanItem> executeNoArg(@NonNull IFunction function,
@NonNull List<ISequence<?>> 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<IBooleanItem> executeOneArg(@NonNull IFunction function,
@NonNull List<ISequence<?>> 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.
* <p>
* Based on the XPath 3.1 <a href=
* "https://www.w3.org/TR/xpath-functions-31/#func-has-children">fn:has-children</a>
* 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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));
}
}

0 comments on commit d36a50d

Please sign in to comment.