1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.metapath.function;
7   
8   import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
9   import gov.nist.secauto.metaschema.core.metapath.ISequence;
10  import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
11  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
12  import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
13  import gov.nist.secauto.metaschema.core.metapath.item.IItem;
14  import gov.nist.secauto.metaschema.core.metapath.item.TypeSystem;
15  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
16  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyUriItem;
17  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.EnumSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Set;
27  import java.util.stream.Stream;
28  
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  import edu.umd.cs.findbugs.annotations.Nullable;
31  
32  /**
33   * Provides a concrete implementation of a function call executor.
34   */
35  public class DefaultFunction
36      extends AbstractFunction {
37    // private static final Logger logger =
38    // LogManager.getLogger(AbstractFunction.class);
39  
40    @NonNull
41    private final Set<FunctionProperty> properties;
42    @NonNull
43    private final ISequenceType result;
44    @NonNull
45    private final IFunctionExecutor handler;
46  
47    /**
48     * Construct a new function signature.
49     *
50     * @param name
51     *          the name of the function
52     * @param properties
53     *          the characteristics of the function
54     * @param arguments
55     *          the argument signatures or an empty list
56     * @param result
57     *          the type of the result
58     * @param handler
59     *          the handler to call to execute the function
60     */
61    @SuppressWarnings({ "null", "PMD.LooseCoupling" })
62    DefaultFunction(
63        @NonNull String name,
64        @NonNull String namespace,
65        @NonNull EnumSet<FunctionProperty> properties,
66        @NonNull List<IArgument> arguments,
67        @NonNull ISequenceType result,
68        @NonNull IFunctionExecutor handler) {
69      super(name, namespace, arguments);
70      this.properties = Collections.unmodifiableSet(properties);
71      this.result = result;
72      this.handler = handler;
73    }
74  
75    @Override
76    public Set<FunctionProperty> getProperties() {
77      return properties;
78    }
79  
80    @Override
81    public ISequenceType getResult() {
82      return result;
83    }
84    //
85    // @Override
86    // public boolean isSupported(List<IExpression<?>> expressionArguments) {
87    // boolean retval;
88    // if (expressionArguments.isEmpty() && getArguments().isEmpty()) {
89    // // no arguments
90    // retval = true;
91    // // } else if (arity() == 1 && expressionArguments.isEmpty()) {
92    // // // the context item will be the argument
93    // // // TODO: check the context item for type compatibility
94    // // retval = true;
95    // } else if ((expressionArguments.size() == getArguments().size())
96    // || (isArityUnbounded() && expressionArguments.size() >
97    // getArguments().size())) {
98    // retval = true;
99    // // check that argument requirements are satisfied
100   // Iterator<IArgument> argumentIterator = getArguments().iterator();
101   // Iterator<IExpression<?>> expressionIterator = expressionArguments.iterator();
102   //
103   // IArgument argument = null;
104   // while (argumentIterator.hasNext()) {
105   // argument = argumentIterator.next();
106   // IExpression<?> expression = expressionIterator.hasNext() ?
107   // expressionIterator.next() : null;
108   //
109   // if (expression != null) {
110   // // is the expression supported by the argument?
111   // retval = argument.isSupported(expression);
112   // if (!retval) {
113   // break;
114   // }
115   // } else {
116   // // there are no more expression arguments. Make sure that the remaining
117   // arguments are optional
118   // if (!argument.getSequenceType().getOccurrence().isOptional()) {
119   // retval = false;
120   // break;
121   // }
122   // }
123   // }
124   //
125   // if (retval && expressionIterator.hasNext()) {
126   // if (isArityUnbounded()) {
127   // // check remaining expressions against the last argument
128   // while (expressionIterator.hasNext()) {
129   // IExpression<?> expression = expressionIterator.next();
130   // @SuppressWarnings("null")
131   // boolean result = argument.isSupported(expression);
132   // if (!result) {
133   // retval = result;
134   // break;
135   // }
136   // }
137   // } else {
138   // // there are extra expressions, which do not match the arguments
139   // retval = false;
140   // }
141   // }
142   // } else {
143   // retval = false;
144   // }
145   // return retval;
146   // }
147 
148   /**
149    * Converts arguments in an attempt to align with the function's signature.
150    *
151    * @param function
152    *          the function
153    * @param parameters
154    *          the argument parameters
155    * @return the converted argument list
156    */
157   @NonNull
158   public static List<ISequence<?>> convertArguments(
159       @NonNull IFunction function,
160       @NonNull List<ISequence<?>> parameters) {
161     @NonNull List<ISequence<?>> retval = new ArrayList<>(parameters.size());
162 
163     Iterator<IArgument> argumentIterator = function.getArguments().iterator();
164     Iterator<ISequence<?>> parametersIterator = parameters.iterator();
165 
166     IArgument argument = null;
167     while (parametersIterator.hasNext()) {
168       if (argumentIterator.hasNext()) {
169         argument = argumentIterator.next();
170       } else if (!function.isArityUnbounded()) {
171         throw new InvalidTypeMetapathException(
172             null,
173             String.format("argument signature doesn't match '%s'", function.toSignature()));
174       }
175 
176       ISequence<?> parameter = parametersIterator.next();
177       assert argument != null;
178       assert parameter != null;
179 
180       retval.add(convertArgument(argument, parameter));
181     }
182     return retval;
183   }
184 
185   @NonNull
186   private static ISequence<?> convertArgument(
187       @NonNull IArgument argument,
188       @NonNull ISequence<?> parameter) {
189     // apply occurrence
190     ISequence<?> retval = argument.getSequenceType().getOccurrence().getSequenceHandler().handle(parameter);
191 
192     // apply function conversion and type promotion to the parameter
193     if (!retval.isEmpty()) {
194       retval = convertSequence(argument, retval);
195 
196       // verify resulting values
197       Class<? extends IItem> argumentClass = argument.getSequenceType().getType();
198       for (IItem item : retval.getValue()) {
199         Class<? extends IItem> itemClass = item.getClass();
200         if (!argumentClass.isAssignableFrom(itemClass)) {
201           throw new InvalidTypeMetapathException(
202               item,
203               String.format("The type '%s' is not a subtype of '%s'",
204                   TypeSystem.getName(itemClass),
205                   TypeSystem.getName(argumentClass)));
206         }
207       }
208     }
209     return retval;
210   }
211 
212   /**
213    * Based on XPath 3.1
214    * <a href="https://www.w3.org/TR/xpath-31/#dt-function-conversion">function
215    * conversion</a> rules.
216    *
217    * @param argument
218    *          the function argument signature details
219    * @param sequence
220    *          the sequence to convert
221    * @return the converted sequence
222    */
223   @NonNull
224   protected static ISequence<?> convertSequence(@NonNull IArgument argument, @NonNull ISequence<?> sequence) {
225     ISequenceType requiredSequenceType = argument.getSequenceType();
226     Class<? extends IItem> requiredSequenceTypeClass = requiredSequenceType.getType();
227 
228     Stream<? extends IItem> stream = sequence.safeStream();
229 
230     if (IAnyAtomicItem.class.isAssignableFrom(requiredSequenceTypeClass)) {
231       Stream<? extends IAnyAtomicItem> atomicStream = stream.flatMap(FnData::atomize);
232 
233       // if (IUntypedAtomicItem.class.isInstance(item)) { // NOPMD
234       // // TODO: apply cast to atomic type
235       // }
236 
237       if (IStringItem.class.equals(requiredSequenceTypeClass)) {
238         // promote URIs to strings if a string is required
239         atomicStream = atomicStream.map(item -> IAnyUriItem.class.isInstance(item) ? IStringItem.cast(item) : item);
240       }
241 
242       stream = atomicStream;
243     }
244 
245     stream = stream.peek(item -> {
246       if (!requiredSequenceTypeClass.isInstance(item)) {
247         throw new InvalidTypeMetapathException(
248             item,
249             String.format("The type '%s' is not a subtype of '%s'",
250                 item.getClass().getName(),
251                 requiredSequenceTypeClass.getName()));
252       }
253     });
254     assert stream != null;
255 
256     return ISequence.of(stream);
257   }
258 
259   @Override
260   public ISequence<?> execute(
261       @NonNull List<ISequence<?>> arguments,
262       @NonNull DynamicContext dynamicContext,
263       @NonNull ISequence<?> focus) {
264     try {
265       List<ISequence<?>> convertedArguments = convertArguments(this, arguments);
266 
267       IItem contextItem = isFocusDepenent() ? ObjectUtils.requireNonNull(focus.getFirstItem(true)) : null;
268 
269       CallingContext callingContext = null;
270       ISequence<?> result = null;
271       if (isDeterministic()) {
272         // check cache
273         callingContext = new CallingContext(arguments, contextItem);
274         // TODO: implement something like computeIfAbsent
275         // attempt to get the result from the cache
276         result = dynamicContext.getCachedResult(callingContext);
277       }
278 
279       if (result == null) {
280         result = handler.execute(this, convertedArguments, dynamicContext, contextItem);
281 
282         if (callingContext != null) {
283           // add result to cache
284           dynamicContext.cacheResult(callingContext, result);
285         }
286       }
287 
288       // logger.info(String.format("Executed function '%s' with arguments '%s'
289       // producing result '%s'",
290       // toSignature(), convertedArguments.toString(), result.asList().toString()));
291       return result;
292     } catch (MetapathException ex) {
293       throw new MetapathException(String.format("Unable to execute function '%s'", toSignature()), ex);
294     }
295   }
296 
297   @Override
298   public int hashCode() {
299     return Objects.hash(getQName(), getArguments(), handler, properties, result);
300   }
301 
302   @Override
303   public boolean equals(Object obj) {
304     if (this == obj) {
305       return true; // NOPMD - readability
306     }
307     if (obj == null) {
308       return false; // NOPMD - readability
309     }
310     if (getClass() != obj.getClass()) {
311       return false; // NOPMD - readability
312     }
313     DefaultFunction other = (DefaultFunction) obj;
314     return Objects.equals(getQName(), other.getQName())
315         && Objects.equals(getArguments(), other.getArguments())
316         && Objects.equals(handler, other.handler)
317         && Objects.equals(properties, other.properties)
318         && Objects.equals(result, other.result);
319   }
320 
321   @Override
322   public String toString() {
323     return toSignature();
324   }
325 
326   public final class CallingContext {
327     @Nullable
328     private final IItem contextItem;
329     @NonNull
330     private final List<ISequence<?>> arguments;
331 
332     /**
333      * Set up the execution context for this function.
334      *
335      * @param arguments
336      *          the function arguments
337      * @param contextItem
338      *          the current node context
339      */
340     private CallingContext(@NonNull List<ISequence<?>> arguments, @Nullable IItem contextItem) {
341       this.contextItem = contextItem;
342       this.arguments = arguments;
343     }
344 
345     /**
346      * Get the function instance associated with the calling context.
347      *
348      * @return the function instance
349      */
350     @NonNull
351     public DefaultFunction getFunction() {
352       return DefaultFunction.this;
353     }
354 
355     /**
356      * Get the node item focus associated with the calling context.
357      *
358      * @return the function instance
359      */
360     @Nullable
361     public IItem getContextItem() {
362       return contextItem;
363     }
364 
365     /**
366      * Get the arguments associated with the calling context.
367      *
368      * @return the arguments
369      */
370     @NonNull
371     public List<ISequence<?>> getArguments() {
372       return arguments;
373     }
374 
375     @Override
376     public int hashCode() {
377       final int prime = 31;
378       int result = 1;
379       result = prime * result + getFunction().hashCode();
380       result = prime * result + Objects.hash(contextItem, arguments);
381       return result;
382     }
383 
384     @Override
385     public boolean equals(Object obj) {
386       if (this == obj) {
387         return true; // NOPMD - readability
388       }
389       if (obj == null) {
390         return false; // NOPMD - readability
391       }
392       if (getClass() != obj.getClass()) {
393         return false; // NOPMD - readability
394       }
395       CallingContext other = (CallingContext) obj;
396       if (!getFunction().equals(other.getFunction())) {
397         return false; // NOPMD - readability
398       }
399       return Objects.equals(arguments, other.arguments) && Objects.equals(contextItem, other.contextItem);
400     }
401   }
402 }