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 92f9ff3a1..a29183259 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 @@ -107,6 +107,9 @@ public DefaultFunctionLibrary() { // NOPMD - intentional registerFunction(FnInsertBefore.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-iri-to-uri // P1: https://www.w3.org/TR/xpath-functions-31/#func-last + // https://www.w3.org/TR/xpath-functions-31/#func-local-name + registerFunction(FnLocalName.SIGNATURE_NO_ARG); + registerFunction(FnLocalName.SIGNATURE_ONE_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-lower-case registerFunction(FnLowerCase.SIGNATURE_ONE_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-matches 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 new file mode 100644 index 000000000..4fd0a05db --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalName.java @@ -0,0 +1,111 @@ +/* + * 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.StaticContext; +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.IStringItem; +import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem; +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:name + * functions. + */ +public final class FnLocalName { + @NonNull + private static final String NAME = "local-name"; + @NonNull + static final IFunction SIGNATURE_NO_ARG = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextDependent() + .focusDependent() + .returnType(IStringItem.type()) + .returnOne() + .functionHandler(FnLocalName::executeNoArg) + .build(); + @NonNull + static final IFunction SIGNATURE_ONE_ARG = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextIndependent() + .focusDependent() + .argument(IArgument.builder() + .name("arg") + .type(INodeItem.type()) + .zeroOrOne() + .build()) + .returnType(IStringItem.type()) + .returnOne() + .functionHandler(FnLocalName::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( + IStringItem.valueOf(fnLocalName(arg, dynamicContext.getStaticContext()))); + } + + @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 ISequence.of( + IStringItem.valueOf(arg == null ? "" : fnLocalName(arg, dynamicContext.getStaticContext()))); + } + + /** + * Get the name of the provided node item. + *

+ * Based on the XPath 3.1 fn:local-name + * function. + * + * @param arg + * the node item to get the name for + * @param staticContext + * the static context used to resolve the namespace prefix + * @return the name of the node if it has one, or an empty string otherwise + */ + @NonNull + public static String fnLocalName(@NonNull INodeItem arg, @NonNull StaticContext staticContext) { + return arg instanceof IDefinitionNodeItem + ? ((IDefinitionNodeItem) arg).getQName().getLocalName() + : ""; + } + + private FnLocalName() { + // disable construction + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalNameTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalNameTest.java new file mode 100644 index 000000000..6a0647477 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnLocalNameTest.java @@ -0,0 +1,117 @@ +/* + * 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.assertNotNull; +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.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; +import edu.umd.cs.findbugs.annotations.Nullable; + +class FnLocalNameTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + null, + "local-name()"), + Arguments.of( + null, + "local-name(.)"), + Arguments.of( + MockedDocumentGenerator.ROOT_QNAME, + "local-name(/root)"), + Arguments.of( + MockedDocumentGenerator.ASSEMBLY_QNAME, + "local-name(/root/assembly)"), + Arguments.of( + MockedDocumentGenerator.ASSEMBLY_FLAG_QNAME, + "local-name(/root/assembly/@assembly-flag)"), + Arguments.of( + MockedDocumentGenerator.FIELD_QNAME, + "local-name(/root/field)"), + Arguments.of( + MockedDocumentGenerator.FIELD_FLAG_QNAME, + "local-name(/root/field/@field-flag)")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@Nullable IEnhancedQName expected, @NonNull String metapath) { + DynamicContext dynamicContext = newDynamicContext(); + + IStringItem result = IMetapathExpression.compile(metapath, dynamicContext.getStaticContext()) + .evaluateAs( + MockedDocumentGenerator.generateDocumentNodeItem(getContext()), + IMetapathExpression.ResultType.ITEM, + dynamicContext); + assertNotNull(result); + assertEquals( + expected == null + ? "" + : expected.getLocalName(), + result.asString()); + } + + @Test + void testContextAbsent() { + DynamicContext dynamicContext = newDynamicContext(); + + MetapathException ex = assertThrows(MetapathException.class, () -> { + IMetapathExpression.compile("local-name()", 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("local-name()", 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/metapath/function/library/FnNameTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNameTest.java index f9ccd1aa1..5b9a376d9 100644 --- a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNameTest.java +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnNameTest.java @@ -10,33 +10,22 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import gov.nist.secauto.metaschema.core.mdm.IDMAssemblyNodeItem; -import gov.nist.secauto.metaschema.core.mdm.IDMDocumentNodeItem; -import gov.nist.secauto.metaschema.core.mdm.IDMFieldNodeItem; -import gov.nist.secauto.metaschema.core.mdm.IDMRootAssemblyNodeItem; 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.IDocumentNodeItem; import gov.nist.secauto.metaschema.core.metapath.type.InvalidTypeMetapathException; import gov.nist.secauto.metaschema.core.metapath.type.TypeMetapathException; -import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition; -import gov.nist.secauto.metaschema.core.model.IAssemblyInstance; -import gov.nist.secauto.metaschema.core.model.IFieldInstance; -import gov.nist.secauto.metaschema.core.model.IFlagInstance; -import gov.nist.secauto.metaschema.core.model.IResourceLocation; import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; -import gov.nist.secauto.metaschema.core.testing.MockedModelTestSupport; 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.net.URI; import java.util.stream.Stream; import edu.umd.cs.findbugs.annotations.NonNull; @@ -44,41 +33,6 @@ class FnNameTest extends ExpressionTestBase { - @NonNull - private static final IEnhancedQName ROOT_QNAME = IEnhancedQName.of(NS, "root"); - @NonNull - private static final IEnhancedQName ASSEMBLY_QNAME = IEnhancedQName.of(NS, "assembly"); - @NonNull - private static final IEnhancedQName FIELD_QNAME = IEnhancedQName.of(NS, "field"); - @NonNull - private static final IEnhancedQName ASSEMBLY_FLAG_QNAME = IEnhancedQName.of("assembly-flag"); - @NonNull - private static final IEnhancedQName FIELD_FLAG_QNAME = IEnhancedQName.of("field-flag"); - - private IDocumentNodeItem newDocumentNodeItem() { - MockedModelTestSupport mocking = new MockedModelTestSupport(getContext()); - IResourceLocation resourceLocation = mocking.mock(IResourceLocation.class); - - IAssemblyDefinition rootDefinition = mocking.assembly().qname(ROOT_QNAME).rootQName(ROOT_QNAME).toDefinition(); - IAssemblyInstance assemblyInstance = mocking.assembly().qname(ASSEMBLY_QNAME).toInstance(rootDefinition); - IFlagInstance assemblyFlag = mocking.flag().qname(ASSEMBLY_FLAG_QNAME).toInstance(assemblyInstance.getDefinition()); - IFieldInstance fieldInstance = mocking.field().qname(FIELD_QNAME).toInstance(rootDefinition); - IFlagInstance fieldFlag = mocking.flag().qname(FIELD_FLAG_QNAME).toInstance(fieldInstance.getDefinition()); - - IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance( - URI.create("https://example.com/resource"), - resourceLocation, - rootDefinition, - resourceLocation); - IDMRootAssemblyNodeItem root = document.getRootAssemblyNodeItem(); - IDMAssemblyNodeItem assembly = root.newAssembly(assemblyInstance, resourceLocation); - assembly.newFlag(assemblyFlag, resourceLocation, IStringItem.valueOf("assembly-flag")); - IDMFieldNodeItem field = root.newField(fieldInstance, resourceLocation, IStringItem.valueOf("field")); - field.newFlag(fieldFlag, resourceLocation, IStringItem.valueOf("field-flag")); - - return document; - } - private static Stream provideValues() { // NOPMD - false positive return Stream.of( Arguments.of( @@ -88,19 +42,19 @@ private static Stream provideValues() { // NOPMD - false positive null, "name(.)"), Arguments.of( - ROOT_QNAME, + MockedDocumentGenerator.ROOT_QNAME, "name(/root)"), Arguments.of( - ASSEMBLY_QNAME, + MockedDocumentGenerator.ASSEMBLY_QNAME, "name(/root/assembly)"), Arguments.of( - ASSEMBLY_FLAG_QNAME, + MockedDocumentGenerator.ASSEMBLY_FLAG_QNAME, "name(/root/assembly/@assembly-flag)"), Arguments.of( - FIELD_QNAME, + MockedDocumentGenerator.FIELD_QNAME, "name(/root/field)"), Arguments.of( - FIELD_FLAG_QNAME, + MockedDocumentGenerator.FIELD_FLAG_QNAME, "name(/root/field/@field-flag)")); } @@ -110,7 +64,10 @@ void test(@Nullable IEnhancedQName expected, @NonNull String metapath) { DynamicContext dynamicContext = newDynamicContext(); IStringItem result = IMetapathExpression.compile(metapath, dynamicContext.getStaticContext()) - .evaluateAs(newDocumentNodeItem(), IMetapathExpression.ResultType.ITEM, dynamicContext); + .evaluateAs( + MockedDocumentGenerator.generateDocumentNodeItem(getContext()), + IMetapathExpression.ResultType.ITEM, + dynamicContext); assertNotNull(result); assertEquals( expected == null diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/impl/MockedDocumentGenerator.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/impl/MockedDocumentGenerator.java new file mode 100644 index 000000000..065a22e1a --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/impl/MockedDocumentGenerator.java @@ -0,0 +1,65 @@ + +package gov.nist.secauto.metaschema.core.metapath.function.library.impl; + +import gov.nist.secauto.metaschema.core.mdm.IDMAssemblyNodeItem; +import gov.nist.secauto.metaschema.core.mdm.IDMDocumentNodeItem; +import gov.nist.secauto.metaschema.core.mdm.IDMFieldNodeItem; +import gov.nist.secauto.metaschema.core.mdm.IDMRootAssemblyNodeItem; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem; +import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition; +import gov.nist.secauto.metaschema.core.model.IAssemblyInstance; +import gov.nist.secauto.metaschema.core.model.IFieldInstance; +import gov.nist.secauto.metaschema.core.model.IFlagInstance; +import gov.nist.secauto.metaschema.core.model.IResourceLocation; +import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; +import gov.nist.secauto.metaschema.core.testing.MockedModelTestSupport; + +import org.jmock.Mockery; + +import java.net.URI; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public class MockedDocumentGenerator { + @NonNull + public static final String NS = "http://example.com/ns"; + @NonNull + public static final IEnhancedQName ROOT_QNAME = IEnhancedQName.of(NS, "root"); + @NonNull + public static final IEnhancedQName ASSEMBLY_QNAME = IEnhancedQName.of(NS, "assembly"); + @NonNull + public static final IEnhancedQName FIELD_QNAME = IEnhancedQName.of(NS, "field"); + @NonNull + public static final IEnhancedQName ASSEMBLY_FLAG_QNAME = IEnhancedQName.of("assembly-flag"); + @NonNull + public static final IEnhancedQName FIELD_FLAG_QNAME = IEnhancedQName.of("field-flag"); + + public static IDMDocumentNodeItem generateDocumentNodeItem(@NonNull Mockery context) { + MockedModelTestSupport mocking = new MockedModelTestSupport(context); + IResourceLocation resourceLocation = mocking.mock(IResourceLocation.class); + + IAssemblyDefinition rootDefinition = mocking.assembly().qname(ROOT_QNAME).rootQName(ROOT_QNAME).toDefinition(); + IAssemblyInstance assemblyInstance = mocking.assembly().qname(ASSEMBLY_QNAME).toInstance(rootDefinition); + IFlagInstance assemblyFlag = mocking.flag().qname(ASSEMBLY_FLAG_QNAME).toInstance(assemblyInstance.getDefinition()); + IFieldInstance fieldInstance = mocking.field().qname(FIELD_QNAME).toInstance(rootDefinition); + IFlagInstance fieldFlag = mocking.flag().qname(FIELD_FLAG_QNAME).toInstance(fieldInstance.getDefinition()); + + IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance( + URI.create("https://example.com/resource"), + resourceLocation, + rootDefinition, + resourceLocation); + IDMRootAssemblyNodeItem root = document.getRootAssemblyNodeItem(); + IDMAssemblyNodeItem assembly = root.newAssembly(assemblyInstance, resourceLocation); + assembly.newFlag(assemblyFlag, resourceLocation, IStringItem.valueOf("assembly-flag")); + IDMFieldNodeItem field = root.newField(fieldInstance, resourceLocation, IStringItem.valueOf("field")); + field.newFlag(fieldFlag, resourceLocation, IStringItem.valueOf("field-flag")); + + return document; + } + + private MockedDocumentGenerator() { + // disable construction + } + +} diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/io/MetaschemaModuleMetaschemaTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/io/MetaschemaModuleMetaschemaTest.java index 490b11e26..3711c1be8 100644 --- a/databind/src/test/java/gov/nist/secauto/metaschema/databind/io/MetaschemaModuleMetaschemaTest.java +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/io/MetaschemaModuleMetaschemaTest.java @@ -98,7 +98,7 @@ void testOscalBindingModuleLoader() throws MetaschemaException, IOException { IBindingModuleLoader loader = newBindingContext().newModuleLoader(); loader.allowEntityResolution(); IBindingMetaschemaModule module = loader.load(ObjectUtils.notNull(URI.create( - "https://raw.githubusercontent.com/usnistgov/OSCAL/refs/tags/v1.1.2/src/metaschema/oscal_complete_metaschema.xml"))); + "https://raw.githubusercontent.com/usnistgov/OSCAL/refs/tags/v1.1.3/src/metaschema/oscal_complete_metaschema.xml"))); assertNotNull(module); } @@ -108,7 +108,7 @@ void testOscalXmlModuleLoader() throws MetaschemaException, IOException { // loader.allowEntityResolution(); IXmlMetaschemaModule module = loader.load(ObjectUtils.notNull(URI.create( - "https://raw.githubusercontent.com/usnistgov/OSCAL/refs/tags/v1.1.2/src/metaschema/oscal_complete_metaschema.xml"))); + "https://raw.githubusercontent.com/usnistgov/OSCAL/refs/tags/v1.1.3/src/metaschema/oscal_complete_metaschema.xml"))); assertNotNull(module); } }