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