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