1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.metapath;
7   
8   import com.github.benmanes.caffeine.cache.Caffeine;
9   
10  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
11  import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
12  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
13  import gov.nist.secauto.metaschema.core.metapath.function.CalledContext;
14  import gov.nist.secauto.metaschema.core.metapath.function.DateTimeFunctionException;
15  import gov.nist.secauto.metaschema.core.metapath.function.IFunction;
16  import gov.nist.secauto.metaschema.core.metapath.function.IFunction.FunctionProperty;
17  import gov.nist.secauto.metaschema.core.metapath.item.ISequence;
18  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
19  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
20  import gov.nist.secauto.metaschema.core.model.IUriResolver;
21  import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
22  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
23  
24  import java.io.IOException;
25  import java.net.URI;
26  import java.time.Clock;
27  import java.time.Duration;
28  import java.time.LocalDateTime;
29  import java.time.ZoneId;
30  import java.time.ZoneOffset;
31  import java.time.ZonedDateTime;
32  import java.util.ArrayDeque;
33  import java.util.Collections;
34  import java.util.Deque;
35  import java.util.HashMap;
36  import java.util.Map;
37  import java.util.concurrent.ConcurrentHashMap;
38  import java.util.concurrent.TimeUnit;
39  import java.util.stream.Collectors;
40  
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  import edu.umd.cs.findbugs.annotations.Nullable;
43  
44  // TODO: add support for in-scope namespaces
45  /**
46   * The implementation of a Metapath
47   * <a href="https://www.w3.org/TR/xpath-31/#eval_context">dynamic context</a>.
48   */
49  public class DynamicContext { // NOPMD - intentional data class
50  
51    @NonNull
52    private final Map<Integer, ISequence<?>> letVariableMap;
53    @NonNull
54    private final SharedState sharedState;
55  
56    /**
57     * Construct a new dynamic context with a default static context.
58     */
59    public DynamicContext() {
60      this(StaticContext.instance());
61    }
62  
63    /**
64     * Construct a new Metapath dynamic context using the provided static context.
65     *
66     * @param staticContext
67     *          the Metapath static context
68     */
69    public DynamicContext(@NonNull StaticContext staticContext) {
70      this.letVariableMap = new ConcurrentHashMap<>();
71      this.sharedState = new SharedState(staticContext);
72    }
73  
74    private DynamicContext(@NonNull DynamicContext context) {
75      this.letVariableMap = new ConcurrentHashMap<>(context.letVariableMap);
76      this.sharedState = context.sharedState;
77    }
78  
79    private static class SharedState {
80      @NonNull
81      private final StaticContext staticContext;
82      @NonNull
83      private final ZonedDateTime currentDateTime;
84      @NonNull
85      private final Map<URI, IDocumentNodeItem> availableDocuments;
86      @NonNull
87      private final Map<CalledContext, ISequence<?>> functionResultCache;
88      @Nullable
89      private CachingLoader documentLoader;
90      @NonNull
91      private final IMutableConfiguration<MetapathEvaluationFeature<?>> configuration;
92      @NonNull
93      private final Deque<IExpression> executionStack = new ArrayDeque<>();
94      @NonNull
95      private ZoneId implicitTimeZone;
96  
97      public SharedState(@NonNull StaticContext staticContext) {
98        this.staticContext = staticContext;
99  
100       Clock clock = Clock.systemDefaultZone();
101 
102       this.implicitTimeZone = ObjectUtils.notNull(clock.getZone());
103 
104       this.currentDateTime = ObjectUtils.notNull(ZonedDateTime.now(clock));
105       this.availableDocuments = new HashMap<>();
106       this.functionResultCache = ObjectUtils.notNull(Caffeine.newBuilder()
107           .maximumSize(5000)
108           .expireAfterAccess(10, TimeUnit.MINUTES)
109           .<CalledContext, ISequence<?>>build().asMap());
110       this.configuration = new DefaultConfiguration<>();
111       this.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
112     }
113   }
114 
115   /**
116    * Generate a new dynamic context that is a copy of this dynamic context.
117    * <p>
118    * This method can be used to create a new sub-context where changes can be made
119    * without affecting this context. This is useful for setting information that
120    * is only used in a limited evaluation sub-scope, such as for handling variable
121    * assignment.
122    *
123    * @return a new dynamic context
124    */
125   @NonNull
126   public DynamicContext subContext() {
127     return new DynamicContext(this);
128   }
129 
130   /**
131    * Get the static context associated with this dynamic context.
132    *
133    * @return the associated static context
134    */
135   @NonNull
136   public StaticContext getStaticContext() {
137     return sharedState.staticContext;
138   }
139 
140   /**
141    * Get the default time zone used for evaluation.
142    *
143    * @return the time zone identifier object
144    */
145   @NonNull
146   public ZoneId getImplicitTimeZone() {
147     return sharedState.implicitTimeZone;
148   }
149 
150   /**
151    * Get the default time zone used for evaluation.
152    *
153    * @return the time zone identifier object
154    */
155   @NonNull
156   public IDayTimeDurationItem getImplicitTimeZoneAsDayTimeDuration() {
157     LocalDateTime referenceDateTime = MetapathConstants.REFERENCE_DATE_TIME.asLocalDateTime();
158     ZonedDateTime reference = referenceDateTime.atZone(getImplicitTimeZone());
159     ZonedDateTime referenceZ = referenceDateTime.atZone(ZoneOffset.UTC);
160 
161     return IDayTimeDurationItem.valueOf(ObjectUtils.notNull(
162         Duration.between(
163             reference,
164             referenceZ)));
165   }
166 
167   /**
168    * Set the implicit timezone to the provided value.
169    * <p>
170    * Note: This value should only be adjusted when the context is first created.
171    * Once the context is used, this value is expected to be stable.
172    *
173    * @param timezone
174    *          the timezone to use
175    */
176   public void setImplicitTimeZone(@NonNull ZoneId timezone) {
177     sharedState.implicitTimeZone = timezone;
178   }
179 
180   /**
181    * Set the implicit timezone to the provided value.
182    * <p>
183    * Note: This value should only be adjusted when the context is first created.
184    * Once the context is used, this value is expected to be stable.
185    *
186    * @param offset
187    *          the offset which must be &gt;= -PT14H and &lt;= PT13H
188    * @throws DateTimeFunctionException
189    *           with the code
190    *           {@link DateTimeFunctionException#INVALID_TIME_ZONE_VALUE_ERROR} if
191    *           the offset is &lt; -PT14H or &gt; PT14H
192    */
193   public void setImplicitTimeZone(@NonNull IDayTimeDurationItem offset) {
194     setImplicitTimeZone(offset.asZoneOffset());
195   }
196 
197   /**
198    * Get the current date and time.
199    *
200    * @return the current date and time
201    */
202   @NonNull
203   public ZonedDateTime getCurrentDateTime() {
204     return sharedState.currentDateTime;
205   }
206 
207   /**
208    * Get the mapping of loaded documents from the document URI to the document
209    * node.
210    *
211    * @return the map of document URIs to document nodes
212    */
213   @SuppressWarnings("null")
214   @NonNull
215   public Map<URI, IDocumentNodeItem> getAvailableDocuments() {
216     return Collections.unmodifiableMap(sharedState.availableDocuments);
217   }
218 
219   /**
220    * Get the document loader assigned to this dynamic context.
221    *
222    * @return the loader
223    * @throws ContextAbsentDynamicMetapathException
224    *           if a document loader is not configured for this dynamic context
225    */
226   @NonNull
227   public IDocumentLoader getDocumentLoader() {
228     IDocumentLoader retval = sharedState.documentLoader;
229     if (retval == null) {
230       throw new UnsupportedOperationException(
231           "No document loader configured for the dynamic context. Use setDocumentLoader(loader) to confgure one.");
232     }
233     return retval;
234   }
235 
236   /**
237    * Assign a document loader to this dynamic context.
238    *
239    * @param documentLoader
240    *          the document loader to assign
241    */
242   public void setDocumentLoader(@NonNull IDocumentLoader documentLoader) {
243     this.sharedState.documentLoader = new CachingLoader(documentLoader);
244   }
245 
246   /**
247    * Get the cached function call result for evaluating a function that has the
248    * property {@link FunctionProperty#DETERMINISTIC}.
249    *
250    * @param callingContext
251    *          the function calling context information that distinguishes the call
252    *          from any other call
253    * @return the cached result sequence for the function call
254    */
255   @Nullable
256   public ISequence<?> getCachedResult(@NonNull CalledContext callingContext) {
257     return sharedState.functionResultCache.get(callingContext);
258   }
259 
260   /**
261    * Cache a function call result for a that has the property
262    * {@link FunctionProperty#DETERMINISTIC}.
263    *
264    * @param callingContext
265    *          the calling context information that distinguishes the call from any
266    *          other call
267    * @param result
268    *          the function call result
269    */
270   public void cacheResult(@NonNull CalledContext callingContext, @NonNull ISequence<?> result) {
271     ISequence<?> old = sharedState.functionResultCache.put(callingContext, result);
272     assert old == null;
273   }
274 
275   /**
276    * Used to disable the evaluation of predicate expressions during Metapath
277    * evaluation.
278    * <p>
279    * This can be useful for determining the potential targets identified by a
280    * Metapath expression as a partial evaluation, without evaluating that these
281    * targets match the predicate.
282    *
283    * @return this dynamic context
284    */
285   @NonNull
286   public DynamicContext disablePredicateEvaluation() {
287     this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
288     return this;
289   }
290 
291   /**
292    * Used to enable the evaluation of predicate expressions during Metapath
293    * evaluation.
294    * <p>
295    * This is the default behavior if unchanged.
296    *
297    * @return this dynamic context
298    */
299   @NonNull
300   public DynamicContext enablePredicateEvaluation() {
301     this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
302     return this;
303   }
304 
305   /**
306    * Get the Metapath evaluation configuration.
307    *
308    * @return the configuration
309    */
310   @NonNull
311   public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
312     return sharedState.configuration;
313   }
314 
315   /**
316    * Get the sequence value assigned to a let variable with the provided qualified
317    * name.
318    *
319    * @param name
320    *          the variable qualified name
321    * @return the non-null variable value
322    * @throws DynamicMetapathException
323    *           of the variable has not been assigned or if the variable value is
324    *           {@code null}
325    */
326   @NonNull
327   public ISequence<?> getVariableValue(@NonNull IEnhancedQName name) {
328     ISequence<?> retval = letVariableMap.get(name.getIndexPosition());
329     if (retval == null) {
330       throw new StaticMetapathException(
331           StaticMetapathException.NOT_DEFINED,
332           String.format("Variable '%s' not defined in the dynamic context.", name))
333               .registerEvaluationContext(this);
334     }
335     return retval;
336   }
337 
338   /**
339    * Get the function with the provided name and arity.
340    *
341    * @param name
342    *          the requested function's qualified name
343    * @param arity
344    *          the number of arguments in the requested function
345    * @return the function
346    * @throws StaticMetapathException
347    *           with the code {@link StaticMetapathException#NO_FUNCTION_MATCH} if
348    *           a matching function was not found
349    */
350   @NonNull
351   public IFunction lookupFunction(@NonNull IEnhancedQName name, int arity) {
352     return getStaticContext().lookupFunction(name, arity);
353   }
354 
355   /**
356    * Bind the variable {@code name} to the sequence {@code value}.
357    *
358    * @param name
359    *          the name of the variable to bind
360    * @param boundValue
361    *          the value to bind to the variable
362    * @return this dynamic context
363    */
364   @NonNull
365   public DynamicContext bindVariableValue(@NonNull IEnhancedQName name, @NonNull ISequence<?> boundValue) {
366     letVariableMap.put(name.getIndexPosition(), boundValue);
367     return this;
368   }
369 
370   /**
371    * Push the current expression under evaluation to the execution queue.
372    *
373    * @param expression
374    *          the expression to push
375    */
376   public void pushExecutionStack(@NonNull IExpression expression) {
377     this.sharedState.executionStack.push(expression);
378   }
379 
380   /**
381    * Pop the expression that was under evaluation from the execution queue.
382    *
383    * @param expression
384    *          the expected expression to be popped
385    */
386   public void popExecutionStack(@NonNull IExpression expression) {
387     IExpression popped = this.sharedState.executionStack.pop();
388     if (!expression.equals(popped)) {
389       throw new IllegalStateException("Popped expression does not match expected expression");
390     }
391   }
392 
393   /**
394    * Return a copy of the current execution stack.
395    *
396    * @return the execution stack
397    */
398   @NonNull
399   public Deque<IExpression> getExecutionStack() {
400     return new ArrayDeque<>(this.sharedState.executionStack);
401   }
402 
403   /**
404    * Provides a formatted stack trace.
405    *
406    * @return the formatted stack trace
407    */
408   @NonNull
409   public String formatExecutionStackTrace() {
410     return ObjectUtils.notNull(getExecutionStack().stream()
411         .map(IExpression::toCSTString)
412         .collect(Collectors.joining("\n-> ")));
413   }
414 
415   private class CachingLoader implements IDocumentLoader {
416     @NonNull
417     private final IDocumentLoader proxy;
418 
419     public CachingLoader(@NonNull IDocumentLoader proxy) {
420       this.proxy = proxy;
421     }
422 
423     @Override
424     public IUriResolver getUriResolver() {
425       return new ContextUriResolver();
426     }
427 
428     @Override
429     public void setUriResolver(@NonNull IUriResolver resolver) {
430       // we delegate to the document loader proxy, so the resolver should be set there
431       throw new UnsupportedOperationException("Set the resolver on the proxy");
432     }
433 
434     @NonNull
435     protected IDocumentLoader getProxiedDocumentLoader() {
436       return proxy;
437     }
438 
439     @Override
440     public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
441       IDocumentNodeItem retval = sharedState.availableDocuments.get(uri);
442       if (retval == null) {
443         retval = getProxiedDocumentLoader().loadAsNodeItem(uri);
444         sharedState.availableDocuments.put(uri, retval);
445       }
446       return retval;
447     }
448 
449     public class ContextUriResolver implements IUriResolver {
450 
451       /**
452        * {@inheritDoc}
453        * <p>
454        * This method first resolves the provided URI against the static context's base
455        * URI.
456        */
457       @Override
458       public URI resolve(URI uri) {
459         URI baseUri = getStaticContext().getBaseUri();
460 
461         URI resolvedUri;
462         if (baseUri == null) {
463           resolvedUri = uri;
464         } else {
465           resolvedUri = ObjectUtils.notNull(baseUri.resolve(uri));
466         }
467 
468         IUriResolver resolver = getProxiedDocumentLoader().getUriResolver();
469         return resolver == null ? resolvedUri : resolver.resolve(resolvedUri);
470       }
471     }
472   }
473 }