View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.core.metapath.function;
28  
29  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
30  import gov.nist.secauto.metaschema.core.metapath.ISequence;
31  import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
32  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
33  import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
34  import gov.nist.secauto.metaschema.core.metapath.item.IItem;
35  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
36  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyUriItem;
37  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
38  
39  import java.util.ArrayList;
40  import java.util.Collections;
41  import java.util.EnumSet;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Objects;
45  import java.util.Set;
46  import java.util.stream.Collectors;
47  
48  import edu.umd.cs.findbugs.annotations.NonNull;
49  import edu.umd.cs.findbugs.annotations.Nullable;
50  
51  /**
52   * Provides a concrete implementation of a function call executor.
53   */
54  public class DefaultFunction
55      extends AbstractFunction {
56    // private static final Logger logger =
57    // LogManager.getLogger(AbstractFunction.class);
58  
59    @NonNull
60    private final Set<FunctionProperty> properties;
61    @NonNull
62    private final ISequenceType result;
63    @NonNull
64    private final IFunctionExecutor handler;
65  
66    /**
67     * Construct a new function signature.
68     *
69     * @param name
70     *          the name of the function
71     * @param properties
72     *          the characteristics of the function
73     * @param arguments
74     *          the argument signatures or an empty list
75     * @param result
76     *          the type of the result
77     * @param handler
78     *          the handler to call to execute the function
79     */
80    @SuppressWarnings({ "null", "PMD.LooseCoupling" })
81    DefaultFunction(
82        @NonNull String name,
83        @NonNull String namespace,
84        @NonNull EnumSet<FunctionProperty> properties,
85        @NonNull List<IArgument> arguments,
86        @NonNull ISequenceType result,
87        @NonNull IFunctionExecutor handler) {
88      super(name, namespace, arguments);
89      this.properties = Collections.unmodifiableSet(properties);
90      this.result = result;
91      this.handler = handler;
92    }
93  
94    @Override
95    public Set<FunctionProperty> getProperties() {
96      return properties;
97    }
98  
99    @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 }