1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function.impl;
7   
8   import java.util.ArrayList;
9   import java.util.Iterator;
10  import java.util.List;
11  import java.util.Objects;
12  import java.util.stream.Stream;
13  
14  import dev.metaschema.core.metapath.ContextAbsentDynamicMetapathException;
15  import dev.metaschema.core.metapath.DynamicContext;
16  import dev.metaschema.core.metapath.MetapathException;
17  import dev.metaschema.core.metapath.function.CalledContext;
18  import dev.metaschema.core.metapath.function.IArgument;
19  import dev.metaschema.core.metapath.function.IFunction;
20  import dev.metaschema.core.metapath.item.IItem;
21  import dev.metaschema.core.metapath.item.IItemVisitor;
22  import dev.metaschema.core.metapath.item.ISequence;
23  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
24  import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
25  import dev.metaschema.core.metapath.item.atomic.IStringItem;
26  import dev.metaschema.core.metapath.type.IItemType;
27  import dev.metaschema.core.metapath.type.ISequenceType;
28  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
29  import dev.metaschema.core.qname.IEnhancedQName;
30  import dev.metaschema.core.util.CollectionUtil;
31  import edu.umd.cs.findbugs.annotations.NonNull;
32  import edu.umd.cs.findbugs.annotations.Nullable;
33  
34  /**
35   * This abstract implementation provides common functionality shared by all
36   * functions.
37   */
38  public abstract class AbstractFunction implements IFunction {
39    @NonNull
40    private final IEnhancedQName qname;
41    @NonNull
42    private final List<IArgument> arguments;
43  
44    /**
45     * Construct a new function using the provided name and namespace, used together
46     * to form the function's qualified name, and the provided arguments.
47     * <p>
48     * This constructor is equivalent to calling:
49     *
50     * <pre>
51     * {@code
52     * String name = ...;
53     * String namespace = ...;
54     * List<IArgument> arguments = ...;
55     * new AbstractFunction(IEnhancedQName.of(namespace, name), arguments);
56     * }
57     * </pre>
58     *
59     * @param name
60     *          the function's name
61     * @param namespace
62     *          the function's namespace
63     * @param arguments
64     *          the function's arguments
65     */
66    protected AbstractFunction(
67        @NonNull String name,
68        @NonNull String namespace,
69        @NonNull List<IArgument> arguments) {
70      this(IEnhancedQName.of(namespace, name), arguments);
71    }
72  
73    /**
74     * Construct a new function using the provided qualified name and arguments.
75     *
76     * @param qname
77     *          the function's qualified name
78     * @param arguments
79     *          the function's arguments
80     */
81    protected AbstractFunction(
82        @NonNull IEnhancedQName qname,
83        @NonNull List<IArgument> arguments) {
84      this.qname = qname;
85      this.arguments = arguments;
86    }
87  
88    @Override
89    public IEnhancedQName getQName() {
90      return qname;
91    }
92  
93    @Override
94    public int arity() {
95      return arguments.size();
96    }
97  
98    @Override
99    public List<IArgument> getArguments() {
100     return arguments;
101   }
102 
103   @Override
104   public Object getValue() {
105     // never a value
106     return null;
107   }
108 
109   @Override
110   public void accept(IItemVisitor visitor) {
111     visitor.visit(this);
112   }
113 
114   /**
115    * Converts arguments in an attempt to align with the function's signature.
116    *
117    * @param function
118    *          the function
119    * @param parameters
120    *          the argument parameters
121    * @param dynamicContext
122    *          the dynamic evaluation context
123    * @return a new unmodifiable list containing the converted arguments
124    */
125   @NonNull
126   public static List<ISequence<?>> convertArguments(
127       @NonNull IFunction function,
128       @NonNull List<? extends ISequence<?>> parameters,
129       @NonNull DynamicContext dynamicContext) {
130     List<ISequence<?>> retval = new ArrayList<>(parameters.size());
131     Iterator<IArgument> argumentIterator = function.getArguments().iterator();
132     IArgument argument = null;
133     for (ISequence<?> parameter : parameters) {
134       if (argumentIterator.hasNext()) {
135         argument = argumentIterator.next();
136       } else if (!function.isArityUnbounded()) {
137         throw new InvalidTypeMetapathException(
138             function,
139             String.format("Argument signature doesn't match '%s'.", function.toSignature()));
140       }
141 
142       assert argument != null;
143       assert parameter != null;
144 
145       retval.add(convertArgument(argument, parameter, dynamicContext));
146     }
147     return CollectionUtil.unmodifiableList(retval);
148   }
149 
150   @NonNull
151   private static ISequence<?> convertArgument(
152       @NonNull IArgument argument,
153       @NonNull ISequence<?> parameter,
154       @NonNull DynamicContext dynamicContext) {
155     ISequenceType sequenceType = argument.getSequenceType();
156 
157     // apply occurrence
158     ISequence<?> result = sequenceType.getOccurrence().getSequenceHandler().handle(parameter);
159 
160     // apply function conversion and type promotion to the parameter
161     if (!result.isEmpty()) {
162       IItemType type = sequenceType.getType();
163       // this is not required to be an empty sequence
164       result = convertSequence(argument, result, type, dynamicContext);
165     }
166 
167     // verify resulting values
168     return sequenceType.test(result);
169   }
170 
171   /**
172    * Based on XPath 3.1
173    * <a href="https://www.w3.org/TR/xpath-31/#dt-function-conversion">function
174    * conversion</a> rules.
175    *
176    * @param argument
177    *          the function argument signature details
178    * @param sequence
179    *          the sequence to convert
180    * @param requiredSequenceType
181    *          the expected item type for the sequence
182    * @param dynamicContext
183    *          the dynamic evaluation context
184    * @return the converted sequence
185    */
186   @NonNull
187   protected static ISequence<?> convertSequence(
188       @NonNull IArgument argument,
189       @NonNull ISequence<?> sequence,
190       @NonNull IItemType requiredSequenceType,
191       @NonNull DynamicContext dynamicContext) {
192     Class<? extends IItem> requiredSequenceTypeClass = requiredSequenceType.getItemClass();
193 
194     Stream<? extends IItem> stream = sequence.safeStream();
195 
196     if (IAnyAtomicItem.class.isAssignableFrom(requiredSequenceTypeClass)) {
197       Stream<? extends IAnyAtomicItem> atomicStream = stream.flatMap(IItem::atomize);
198 
199       // if (IUntypedAtomicItem.class.isInstance(item)) {
200       // // TODO: apply cast to atomic type
201       // }
202 
203       if (IStringItem.class.equals(requiredSequenceTypeClass)) {
204         // promote URIs to strings if a string is required
205         atomicStream = atomicStream.map(item -> IAnyUriItem.class.isInstance(item) ? IStringItem.cast(item) : item);
206       }
207 
208       stream = atomicStream;
209     }
210 
211     stream = stream.peek(item -> {
212       if (!requiredSequenceTypeClass.isInstance(item)) {
213         throw new InvalidTypeMetapathException(
214             item,
215             String.format("The type '%s' is not a subtype of '%s'",
216                 item.getClass().getName(),
217                 requiredSequenceTypeClass.getName()));
218       }
219     });
220     assert stream != null;
221 
222     return ISequence.of(stream);
223   }
224 
225   @Nullable
226   private IItem getContextItem(@NonNull ISequence<?> focus) {
227     return isFocusDependent()
228         ? focus.getFirstItem(true)
229         : null;
230   }
231 
232   @Override
233   public ISequence<?> execute(
234       @NonNull List<? extends ISequence<?>> arguments,
235       @NonNull DynamicContext dynamicContext,
236       @NonNull ISequence<?> focus) {
237 
238     try {
239       IItem contextItem = getContextItem(focus);
240       if (isFocusDependent() && contextItem == null) {
241         throw new ContextAbsentDynamicMetapathException("The context item is empty.");
242       }
243 
244       List<ISequence<?>> convertedArguments = convertArguments(this, arguments, dynamicContext);
245 
246       CalledContext callingContext = null;
247       ISequence<?> result = null;
248       if (isDeterministic()) {
249         // check cache
250         callingContext = new CalledContext(this, convertedArguments, contextItem);
251         // TODO: implement something like computeIfAbsent
252         // attempt to get the result from the cache
253         result = dynamicContext.getCachedResult(callingContext);
254       }
255 
256       if (result == null) {
257         result = executeInternal(convertedArguments, dynamicContext, contextItem);
258 
259         if (callingContext != null) {
260           // add result to cache
261           dynamicContext.cacheResult(
262               callingContext,
263               // ensure the result sequence is list backed
264               result.reusable());
265         }
266       }
267 
268       // logger.info(String.format("Executed function '%s' with arguments '%s'
269       // producing result '%s'",
270       // toSignature(), convertedArguments.toString(), result.asList().toString()));
271       return result;
272     } catch (MetapathException ex) {
273       throw ex.registerEvaluationContext(dynamicContext);
274     }
275   }
276 
277   /**
278    * Execute the provided function using the provided arguments, dynamic context,
279    * and focus.
280    *
281    * @param arguments
282    *          the function arguments
283    * @param dynamicContext
284    *          the dynamic evaluation context
285    * @param focus
286    *          the current focus
287    * @return a sequence containing the result of the execution
288    * @throws MetapathException
289    *           if an error occurred while executing the function
290    */
291   @NonNull
292   protected abstract ISequence<?> executeInternal(
293       @NonNull List<ISequence<?>> arguments,
294       @NonNull DynamicContext dynamicContext,
295       @Nullable IItem focus);
296 
297   @Override
298   public int hashCode() {
299     return Objects.hash(getQName(), getArguments());
300   }
301 
302   @Override
303   public boolean equals(Object obj) {
304     if (this == obj) {
305       return true;
306     }
307     if (obj == null || getClass() != obj.getClass()) {
308       return false;
309     }
310     AbstractFunction other = (AbstractFunction) obj;
311     return Objects.equals(getQName(), other.getQName())
312         && Objects.equals(getArguments(), other.getArguments());
313   }
314 
315   @Override
316   public String toString() {
317     return toSignature();
318   }
319 }