1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function;
7   
8   import java.util.ArrayList;
9   import java.util.EnumSet;
10  import java.util.LinkedList;
11  import java.util.List;
12  import java.util.Objects;
13  import java.util.Set;
14  import java.util.stream.Collectors;
15  
16  import dev.metaschema.core.metapath.DynamicContext;
17  import dev.metaschema.core.metapath.MetapathException;
18  import dev.metaschema.core.metapath.StaticContext;
19  import dev.metaschema.core.metapath.StaticMetapathException;
20  import dev.metaschema.core.metapath.item.ICollectionValue;
21  import dev.metaschema.core.metapath.item.IItem;
22  import dev.metaschema.core.metapath.item.ISequence;
23  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
24  import dev.metaschema.core.metapath.type.IItemType;
25  import dev.metaschema.core.metapath.type.ISequenceType;
26  import dev.metaschema.core.metapath.type.Occurrence;
27  import dev.metaschema.core.qname.IEnhancedQName;
28  import dev.metaschema.core.util.ObjectUtils;
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  
31  /**
32   * A common interface for all Metapath functions.
33   */
34  public interface IFunction extends IItem {
35    /**
36     * Details specific characteristics of a function.
37     */
38    enum FunctionProperty {
39      /**
40       * Indicates that the function will produce identical results for the same
41       * arguments (see XPath 3.1 <a href=
42       * "https://www.w3.org/TR/xpath-functions-31/#dt-deterministic">deterministic</a>).
43       * If not assigned to a function definition, a function call with the same
44       * arguments is not guaranteed to produce the same results in the same order for
45       * subsequent calls within the same execution context.
46       */
47      DETERMINISTIC,
48      /**
49       * Indicates that the result of the function depends on property values within
50       * the static or dynamic context and the provided arguments (see XPath 3.1
51       * <a href=
52       * "https://www.w3.org/TR/xpath-functions-31/#dt-context-dependent">context-dependent</a>).
53       * If not assigned to a function definition, a call will not be affected by the
54       * property values within the static or dynamic context and will not have any
55       * arguments.
56       */
57      CONTEXT_DEPENDENT,
58      /**
59       * Indicates that the result of the function depends on the current focus (see
60       * XPath 3.1 <a href=
61       * "https://www.w3.org/TR/xpath-functions-31/#dt-focus-independent">focus-dependent</a>).
62       * If not assigned to a function definition, a call will not be affected by the
63       * current focus.
64       */
65      FOCUS_DEPENDENT,
66      /**
67       * The function allows the last argument to be repeated any number of times.
68       */
69      UNBOUNDED_ARITY;
70    }
71  
72    /**
73     * Get the type information for this item.
74     *
75     * @return the type information
76     */
77    @NonNull
78    static IItemType type() {
79      return IItemType.function();
80    }
81  
82    @Override
83    default IItemType getType() {
84      // TODO: implement this based on the signature
85      return IItemType.function();
86    }
87  
88    /**
89     * Retrieve the name of the function.
90     *
91     * @return the function's name
92     */
93    @NonNull
94    default String getName() {
95      return ObjectUtils.notNull(getQName().getLocalName());
96    }
97  
98    /**
99     * Retrieve the namespace qualified name of the function.
100    *
101    * @return the namespace qualified name
102    */
103   @NonNull
104   IEnhancedQName getQName();
105 
106   /**
107    * Retrieve the set of assigned function properties.
108    *
109    * @return the set of properties or an empty set
110    */
111   @NonNull
112   Set<FunctionProperty> getProperties();
113 
114   /**
115    * Retrieve the list of function arguments.
116    *
117    * @return the function arguments or an empty list if there are none
118    */
119   @NonNull
120   List<IArgument> getArguments();
121 
122   /**
123    * Determine the number of arguments the function has.
124    *
125    * @return the number of function arguments
126    */
127   int arity();
128 
129   /**
130    * Determines if the result of the function call will produce identical results
131    * when provided the same implicit or explicit arguments.
132    *
133    * @return {@code true} if function is deterministic or {@code false} otherwise
134    * @see FunctionProperty#DETERMINISTIC
135    */
136   default boolean isDeterministic() {
137     return getProperties().contains(FunctionProperty.DETERMINISTIC);
138   }
139 
140   /**
141    * Determines if the result of the function call depends on property values
142    * within the static or dynamic context and the provided arguments.
143    *
144    * @return {@code true} if function is context dependent or {@code false}
145    *         otherwise
146    * @see FunctionProperty#CONTEXT_DEPENDENT
147    */
148   default boolean isContextDepenent() {
149     return getProperties().contains(FunctionProperty.CONTEXT_DEPENDENT);
150   }
151 
152   /**
153    * Determines if the result of the function call depends on the current focus.
154    *
155    * @return {@code true} if function is focus dependent or {@code false}
156    *         otherwise
157    * @see FunctionProperty#FOCUS_DEPENDENT
158    */
159   default boolean isFocusDependent() {
160     return getProperties().contains(FunctionProperty.FOCUS_DEPENDENT);
161   }
162 
163   /**
164    * Determines if the final argument can be repeated.
165    *
166    * @return {@code true} if the final argument can be repeated or {@code false}
167    *         otherwise
168    * @see FunctionProperty#UNBOUNDED_ARITY
169    */
170   default boolean isArityUnbounded() {
171     return getProperties().contains(FunctionProperty.UNBOUNDED_ARITY);
172   }
173 
174   /**
175    * Retrieve the function result sequence type.
176    *
177    * @return the function result sequence type
178    */
179   @NonNull
180   ISequenceType getResult();
181 
182   /**
183    * Determine if the function is a named function.
184    *
185    * @return {@code true} if the function is named or {@code false} if the
186    *         function is anonymous
187    */
188   boolean isNamedFunction();
189 
190   // /**
191   // * Determines by static analysis if the function supports the expression
192   // arguments provided.
193   // *
194   // * @param arguments
195   // * the expression arguments to evaluate
196   // * @return {@code true} if the arguments are supported or {@code false}
197   // otherwise
198   // */
199   // boolean isSupported(List<IExpression<?>> arguments);
200 
201   @Override
202   default boolean deepEquals(ICollectionValue other, DynamicContext dynamicContext) {
203     // this is always the expected result
204     return false;
205   }
206 
207   /**
208    * Execute the function with the provided {@code arguments}, using the provided
209    * {@code DynamicContext} and {@code focus}.
210    *
211    * @param arguments
212    *          the function arguments or an empty list if there are no arguments
213    * @param dynamicContext
214    *          the dynamic evaluation context
215    * @param focus
216    *          the current focus or an empty sequence if there is no focus
217    * @return the function result
218    * @throws MetapathException
219    *           if an error occurred while executing the function
220    */
221   @NonNull
222   ISequence<?> execute(
223       @NonNull List<? extends ISequence<?>> arguments,
224       @NonNull DynamicContext dynamicContext,
225       @NonNull ISequence<?> focus);
226 
227   @Override
228   default IAnyAtomicItem toAtomicItem() {
229     throw new InvalidTypeFunctionException(InvalidTypeFunctionException.DATA_ITEM_IS_FUNCTION, this);
230   }
231 
232   /**
233    * Get the signature of the function as a string.
234    *
235    * @return the signature
236    */
237   @Override
238   @NonNull
239   default String toSignature() {
240     return ObjectUtils.notNull(String.format("%s(%s) as %s",
241         getQName(),
242         getArguments().isEmpty() ? ""
243             : getArguments().stream().map(IArgument::toSignature).collect(Collectors.joining(","))
244                 + (isArityUnbounded() ? ", ..." : ""),
245         getResult().toSignature()));
246   }
247 
248   /**
249    * Construct a new function signature builder.
250    *
251    * @return the new builder instance
252    */
253   @NonNull
254   static Builder builder() {
255     return builder(StaticContext.instance());
256   }
257 
258   /**
259    * Construct a new function signature builder.
260    *
261    * @param staticContext
262    *          the static context used to lookup data types and function
263    *          implementations
264    *
265    * @return the new builder instance
266    */
267   @NonNull
268   static Builder builder(@NonNull StaticContext staticContext) {
269     return new Builder(staticContext);
270   }
271 
272   /**
273    * Used to create a function's signature using a builder pattern.
274    */
275   @SuppressWarnings("PMD.LooseCoupling")
276   final class Builder {
277     @NonNull
278     private final StaticContext staticContext;
279     private String name;
280     private String namespace;
281     @SuppressWarnings("null")
282     @NonNull
283     private final EnumSet<FunctionProperty> properties = EnumSet.noneOf(FunctionProperty.class);
284     @NonNull
285     private final List<IArgument> arguments = new LinkedList<>();
286     @NonNull
287     private IItemType returnType = IItem.type();
288     @NonNull
289     private Occurrence returnOccurrence = Occurrence.ONE;
290     private IFunctionExecutor functionHandler;
291 
292     private Builder(@NonNull StaticContext staticContext) {
293       this.staticContext = staticContext;
294     }
295 
296     private StaticContext getStaticContext() {
297       return staticContext;
298     }
299 
300     /**
301      * Define the name of the function.
302      *
303      * @param name
304      *          the function's name
305      * @return this builder
306      */
307     @NonNull
308     public Builder name(@NonNull String name) {
309       Objects.requireNonNull(name, "name");
310       if (name.isBlank()) {
311         throw new IllegalArgumentException("the name must be non-blank");
312       }
313       this.name = name.trim();
314       return this;
315     }
316 
317     /**
318      * Define the namespace of the function.
319      *
320      * @param name
321      *          the function's namespace URI as a string
322      * @return this builder
323      */
324     @NonNull
325     public Builder namespace(@NonNull String name) {
326       Objects.requireNonNull(name, "name");
327       if (name.isBlank()) {
328         throw new IllegalArgumentException("the name must be non-blank");
329       }
330       this.namespace = name.trim();
331       return this;
332     }
333 
334     /**
335      * Mark the function as deterministic.
336      *
337      * @return this builder
338      * @see IFunction.FunctionProperty#DETERMINISTIC
339      */
340     @NonNull
341     public Builder deterministic() {
342       properties.add(FunctionProperty.DETERMINISTIC);
343       return this;
344     }
345 
346     /**
347      * Mark the function as non-deterministic.
348      *
349      * @return this builder
350      * @see IFunction.FunctionProperty#DETERMINISTIC
351      */
352     @NonNull
353     public Builder nonDeterministic() {
354       properties.remove(FunctionProperty.DETERMINISTIC);
355       return this;
356     }
357 
358     /**
359      * Mark the function as context dependent.
360      *
361      * @return this builder
362      * @see IFunction.FunctionProperty#CONTEXT_DEPENDENT
363      */
364     @NonNull
365     public Builder contextDependent() {
366       properties.add(FunctionProperty.CONTEXT_DEPENDENT);
367       return this;
368     }
369 
370     /**
371      * Mark the function as context independent.
372      *
373      * @return this builder
374      * @see IFunction.FunctionProperty#CONTEXT_DEPENDENT
375      */
376     @NonNull
377     public Builder contextIndependent() {
378       properties.remove(FunctionProperty.CONTEXT_DEPENDENT);
379       return this;
380     }
381 
382     /**
383      * Mark the function as focus dependent.
384      *
385      * @return this builder
386      * @see IFunction.FunctionProperty#FOCUS_DEPENDENT
387      */
388     @NonNull
389     public Builder focusDependent() {
390       properties.add(FunctionProperty.FOCUS_DEPENDENT);
391       return this;
392     }
393 
394     /**
395      * Mark the function as focus independent.
396      *
397      * @return this builder
398      * @see IFunction.FunctionProperty#FOCUS_DEPENDENT
399      */
400     @NonNull
401     public Builder focusIndependent() {
402       properties.remove(FunctionProperty.FOCUS_DEPENDENT);
403       return this;
404     }
405 
406     /**
407      * Indicate if the last argument can be repeated.
408      *
409      * @param allow
410      *          if {@code true} then the the last argument can be repeated an
411      *          unlimited number of times, or {@code false} otherwise
412      * @return this builder
413      */
414     @NonNull
415     public Builder allowUnboundedArity(boolean allow) {
416       if (allow) {
417         properties.add(FunctionProperty.UNBOUNDED_ARITY);
418       } else {
419         properties.remove(FunctionProperty.UNBOUNDED_ARITY);
420       }
421       return this;
422     }
423 
424     /**
425      * Define the return sequence Java type of the function.
426      *
427      * @param name
428      *          the extended qualified name of the function's return data type
429      * @return this builder
430      */
431     @NonNull
432     public Builder returnType(@NonNull String name) {
433       try {
434         this.returnType = getStaticContext().lookupAtomicType(name);
435       } catch (StaticMetapathException ex) {
436         throw new IllegalArgumentException(
437             String.format("No data type with the name '%s'.", name), ex);
438       }
439       return this;
440     }
441 
442     /**
443      * Define the return sequence Java type of the function.
444      *
445      * @param name
446      *          the qualified name of the function's return data type
447      * @return this builder
448      */
449     @NonNull
450     public Builder returnType(@NonNull IEnhancedQName name) {
451       try {
452         this.returnType = StaticContext.lookupAtomicType(name);
453       } catch (StaticMetapathException ex) {
454         throw new IllegalArgumentException(
455             String.format("No data type with the name '%s'.", name), ex);
456       }
457       return this;
458     }
459 
460     /**
461      * Define the return sequence Java type of the function.
462      *
463      * @param type
464      *          the function's return Java type
465      * @return this builder
466      */
467     @NonNull
468     public Builder returnType(@NonNull IItemType type) {
469       this.returnType = type;
470       return this;
471     }
472 
473     /**
474      * Indicate the sequence returned will contain zero or one items.
475      *
476      * @return this builder
477      */
478     @NonNull
479     public Builder returnZeroOrOne() {
480       return returnOccurrence(Occurrence.ZERO_OR_ONE);
481     }
482 
483     /**
484      * Indicate the sequence returned will contain one item.
485      *
486      * @return this builder
487      */
488     @NonNull
489     public Builder returnOne() {
490       return returnOccurrence(Occurrence.ONE);
491     }
492 
493     /**
494      * Indicate the sequence returned will contain zero or more items.
495      *
496      * @return this builder
497      */
498     @NonNull
499     public Builder returnZeroOrMore() {
500       return returnOccurrence(Occurrence.ZERO_OR_MORE);
501     }
502 
503     /**
504      * Indicate the sequence returned will contain one or more items.
505      *
506      * @return this builder
507      */
508     @NonNull
509     public Builder returnOneOrMore() {
510       return returnOccurrence(Occurrence.ONE_OR_MORE);
511     }
512 
513     @NonNull
514     private Builder returnOccurrence(@NonNull Occurrence occurrence) {
515       Objects.requireNonNull(occurrence, "occurrence");
516       this.returnOccurrence = occurrence;
517       return this;
518     }
519 
520     /**
521      * Add an argument based on the provided {@code builder}.
522      *
523      * @param builder
524      *          the argument builder
525      * @return this builder
526      */
527     @NonNull
528     public Builder argument(@NonNull IArgument.Builder builder) {
529       return argument(builder.build());
530     }
531 
532     /**
533      * Add an argument based on the provided {@code argument} signature.
534      *
535      * @param argument
536      *          the argument
537      * @return this builder
538      */
539     @NonNull
540     public Builder argument(@NonNull IArgument argument) {
541       Objects.requireNonNull(argument, "argument");
542       this.arguments.add(argument);
543       return this;
544     }
545 
546     /**
547      * Specify the static function to call when executing the function.
548      *
549      * @param handler
550      *          a method implementing the {@link IFunctionExecutor} functional
551      *          interface
552      * @return this builder
553      */
554     @NonNull
555     public Builder functionHandler(@NonNull IFunctionExecutor handler) {
556       Objects.requireNonNull(handler, "handler");
557       this.functionHandler = handler;
558       return this;
559     }
560 
561     /**
562      * Builds the function's signature.
563      *
564      * @return the function's signature
565      */
566     @NonNull
567     public IFunction build() {
568       if (properties.contains(FunctionProperty.UNBOUNDED_ARITY) && arguments.isEmpty()) {
569         throw new IllegalStateException("to allow unbounded arity, at least one argument must be provided");
570       }
571 
572       return new DefaultFunction(
573           ObjectUtils.requireNonNull(name, "the name must not be null"),
574           ObjectUtils.requireNonNull(namespace, "the namespace must not be null"),
575           properties,
576           new ArrayList<>(arguments),
577           // FIXME: Should return type be ISequenceType?
578           ISequenceType.of(returnType, returnOccurrence),
579           ObjectUtils.requireNonNull(functionHandler, "the function handler must not be null"));
580     }
581   }
582 }