001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.metapath.function;
028
029import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
030import gov.nist.secauto.metaschema.core.metapath.ISequence;
031import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
032import gov.nist.secauto.metaschema.core.metapath.MetapathException;
033import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
034import gov.nist.secauto.metaschema.core.metapath.item.IItem;
035import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
036import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyUriItem;
037import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
038
039import java.util.ArrayList;
040import java.util.Collections;
041import java.util.EnumSet;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Objects;
045import java.util.Set;
046import java.util.stream.Collectors;
047
048import edu.umd.cs.findbugs.annotations.NonNull;
049import edu.umd.cs.findbugs.annotations.Nullable;
050
051/**
052 * Provides a concrete implementation of a function call executor.
053 */
054public class DefaultFunction
055    extends AbstractFunction {
056  // private static final Logger logger =
057  // LogManager.getLogger(AbstractFunction.class);
058
059  @NonNull
060  private final Set<FunctionProperty> properties;
061  @NonNull
062  private final ISequenceType result;
063  @NonNull
064  private final IFunctionExecutor handler;
065
066  /**
067   * Construct a new function signature.
068   *
069   * @param name
070   *          the name of the function
071   * @param properties
072   *          the characteristics of the function
073   * @param arguments
074   *          the argument signatures or an empty list
075   * @param result
076   *          the type of the result
077   * @param handler
078   *          the handler to call to execute the function
079   */
080  @SuppressWarnings({ "null", "PMD.LooseCoupling" })
081  DefaultFunction(
082      @NonNull String name,
083      @NonNull String namespace,
084      @NonNull EnumSet<FunctionProperty> properties,
085      @NonNull List<IArgument> arguments,
086      @NonNull ISequenceType result,
087      @NonNull IFunctionExecutor handler) {
088    super(name, namespace, arguments);
089    this.properties = Collections.unmodifiableSet(properties);
090    this.result = result;
091    this.handler = handler;
092  }
093
094  @Override
095  public Set<FunctionProperty> getProperties() {
096    return properties;
097  }
098
099  @Override
100  public ISequenceType getResult() {
101    return result;
102  }
103  //
104  // @Override
105  // public boolean isSupported(List<IExpression<?>> expressionArguments) {
106  // boolean retval;
107  // if (expressionArguments.isEmpty() && getArguments().isEmpty()) {
108  // // no arguments
109  // retval = true;
110  // // } else if (arity() == 1 && expressionArguments.isEmpty()) {
111  // // // the context item will be the argument
112  // // // TODO: check the context item for type compatibility
113  // // retval = true;
114  // } else if ((expressionArguments.size() == getArguments().size())
115  // || (isArityUnbounded() && expressionArguments.size() >
116  // getArguments().size())) {
117  // retval = true;
118  // // check that argument requirements are satisfied
119  // Iterator<IArgument> argumentIterator = getArguments().iterator();
120  // Iterator<IExpression<?>> expressionIterator = expressionArguments.iterator();
121  //
122  // IArgument argument = null;
123  // while (argumentIterator.hasNext()) {
124  // argument = argumentIterator.next();
125  // IExpression<?> expression = expressionIterator.hasNext() ?
126  // expressionIterator.next() : null;
127  //
128  // if (expression != null) {
129  // // is the expression supported by the argument?
130  // retval = argument.isSupported(expression);
131  // if (!retval) {
132  // break;
133  // }
134  // } else {
135  // // there are no more expression arguments. Make sure that the remaining
136  // arguments are optional
137  // if (!argument.getSequenceType().getOccurrence().isOptional()) {
138  // retval = false;
139  // break;
140  // }
141  // }
142  // }
143  //
144  // if (retval && expressionIterator.hasNext()) {
145  // if (isArityUnbounded()) {
146  // // check remaining expressions against the last argument
147  // while (expressionIterator.hasNext()) {
148  // IExpression<?> expression = expressionIterator.next();
149  // @SuppressWarnings("null")
150  // boolean result = argument.isSupported(expression);
151  // if (!result) {
152  // retval = result;
153  // break;
154  // }
155  // }
156  // } else {
157  // // there are extra expressions, which do not match the arguments
158  // retval = false;
159  // }
160  // }
161  // } else {
162  // retval = false;
163  // }
164  // return retval;
165  // }
166
167  /**
168   * Converts arguments in an attempt to align with the function's signature.
169   *
170   * @param function
171   *          the function
172   * @param parameters
173   *          the argument parameters
174   * @return the converted argument list
175   */
176  @NonNull
177  public static List<ISequence<?>> convertArguments(
178      @NonNull IFunction function,
179      @NonNull List<ISequence<?>> parameters) {
180    @NonNull List<ISequence<?>> retval = new ArrayList<>(parameters.size());
181
182    Iterator<IArgument> argumentIterator = function.getArguments().iterator();
183    Iterator<ISequence<?>> parametersIterator = parameters.iterator();
184
185    IArgument argument = null;
186    while (parametersIterator.hasNext()) {
187      if (argumentIterator.hasNext()) {
188        argument = argumentIterator.next();
189      } else if (!function.isArityUnbounded()) {
190        throw new InvalidTypeMetapathException(
191            null,
192            String.format("argument signature doesn't match '%s'", function.toSignature()));
193      }
194
195      assert argument != null;
196
197      ISequence<?> parameter = parametersIterator.next();
198
199      int size = parameter.size();
200      Occurrence occurrence = argument.getSequenceType().getOccurrence();
201      switch (occurrence) {
202      case ONE: {
203        if (size != 1) {
204          throw new InvalidTypeMetapathException(
205              null,
206              String.format("a sequence of one expected, but found '%d'", size));
207        }
208
209        IItem item = FunctionUtils.getFirstItem(parameter, true);
210        parameter = item == null ? ISequence.empty() : ISequence.of(item);
211        break;
212      }
213      case ZERO_OR_ONE: {
214        if (size > 1) {
215          throw new InvalidTypeMetapathException(
216              null,
217              String.format("a sequence of zero or one expected, but found '%d'", size));
218        }
219
220        IItem item = FunctionUtils.getFirstItem(parameter, false);
221        parameter = item == null ? ISequence.empty() : ISequence.of(item);
222        break;
223      }
224      case ONE_OR_MORE:
225        if (size < 1) {
226          throw new InvalidTypeMetapathException(
227              null,
228              String.format("a sequence of zero or more expected, but found '%d'", size));
229        }
230        break;
231      case ZERO:
232        if (size != 0) {
233          throw new InvalidTypeMetapathException(
234              null,
235              String.format("an empty sequence expected, but found '%d'", size));
236        }
237        break;
238      case ZERO_OR_MORE:
239      default:
240        // do nothing
241      }
242
243      Class<? extends IItem> argumentClass = argument.getSequenceType().getType();
244
245      // apply function conversion and type promotion to the parameter
246      parameter = convertSequence(argument, parameter);
247
248      // check resulting values
249      for (IItem item : parameter.asList()) {
250        Class<? extends IItem> itemClass = item.getClass();
251        if (!argumentClass.isAssignableFrom(itemClass)) {
252          throw new InvalidTypeMetapathException(
253              item,
254              String.format("The type '%s' is not a subtype of '%s'", itemClass.getName(), argumentClass.getName()));
255        }
256      }
257
258      retval.add(parameter);
259    }
260    return retval;
261  }
262
263  /**
264   * Based on XPath 3.1
265   * <a href="https://www.w3.org/TR/xpath-31/#dt-function-conversion">function
266   * conversion</a> rules.
267   *
268   * @param argument
269   *          the function argument signature details
270   * @param sequence
271   *          the sequence to convert
272   * @return the converted sequence
273   */
274  @NonNull
275  protected static ISequence<?> convertSequence(@NonNull IArgument argument, @NonNull ISequence<?> sequence) {
276    @NonNull ISequence<?> retval;
277    if (sequence.isEmpty()) {
278      retval = ISequence.empty();
279    } else {
280      ISequenceType requiredSequenceType = argument.getSequenceType();
281      Class<? extends IItem> requiredSequenceTypeClass = requiredSequenceType.getType();
282
283      List<IItem> result = new ArrayList<>(sequence.size());
284
285      boolean atomize = IAnyAtomicItem.class.isAssignableFrom(requiredSequenceTypeClass);
286
287      for (IItem item : sequence.asList()) {
288        assert item != null;
289        if (atomize) {
290          item = FnData.fnDataItem(item); // NOPMD - intentional
291
292          // if (IUntypedAtomicItem.class.isInstance(item)) { // NOPMD
293          // // TODO: apply cast to atomic type
294          // }
295
296          // promote URIs to strings if a string is required
297          if (IStringItem.class.equals(requiredSequenceTypeClass) && IAnyUriItem.class.isInstance(item)) {
298            item = IStringItem.cast((IAnyUriItem) item); // NOPMD - intentional
299          }
300        }
301
302        // item = requiredSequenceType.
303        if (!requiredSequenceTypeClass.isInstance(item)) {
304          throw new InvalidTypeMetapathException(
305              item,
306              String.format("The type '%s' is not a subtype of '%s'", item.getClass().getName(),
307                  requiredSequenceTypeClass.getName()));
308        }
309        result.add(item);
310      }
311      retval = ISequence.of(result);
312    }
313    return retval;
314  }
315
316  @Override
317  public ISequence<?> execute(
318      @NonNull List<ISequence<?>> arguments,
319      @NonNull DynamicContext dynamicContext,
320      @NonNull ISequence<?> focus) {
321    try {
322      List<ISequence<?>> convertedArguments = convertArguments(this, arguments);
323
324      IItem contextItem = isFocusDepenent() ? FunctionUtils.requireFirstItem(focus, true) : null;
325
326      CallingContext callingContext = null;
327      ISequence<?> result = null;
328      if (isDeterministic()) {
329        // check cache
330        callingContext = new CallingContext(arguments, contextItem);
331        // attempt to get the result from the cache
332        result = dynamicContext.getCachedResult(callingContext);
333      }
334
335      if (result == null) {
336        // logger.info(String.format("Executing function '%s' with arguments '%s'.",
337        // toSignature(),
338        // convertedArguments.toString()));
339
340        // INodeItem actualFocus = focus == null ? null : focus.getNodeItem();
341        // if (isFocusDepenent() && actualFocus == null) {
342        // throw new
343        // DynamicMetapathException(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT,
344        // "Null
345        // focus");
346        // }
347        // result = handler.execute(this, convertedArguments, dynamicContext,
348        // actualFocus);
349        result = handler.execute(this, convertedArguments, dynamicContext, contextItem);
350
351        if (callingContext != null) {
352          // add result to cache
353          dynamicContext.cacheResult(callingContext, result);
354        }
355      }
356
357      // logger.info(String.format("Executed function '%s' with arguments '%s'
358      // producing result '%s'",
359      // toSignature(), convertedArguments.toString(), result.asList().toString()));
360      return result;
361    } catch (MetapathException ex) {
362      throw new MetapathException(String.format("Unable to execute function '%s'", toSignature()), ex);
363    }
364  }
365
366  @Override
367  public int hashCode() {
368    return Objects.hash(getName(), getNamespace(), getArguments(), handler, properties, result);
369  }
370
371  @Override
372  public boolean equals(Object obj) {
373    if (this == obj) {
374      return true; // NOPMD - readability
375    }
376    if (obj == null) {
377      return false; // NOPMD - readability
378    }
379    if (getClass() != obj.getClass()) {
380      return false; // NOPMD - readability
381    }
382    DefaultFunction other = (DefaultFunction) obj;
383    return Objects.equals(getName(), other.getName())
384        && Objects.equals(getNamespace(), other.getNamespace())
385        && Objects.equals(getArguments(), other.getArguments())
386        && Objects.equals(handler, other.handler)
387        && Objects.equals(properties, other.properties)
388        && Objects.equals(result, other.result);
389  }
390
391  @Override
392  public String toString() {
393    return toSignature();
394  }
395
396  @Override
397  public String toSignature() {
398    StringBuilder builder = new StringBuilder()
399        .append("Q{")
400        .append(getNamespace())
401        .append('}')
402        .append(getName()) // name
403        .append('('); // arguments
404
405    List<IArgument> arguments = getArguments();
406    if (arguments.isEmpty()) {
407      builder.append("()");
408    } else {
409      builder.append(arguments.stream().map(argument -> argument.toSignature()).collect(Collectors.joining(",")));
410
411      if (isArityUnbounded()) {
412        builder.append(", ...");
413      }
414    }
415
416    builder.append(") as ")
417        .append(getResult().toSignature());// return type
418
419    return builder.toString();
420  }
421
422  public final class CallingContext {
423    @Nullable
424    private final IItem contextItem;
425    @NonNull
426    private final List<ISequence<?>> arguments;
427
428    /**
429     * Set up the execution context for this function.
430     *
431     * @param arguments
432     *          the function arguments
433     * @param contextItem
434     *          the current node context
435     */
436    private CallingContext(@NonNull List<ISequence<?>> arguments, @Nullable IItem contextItem) {
437      this.contextItem = contextItem;
438      this.arguments = arguments;
439    }
440
441    /**
442     * Get the function instance associated with the calling context.
443     *
444     * @return the function instance
445     */
446    @NonNull
447    public DefaultFunction getFunction() {
448      return DefaultFunction.this;
449    }
450
451    /**
452     * Get the node item focus associated with the calling context.
453     *
454     * @return the function instance
455     */
456    @Nullable
457    public IItem getContextItem() {
458      return contextItem;
459    }
460
461    /**
462     * Get the arguments associated with the calling context.
463     *
464     * @return the arguments
465     */
466    @NonNull
467    public List<ISequence<?>> getArguments() {
468      return arguments;
469    }
470
471    @Override
472    public int hashCode() {
473      final int prime = 31;
474      int result = 1;
475      result = prime * result + getFunction().hashCode();
476      result = prime * result + Objects.hash(contextItem, arguments);
477      return result;
478    }
479
480    @Override
481    public boolean equals(Object obj) {
482      if (this == obj) {
483        return true; // NOPMD - readability
484      }
485      if (obj == null) {
486        return false; // NOPMD - readability
487      }
488      if (getClass() != obj.getClass()) {
489        return false; // NOPMD - readability
490      }
491      CallingContext other = (CallingContext) obj;
492      if (!getFunction().equals(other.getFunction())) {
493        return false; // NOPMD - readability
494      }
495      return Objects.equals(arguments, other.arguments) && Objects.equals(contextItem, other.contextItem);
496    }
497  }
498}