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.InvalidTypeMetapathException;
12  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
13  import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
14  import gov.nist.secauto.metaschema.core.metapath.item.IItem;
15  import gov.nist.secauto.metaschema.core.metapath.item.TypeSystem;
16  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
17  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyUriItem;
18  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
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    /**
86     * Converts arguments in an attempt to align with the function's signature.
87     *
88     * @param function
89     *          the function
90     * @param parameters
91     *          the argument parameters
92     * @return the converted argument list
93     */
94    @NonNull
95    public static List<ISequence<?>> convertArguments(
96        @NonNull IFunction function,
97        @NonNull List<? extends ISequence<?>> parameters) {
98      @NonNull
99      List<ISequence<?>> retval = new ArrayList<>(parameters.size());
100 
101     Iterator<IArgument> argumentIterator = function.getArguments().iterator();
102     IArgument argument = null;
103     for (ISequence<?> parameter : parameters) {
104       if (argumentIterator.hasNext()) {
105         argument = argumentIterator.next();
106       } else if (!function.isArityUnbounded()) {
107         throw new InvalidTypeMetapathException(
108             null,
109             String.format("argument signature doesn't match '%s'", function.toSignature()));
110       }
111 
112       assert argument != null;
113       assert parameter != null;
114 
115       retval.add(convertArgument(argument, parameter));
116     }
117     return retval;
118   }
119 
120   @NonNull
121   private static ISequence<?> convertArgument(
122       @NonNull IArgument argument,
123       @NonNull ISequence<?> parameter) {
124     // apply occurrence
125     ISequence<?> retval = argument.getSequenceType().getOccurrence().getSequenceHandler().handle(parameter);
126 
127     // apply function conversion and type promotion to the parameter
128     if (!retval.isEmpty()) {
129       retval = convertSequence(argument, retval);
130 
131       // verify resulting values
132       Class<? extends IItem> argumentClass = argument.getSequenceType().getType();
133       for (IItem item : retval.getValue()) {
134         Class<? extends IItem> itemClass = item.getClass();
135         if (!argumentClass.isAssignableFrom(itemClass)) {
136           throw new InvalidTypeMetapathException(
137               item,
138               String.format("The type '%s' is not a subtype of '%s'",
139                   TypeSystem.getName(itemClass),
140                   TypeSystem.getName(argumentClass)));
141         }
142       }
143     }
144     return retval;
145   }
146 
147   /**
148    * Based on XPath 3.1
149    * <a href="https://www.w3.org/TR/xpath-31/#dt-function-conversion">function
150    * conversion</a> rules.
151    *
152    * @param argument
153    *          the function argument signature details
154    * @param sequence
155    *          the sequence to convert
156    * @return the converted sequence
157    */
158   @NonNull
159   protected static ISequence<?> convertSequence(@NonNull IArgument argument, @NonNull ISequence<?> sequence) {
160     ISequenceType requiredSequenceType = argument.getSequenceType();
161     Class<? extends IItem> requiredSequenceTypeClass = requiredSequenceType.getType();
162 
163     Stream<? extends IItem> stream = sequence.safeStream();
164 
165     if (IAnyAtomicItem.class.isAssignableFrom(requiredSequenceTypeClass)) {
166       Stream<? extends IAnyAtomicItem> atomicStream = stream.flatMap(FnData::atomize);
167 
168       // if (IUntypedAtomicItem.class.isInstance(item)) { // NOPMD
169       // // TODO: apply cast to atomic type
170       // }
171 
172       if (IStringItem.class.equals(requiredSequenceTypeClass)) {
173         // promote URIs to strings if a string is required
174         atomicStream = atomicStream.map(item -> IAnyUriItem.class.isInstance(item) ? IStringItem.cast(item) : item);
175       }
176 
177       stream = atomicStream;
178     }
179 
180     stream = stream.peek(item -> {
181       if (!requiredSequenceTypeClass.isInstance(item)) {
182         throw new InvalidTypeMetapathException(
183             item,
184             String.format("The type '%s' is not a subtype of '%s'",
185                 item.getClass().getName(),
186                 requiredSequenceTypeClass.getName()));
187       }
188     });
189     assert stream != null;
190 
191     return ISequence.of(stream);
192   }
193 
194   private IItem getContextItem(@NonNull ISequence<?> focus) {
195     IItem contextItem = null;
196     if (isFocusDepenent()) {
197       contextItem = focus.getFirstItem(true);
198       if (contextItem == null) {
199         throw new DynamicMetapathException(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT, "The context is empty");
200       }
201     }
202     return contextItem;
203   }
204 
205   @Override
206   public ISequence<?> execute(
207       @NonNull List<? extends ISequence<?>> arguments,
208       @NonNull DynamicContext dynamicContext,
209       @NonNull ISequence<?> focus) {
210 
211     try {
212       IItem contextItem = getContextItem(focus);
213 
214       List<ISequence<?>> convertedArguments = convertArguments(this, arguments);
215 
216       CallingContext callingContext = null;
217       ISequence<?> result = null;
218       if (isDeterministic()) {
219         // check cache
220         callingContext = new CallingContext(convertedArguments, contextItem);
221         // TODO: implement something like computeIfAbsent
222         // attempt to get the result from the cache
223         result = dynamicContext.getCachedResult(callingContext);
224       }
225 
226       if (result == null) {
227         result = handler.execute(this, convertedArguments, dynamicContext, contextItem);
228 
229         if (callingContext != null) {
230           // add result to cache
231           dynamicContext.cacheResult(callingContext, result);
232         }
233       }
234 
235       // logger.info(String.format("Executed function '%s' with arguments '%s'
236       // producing result '%s'",
237       // toSignature(), convertedArguments.toString(), result.asList().toString()));
238       return result;
239     } catch (MetapathException ex) {
240       throw new MetapathException(String.format("Unable to execute function '%s'", toSignature()), ex);
241     }
242   }
243 
244   @Override
245   public int hashCode() {
246     return Objects.hash(getQName(), getArguments(), handler, properties, result);
247   }
248 
249   @Override
250   public boolean equals(Object obj) {
251     if (this == obj) {
252       return true; // NOPMD - readability
253     }
254     if (obj == null || getClass() != obj.getClass()) {
255       return false; // NOPMD - readability
256     }
257     DefaultFunction other = (DefaultFunction) obj;
258     return Objects.equals(getQName(), other.getQName())
259         && Objects.equals(getArguments(), other.getArguments())
260         && Objects.equals(handler, other.handler)
261         && Objects.equals(properties, other.properties)
262         && Objects.equals(result, other.result);
263   }
264 
265   @Override
266   public String toString() {
267     return toSignature();
268   }
269 
270   public final class CallingContext {
271     @Nullable
272     private final IItem contextItem;
273     @NonNull
274     private final List<ISequence<?>> arguments;
275 
276     /**
277      * Set up the execution context for this function.
278      *
279      * @param arguments
280      *          the function arguments
281      * @param contextItem
282      *          the current node context
283      */
284     private CallingContext(@NonNull List<ISequence<?>> arguments, @Nullable IItem contextItem) {
285       this.contextItem = contextItem;
286       this.arguments = arguments;
287     }
288 
289     /**
290      * Get the function instance associated with the calling context.
291      *
292      * @return the function instance
293      */
294     @NonNull
295     public DefaultFunction getFunction() {
296       return DefaultFunction.this;
297     }
298 
299     /**
300      * Get the node item focus associated with the calling context.
301      *
302      * @return the function instance
303      */
304     @Nullable
305     public IItem getContextItem() {
306       return contextItem;
307     }
308 
309     /**
310      * Get the arguments associated with the calling context.
311      *
312      * @return the arguments
313      */
314     @NonNull
315     public List<ISequence<?>> getArguments() {
316       return arguments;
317     }
318 
319     @Override
320     public int hashCode() {
321       final int prime = 31;
322       int result = 1;
323       result = prime * result + getFunction().hashCode();
324       return prime * result + Objects.hash(contextItem, arguments);
325     }
326 
327     @Override
328     public boolean equals(Object obj) {
329       if (this == obj) {
330         return true; // NOPMD - readability
331       }
332       if (obj == null || getClass() != obj.getClass()) {
333         return false; // NOPMD - readability
334       }
335       CallingContext other = (CallingContext) obj;
336       if (!getFunction().equals(other.getFunction())) {
337         return false; // NOPMD - readability
338       }
339       return Objects.equals(arguments, other.arguments) && Objects.equals(contextItem, other.contextItem);
340     }
341   }
342 }