DefaultFunction.java
- /*
- * Portions of this software was developed by employees of the National Institute
- * of Standards and Technology (NIST), an agency of the Federal Government and is
- * being made available as a public service. Pursuant to title 17 United States
- * Code Section 105, works of NIST employees are not subject to copyright
- * protection in the United States. This software may be subject to foreign
- * copyright. Permission in the United States and in foreign countries, to the
- * extent that NIST may hold copyright, to use, copy, modify, create derivative
- * works, and distribute this software and its documentation without fee is hereby
- * granted on a non-exclusive basis, provided that this notice and disclaimer
- * of warranty appears in all copies.
- *
- * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
- * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
- * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
- * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
- * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT
- * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
- * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
- * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
- * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
- * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
- * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
- */
- package gov.nist.secauto.metaschema.core.metapath.function;
- import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
- import gov.nist.secauto.metaschema.core.metapath.ISequence;
- import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
- import gov.nist.secauto.metaschema.core.metapath.MetapathException;
- import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
- import gov.nist.secauto.metaschema.core.metapath.item.IItem;
- import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
- import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyUriItem;
- import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.EnumSet;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Objects;
- import java.util.Set;
- import java.util.stream.Collectors;
- import edu.umd.cs.findbugs.annotations.NonNull;
- import edu.umd.cs.findbugs.annotations.Nullable;
- /**
- * Provides a concrete implementation of a function call executor.
- */
- public class DefaultFunction
- extends AbstractFunction {
- // private static final Logger logger =
- // LogManager.getLogger(AbstractFunction.class);
- @NonNull
- private final Set<FunctionProperty> properties;
- @NonNull
- private final ISequenceType result;
- @NonNull
- private final IFunctionExecutor handler;
- /**
- * Construct a new function signature.
- *
- * @param name
- * the name of the function
- * @param properties
- * the characteristics of the function
- * @param arguments
- * the argument signatures or an empty list
- * @param result
- * the type of the result
- * @param handler
- * the handler to call to execute the function
- */
- @SuppressWarnings({ "null", "PMD.LooseCoupling" })
- DefaultFunction(
- @NonNull String name,
- @NonNull String namespace,
- @NonNull EnumSet<FunctionProperty> properties,
- @NonNull List<IArgument> arguments,
- @NonNull ISequenceType result,
- @NonNull IFunctionExecutor handler) {
- super(name, namespace, arguments);
- this.properties = Collections.unmodifiableSet(properties);
- this.result = result;
- this.handler = handler;
- }
- @Override
- public Set<FunctionProperty> getProperties() {
- return properties;
- }
- @Override
- public ISequenceType getResult() {
- return result;
- }
- //
- // @Override
- // public boolean isSupported(List<IExpression<?>> expressionArguments) {
- // boolean retval;
- // if (expressionArguments.isEmpty() && getArguments().isEmpty()) {
- // // no arguments
- // retval = true;
- // // } else if (arity() == 1 && expressionArguments.isEmpty()) {
- // // // the context item will be the argument
- // // // TODO: check the context item for type compatibility
- // // retval = true;
- // } else if ((expressionArguments.size() == getArguments().size())
- // || (isArityUnbounded() && expressionArguments.size() >
- // getArguments().size())) {
- // retval = true;
- // // check that argument requirements are satisfied
- // Iterator<IArgument> argumentIterator = getArguments().iterator();
- // Iterator<IExpression<?>> expressionIterator = expressionArguments.iterator();
- //
- // IArgument argument = null;
- // while (argumentIterator.hasNext()) {
- // argument = argumentIterator.next();
- // IExpression<?> expression = expressionIterator.hasNext() ?
- // expressionIterator.next() : null;
- //
- // if (expression != null) {
- // // is the expression supported by the argument?
- // retval = argument.isSupported(expression);
- // if (!retval) {
- // break;
- // }
- // } else {
- // // there are no more expression arguments. Make sure that the remaining
- // arguments are optional
- // if (!argument.getSequenceType().getOccurrence().isOptional()) {
- // retval = false;
- // break;
- // }
- // }
- // }
- //
- // if (retval && expressionIterator.hasNext()) {
- // if (isArityUnbounded()) {
- // // check remaining expressions against the last argument
- // while (expressionIterator.hasNext()) {
- // IExpression<?> expression = expressionIterator.next();
- // @SuppressWarnings("null")
- // boolean result = argument.isSupported(expression);
- // if (!result) {
- // retval = result;
- // break;
- // }
- // }
- // } else {
- // // there are extra expressions, which do not match the arguments
- // retval = false;
- // }
- // }
- // } else {
- // retval = false;
- // }
- // return retval;
- // }
- /**
- * Converts arguments in an attempt to align with the function's signature.
- *
- * @param function
- * the function
- * @param parameters
- * the argument parameters
- * @return the converted argument list
- */
- @NonNull
- public static List<ISequence<?>> convertArguments(
- @NonNull IFunction function,
- @NonNull List<ISequence<?>> parameters) {
- @NonNull List<ISequence<?>> retval = new ArrayList<>(parameters.size());
- Iterator<IArgument> argumentIterator = function.getArguments().iterator();
- Iterator<ISequence<?>> parametersIterator = parameters.iterator();
- IArgument argument = null;
- while (parametersIterator.hasNext()) {
- if (argumentIterator.hasNext()) {
- argument = argumentIterator.next();
- } else if (!function.isArityUnbounded()) {
- throw new InvalidTypeMetapathException(
- null,
- String.format("argument signature doesn't match '%s'", function.toSignature()));
- }
- assert argument != null;
- ISequence<?> parameter = parametersIterator.next();
- int size = parameter.size();
- Occurrence occurrence = argument.getSequenceType().getOccurrence();
- switch (occurrence) {
- case ONE: {
- if (size != 1) {
- throw new InvalidTypeMetapathException(
- null,
- String.format("a sequence of one expected, but found '%d'", size));
- }
- IItem item = FunctionUtils.getFirstItem(parameter, true);
- parameter = item == null ? ISequence.empty() : ISequence.of(item);
- break;
- }
- case ZERO_OR_ONE: {
- if (size > 1) {
- throw new InvalidTypeMetapathException(
- null,
- String.format("a sequence of zero or one expected, but found '%d'", size));
- }
- IItem item = FunctionUtils.getFirstItem(parameter, false);
- parameter = item == null ? ISequence.empty() : ISequence.of(item);
- break;
- }
- case ONE_OR_MORE:
- if (size < 1) {
- throw new InvalidTypeMetapathException(
- null,
- String.format("a sequence of zero or more expected, but found '%d'", size));
- }
- break;
- case ZERO:
- if (size != 0) {
- throw new InvalidTypeMetapathException(
- null,
- String.format("an empty sequence expected, but found '%d'", size));
- }
- break;
- case ZERO_OR_MORE:
- default:
- // do nothing
- }
- Class<? extends IItem> argumentClass = argument.getSequenceType().getType();
- // apply function conversion and type promotion to the parameter
- parameter = convertSequence(argument, parameter);
- // check resulting values
- for (IItem item : parameter.asList()) {
- Class<? extends IItem> itemClass = item.getClass();
- if (!argumentClass.isAssignableFrom(itemClass)) {
- throw new InvalidTypeMetapathException(
- item,
- String.format("The type '%s' is not a subtype of '%s'", itemClass.getName(), argumentClass.getName()));
- }
- }
- retval.add(parameter);
- }
- return retval;
- }
- /**
- * Based on XPath 3.1
- * <a href="https://www.w3.org/TR/xpath-31/#dt-function-conversion">function
- * conversion</a> rules.
- *
- * @param argument
- * the function argument signature details
- * @param sequence
- * the sequence to convert
- * @return the converted sequence
- */
- @NonNull
- protected static ISequence<?> convertSequence(@NonNull IArgument argument, @NonNull ISequence<?> sequence) {
- @NonNull ISequence<?> retval;
- if (sequence.isEmpty()) {
- retval = ISequence.empty();
- } else {
- ISequenceType requiredSequenceType = argument.getSequenceType();
- Class<? extends IItem> requiredSequenceTypeClass = requiredSequenceType.getType();
- List<IItem> result = new ArrayList<>(sequence.size());
- boolean atomize = IAnyAtomicItem.class.isAssignableFrom(requiredSequenceTypeClass);
- for (IItem item : sequence.asList()) {
- assert item != null;
- if (atomize) {
- item = FnData.fnDataItem(item); // NOPMD - intentional
- // if (IUntypedAtomicItem.class.isInstance(item)) { // NOPMD
- // // TODO: apply cast to atomic type
- // }
- // promote URIs to strings if a string is required
- if (IStringItem.class.equals(requiredSequenceTypeClass) && IAnyUriItem.class.isInstance(item)) {
- item = IStringItem.cast((IAnyUriItem) item); // NOPMD - intentional
- }
- }
- // item = requiredSequenceType.
- if (!requiredSequenceTypeClass.isInstance(item)) {
- throw new InvalidTypeMetapathException(
- item,
- String.format("The type '%s' is not a subtype of '%s'", item.getClass().getName(),
- requiredSequenceTypeClass.getName()));
- }
- result.add(item);
- }
- retval = ISequence.of(result);
- }
- return retval;
- }
- @Override
- public ISequence<?> execute(
- @NonNull List<ISequence<?>> arguments,
- @NonNull DynamicContext dynamicContext,
- @NonNull ISequence<?> focus) {
- try {
- List<ISequence<?>> convertedArguments = convertArguments(this, arguments);
- IItem contextItem = isFocusDepenent() ? FunctionUtils.requireFirstItem(focus, true) : null;
- CallingContext callingContext = null;
- ISequence<?> result = null;
- if (isDeterministic()) {
- // check cache
- callingContext = new CallingContext(arguments, contextItem);
- // attempt to get the result from the cache
- result = dynamicContext.getCachedResult(callingContext);
- }
- if (result == null) {
- // logger.info(String.format("Executing function '%s' with arguments '%s'.",
- // toSignature(),
- // convertedArguments.toString()));
- // INodeItem actualFocus = focus == null ? null : focus.getNodeItem();
- // if (isFocusDepenent() && actualFocus == null) {
- // throw new
- // DynamicMetapathException(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT,
- // "Null
- // focus");
- // }
- // result = handler.execute(this, convertedArguments, dynamicContext,
- // actualFocus);
- result = handler.execute(this, convertedArguments, dynamicContext, contextItem);
- if (callingContext != null) {
- // add result to cache
- dynamicContext.cacheResult(callingContext, result);
- }
- }
- // logger.info(String.format("Executed function '%s' with arguments '%s'
- // producing result '%s'",
- // toSignature(), convertedArguments.toString(), result.asList().toString()));
- return result;
- } catch (MetapathException ex) {
- throw new MetapathException(String.format("Unable to execute function '%s'", toSignature()), ex);
- }
- }
- @Override
- public int hashCode() {
- return Objects.hash(getName(), getNamespace(), getArguments(), handler, properties, result);
- }
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true; // NOPMD - readability
- }
- if (obj == null) {
- return false; // NOPMD - readability
- }
- if (getClass() != obj.getClass()) {
- return false; // NOPMD - readability
- }
- DefaultFunction other = (DefaultFunction) obj;
- return Objects.equals(getName(), other.getName())
- && Objects.equals(getNamespace(), other.getNamespace())
- && Objects.equals(getArguments(), other.getArguments())
- && Objects.equals(handler, other.handler)
- && Objects.equals(properties, other.properties)
- && Objects.equals(result, other.result);
- }
- @Override
- public String toString() {
- return toSignature();
- }
- @Override
- public String toSignature() {
- StringBuilder builder = new StringBuilder()
- .append("Q{")
- .append(getNamespace())
- .append('}')
- .append(getName()) // name
- .append('('); // arguments
- List<IArgument> arguments = getArguments();
- if (arguments.isEmpty()) {
- builder.append("()");
- } else {
- builder.append(arguments.stream().map(argument -> argument.toSignature()).collect(Collectors.joining(",")));
- if (isArityUnbounded()) {
- builder.append(", ...");
- }
- }
- builder.append(") as ")
- .append(getResult().toSignature());// return type
- return builder.toString();
- }
- public final class CallingContext {
- @Nullable
- private final IItem contextItem;
- @NonNull
- private final List<ISequence<?>> arguments;
- /**
- * Set up the execution context for this function.
- *
- * @param arguments
- * the function arguments
- * @param contextItem
- * the current node context
- */
- private CallingContext(@NonNull List<ISequence<?>> arguments, @Nullable IItem contextItem) {
- this.contextItem = contextItem;
- this.arguments = arguments;
- }
- /**
- * Get the function instance associated with the calling context.
- *
- * @return the function instance
- */
- @NonNull
- public DefaultFunction getFunction() {
- return DefaultFunction.this;
- }
- /**
- * Get the node item focus associated with the calling context.
- *
- * @return the function instance
- */
- @Nullable
- public IItem getContextItem() {
- return contextItem;
- }
- /**
- * Get the arguments associated with the calling context.
- *
- * @return the arguments
- */
- @NonNull
- public List<ISequence<?>> getArguments() {
- return arguments;
- }
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + getFunction().hashCode();
- result = prime * result + Objects.hash(contextItem, arguments);
- return result;
- }
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true; // NOPMD - readability
- }
- if (obj == null) {
- return false; // NOPMD - readability
- }
- if (getClass() != obj.getClass()) {
- return false; // NOPMD - readability
- }
- CallingContext other = (CallingContext) obj;
- if (!getFunction().equals(other.getFunction())) {
- return false; // NOPMD - readability
- }
- return Objects.equals(arguments, other.arguments) && Objects.equals(contextItem, other.contextItem);
- }
- }
- }