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