001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.metapath;
007
008import java.io.IOException;
009import java.io.UncheckedIOException;
010import java.net.URI;
011import java.time.Clock;
012import java.time.Duration;
013import java.time.LocalDateTime;
014import java.time.ZoneId;
015import java.time.ZoneOffset;
016import java.time.ZonedDateTime;
017import java.util.ArrayDeque;
018import java.util.Collections;
019import java.util.Deque;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.stream.Collectors;
023
024import dev.metaschema.core.configuration.DefaultConfiguration;
025import dev.metaschema.core.configuration.IConfiguration;
026import dev.metaschema.core.configuration.IMutableConfiguration;
027import dev.metaschema.core.metapath.function.CalledContext;
028import dev.metaschema.core.metapath.function.DateTimeFunctionException;
029import dev.metaschema.core.metapath.function.IFunction;
030import dev.metaschema.core.metapath.function.IFunction.FunctionProperty;
031import dev.metaschema.core.metapath.item.ISequence;
032import dev.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
033import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
034import dev.metaschema.core.model.IUriResolver;
035import dev.metaschema.core.qname.IEnhancedQName;
036import dev.metaschema.core.util.ObjectUtils;
037import edu.umd.cs.findbugs.annotations.NonNull;
038import edu.umd.cs.findbugs.annotations.Nullable;
039
040// TODO: add support for in-scope namespaces
041/**
042 * The implementation of a Metapath
043 * <a href="https://www.w3.org/TR/xpath-31/#eval_context">dynamic context</a>.
044 */
045public class DynamicContext {
046
047  @NonNull
048  private final Map<Integer, ISequence<?>> letVariableMap;
049  @NonNull
050  private final SharedState sharedState;
051  @Nullable
052  private final FocusContext focusContext;
053  @NonNull
054  private final Deque<IExpression> executionStack;
055
056  /**
057   * Construct a new dynamic context with a default static context.
058   */
059  public DynamicContext() {
060    this(StaticContext.instance());
061  }
062
063  /**
064   * Construct a new Metapath dynamic context using the provided static context.
065   *
066   * @param staticContext
067   *          the Metapath static context
068   */
069  public DynamicContext(@NonNull StaticContext staticContext) {
070    this.letVariableMap = new ConcurrentHashMap<>();
071    this.sharedState = new SharedState(staticContext);
072    this.focusContext = null;
073    this.executionStack = new ArrayDeque<>();
074  }
075
076  private DynamicContext(@NonNull DynamicContext context) {
077    this(context, context.focusContext);
078  }
079
080  private DynamicContext(@NonNull DynamicContext context, @Nullable FocusContext focusContext) {
081    this.letVariableMap = new ConcurrentHashMap<>(context.letVariableMap);
082    this.sharedState = context.sharedState;
083    this.focusContext = focusContext;
084    // Copy parent's stack so error traces show full call chain
085    this.executionStack = new ArrayDeque<>(context.executionStack);
086  }
087
088  private static class SharedState {
089    @NonNull
090    private final StaticContext staticContext;
091    @NonNull
092    private final ZonedDateTime currentDateTime;
093    @NonNull
094    private final Map<URI, IDocumentNodeItem> availableDocuments;
095    @NonNull
096    private final Map<CalledContext, ISequence<?>> functionResultCache;
097    @Nullable
098    private CachingLoader documentLoader;
099    @NonNull
100    private final IMutableConfiguration<MetapathEvaluationFeature<?>> configuration;
101    @NonNull
102    private ZoneId implicitTimeZone;
103
104    public SharedState(@NonNull StaticContext staticContext) {
105      this.staticContext = staticContext;
106
107      Clock clock = Clock.systemDefaultZone();
108
109      this.implicitTimeZone = ObjectUtils.notNull(clock.getZone());
110
111      this.currentDateTime = ObjectUtils.notNull(ZonedDateTime.now(clock));
112      this.availableDocuments = new ConcurrentHashMap<>();
113      this.functionResultCache = new ConcurrentHashMap<>();
114      this.configuration = new DefaultConfiguration<>();
115      this.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
116    }
117  }
118
119  /**
120   * Generate a new dynamic context that is a copy of this dynamic context.
121   * <p>
122   * This method can be used to create a new sub-context where changes can be made
123   * without affecting this context. This is useful for setting information that
124   * is only used in a limited evaluation sub-scope, such as for handling variable
125   * assignment.
126   * <p>
127   * The focus context from this context is preserved in the new sub-context,
128   * allowing nested expressions to access the enclosing focus (e.g., for
129   * {@code position()} and {@code last()} calls within variable binding scopes).
130   *
131   * @return a new dynamic context
132   */
133  @NonNull
134  public DynamicContext subContext() {
135    return new DynamicContext(this);
136  }
137
138  /**
139   * Generate a new dynamic context with the specified focus context.
140   * <p>
141   * This method is used by predicate expressions to establish a new focus for
142   * evaluating predicates. The focus context provides the information needed by
143   * {@code fn:position()} and {@code fn:last()}.
144   *
145   * @param focusContext
146   *          the focus context for the new sub-context
147   * @return a new dynamic context with the specified focus context
148   */
149  @NonNull
150  public DynamicContext subContext(@NonNull FocusContext focusContext) {
151    return new DynamicContext(this, focusContext);
152  }
153
154  /**
155   * Get the focus context for this dynamic context.
156   * <p>
157   * The focus context contains the context item, position, and size as defined in
158   * the <a href="https://www.w3.org/TR/xpath-31/#eval_context">XPath 3.1
159   * evaluation context</a>.
160   *
161   * @return the focus context, or {@code null} if no focus context is established
162   */
163  @Nullable
164  public FocusContext getFocusContext() {
165    return focusContext;
166  }
167
168  /**
169   * Get the static context associated with this dynamic context.
170   *
171   * @return the associated static context
172   */
173  @NonNull
174  public StaticContext getStaticContext() {
175    return sharedState.staticContext;
176  }
177
178  /**
179   * Get the default time zone used for evaluation.
180   *
181   * @return the time zone identifier object
182   */
183  @NonNull
184  public ZoneId getImplicitTimeZone() {
185    return sharedState.implicitTimeZone;
186  }
187
188  /**
189   * Get the default time zone used for evaluation.
190   *
191   * @return the time zone identifier object
192   */
193  @NonNull
194  public IDayTimeDurationItem getImplicitTimeZoneAsDayTimeDuration() {
195    LocalDateTime referenceDateTime = MetapathConstants.REFERENCE_DATE_TIME.asLocalDateTime();
196    ZonedDateTime reference = referenceDateTime.atZone(getImplicitTimeZone());
197    ZonedDateTime referenceZ = referenceDateTime.atZone(ZoneOffset.UTC);
198
199    return IDayTimeDurationItem.valueOf(ObjectUtils.notNull(
200        Duration.between(
201            reference,
202            referenceZ)));
203  }
204
205  /**
206   * Set the implicit timezone to the provided value.
207   * <p>
208   * Note: This value should only be adjusted when the context is first created.
209   * Once the context is used, this value is expected to be stable.
210   *
211   * @param timezone
212   *          the timezone to use
213   */
214  public void setImplicitTimeZone(@NonNull ZoneId timezone) {
215    sharedState.implicitTimeZone = timezone;
216  }
217
218  /**
219   * Set the implicit timezone to the provided value.
220   * <p>
221   * Note: This value should only be adjusted when the context is first created.
222   * Once the context is used, this value is expected to be stable.
223   *
224   * @param offset
225   *          the offset which must be &gt;= -PT14H and &lt;= PT13H
226   * @throws DateTimeFunctionException
227   *           with the code
228   *           {@link DateTimeFunctionException#INVALID_TIME_ZONE_VALUE_ERROR} if
229   *           the offset is &lt; -PT14H or &gt; PT14H
230   */
231  public void setImplicitTimeZone(@NonNull IDayTimeDurationItem offset) {
232    setImplicitTimeZone(offset.asZoneOffset());
233  }
234
235  /**
236   * Get the current date and time.
237   *
238   * @return the current date and time
239   */
240  @NonNull
241  public ZonedDateTime getCurrentDateTime() {
242    return sharedState.currentDateTime;
243  }
244
245  /**
246   * Get the mapping of loaded documents from the document URI to the document
247   * node.
248   *
249   * @return the map of document URIs to document nodes
250   */
251  @SuppressWarnings("null")
252  @NonNull
253  public Map<URI, IDocumentNodeItem> getAvailableDocuments() {
254    return Collections.unmodifiableMap(sharedState.availableDocuments);
255  }
256
257  /**
258   * Get the document loader assigned to this dynamic context.
259   *
260   * @return the loader
261   * @throws ContextAbsentDynamicMetapathException
262   *           if a document loader is not configured for this dynamic context
263   */
264  @NonNull
265  public IDocumentLoader getDocumentLoader() {
266    IDocumentLoader retval = sharedState.documentLoader;
267    if (retval == null) {
268      throw new UnsupportedOperationException(
269          "No document loader configured for the dynamic context. Use setDocumentLoader(loader) to confgure one.");
270    }
271    return retval;
272  }
273
274  /**
275   * Assign a document loader to this dynamic context.
276   *
277   * @param documentLoader
278   *          the document loader to assign
279   */
280  public void setDocumentLoader(@NonNull IDocumentLoader documentLoader) {
281    this.sharedState.documentLoader = new CachingLoader(documentLoader);
282  }
283
284  /**
285   * Get the cached function call result for evaluating a function that has the
286   * property {@link FunctionProperty#DETERMINISTIC}.
287   *
288   * @param callingContext
289   *          the function calling context information that distinguishes the call
290   *          from any other call
291   * @return the cached result sequence for the function call
292   */
293  @Nullable
294  public ISequence<?> getCachedResult(@NonNull CalledContext callingContext) {
295    return sharedState.functionResultCache.get(callingContext);
296  }
297
298  /**
299   * Cache a function call result for a that has the property
300   * {@link FunctionProperty#DETERMINISTIC}.
301   *
302   * @param callingContext
303   *          the calling context information that distinguishes the call from any
304   *          other call
305   * @param result
306   *          the function call result
307   */
308  public void cacheResult(@NonNull CalledContext callingContext, @NonNull ISequence<?> result) {
309    ISequence<?> old = sharedState.functionResultCache.put(callingContext, result);
310    assert old == null;
311  }
312
313  /**
314   * Used to disable the evaluation of predicate expressions during Metapath
315   * evaluation.
316   * <p>
317   * This can be useful for determining the potential targets identified by a
318   * Metapath expression as a partial evaluation, without evaluating that these
319   * targets match the predicate.
320   *
321   * @return this dynamic context
322   */
323  @NonNull
324  public DynamicContext disablePredicateEvaluation() {
325    this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
326    return this;
327  }
328
329  /**
330   * Used to enable the evaluation of predicate expressions during Metapath
331   * evaluation.
332   * <p>
333   * This is the default behavior if unchanged.
334   *
335   * @return this dynamic context
336   */
337  @NonNull
338  public DynamicContext enablePredicateEvaluation() {
339    this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
340    return this;
341  }
342
343  /**
344   * Get the Metapath evaluation configuration.
345   *
346   * @return the configuration
347   */
348  @NonNull
349  public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
350    return sharedState.configuration;
351  }
352
353  /**
354   * Get the sequence value assigned to a let variable with the provided qualified
355   * name.
356   *
357   * @param name
358   *          the variable qualified name
359   * @return the non-null variable value
360   * @throws DynamicMetapathException
361   *           of the variable has not been assigned or if the variable value is
362   *           {@code null}
363   */
364  @NonNull
365  public ISequence<?> getVariableValue(@NonNull IEnhancedQName name) {
366    ISequence<?> retval = letVariableMap.get(name.getIndexPosition());
367    if (retval == null) {
368      throw new StaticMetapathException(
369          StaticMetapathException.NOT_DEFINED,
370          String.format("Variable '%s' not defined in the dynamic context.", name))
371              .registerEvaluationContext(this);
372    }
373    return retval;
374  }
375
376  /**
377   * Get the function with the provided name and arity.
378   *
379   * @param name
380   *          the requested function's qualified name
381   * @param arity
382   *          the number of arguments in the requested function
383   * @return the function
384   * @throws StaticMetapathException
385   *           with the code {@link StaticMetapathException#NO_FUNCTION_MATCH} if
386   *           a matching function was not found
387   */
388  @NonNull
389  public IFunction lookupFunction(@NonNull IEnhancedQName name, int arity) {
390    return getStaticContext().lookupFunction(name, arity);
391  }
392
393  /**
394   * Bind the variable {@code name} to the sequence {@code value}.
395   *
396   * @param name
397   *          the name of the variable to bind
398   * @param boundValue
399   *          the value to bind to the variable
400   * @return this dynamic context
401   */
402  @NonNull
403  public DynamicContext bindVariableValue(@NonNull IEnhancedQName name, @NonNull ISequence<?> boundValue) {
404    letVariableMap.put(name.getIndexPosition(), boundValue);
405    return this;
406  }
407
408  /**
409   * Push the current expression under evaluation to the execution queue.
410   *
411   * @param expression
412   *          the expression to push
413   */
414  public void pushExecutionStack(@NonNull IExpression expression) {
415    this.executionStack.push(expression);
416  }
417
418  /**
419   * Pop the expression that was under evaluation from the execution queue.
420   *
421   * @param expression
422   *          the expected expression to be popped
423   */
424  public void popExecutionStack(@NonNull IExpression expression) {
425    IExpression popped = this.executionStack.pop();
426    if (!expression.equals(popped)) {
427      throw new IllegalStateException("Popped expression does not match expected expression");
428    }
429  }
430
431  /**
432   * Return a copy of the current execution stack.
433   *
434   * @return the execution stack
435   */
436  @NonNull
437  public Deque<IExpression> getExecutionStack() {
438    return new ArrayDeque<>(this.executionStack);
439  }
440
441  /**
442   * Provides a formatted stack trace.
443   *
444   * @return the formatted stack trace
445   */
446  @NonNull
447  public String formatExecutionStackTrace() {
448    return ObjectUtils.notNull(getExecutionStack().stream()
449        .map(IExpression::toCSTString)
450        .collect(Collectors.joining("\n-> ")));
451  }
452
453  private class CachingLoader implements IDocumentLoader {
454    @NonNull
455    private final IDocumentLoader proxy;
456
457    public CachingLoader(@NonNull IDocumentLoader proxy) {
458      this.proxy = proxy;
459    }
460
461    @Override
462    public IUriResolver getUriResolver() {
463      return new ContextUriResolver();
464    }
465
466    @Override
467    public void setUriResolver(@NonNull IUriResolver resolver) {
468      // we delegate to the document loader proxy, so the resolver should be set there
469      throw new UnsupportedOperationException("Set the resolver on the proxy");
470    }
471
472    @NonNull
473    protected IDocumentLoader getProxiedDocumentLoader() {
474      return proxy;
475    }
476
477    @Override
478    public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
479      URI normalizedUri = uri.normalize();
480      try {
481        return sharedState.availableDocuments.computeIfAbsent(normalizedUri, key -> {
482          try {
483            return getProxiedDocumentLoader().loadAsNodeItem(key);
484          } catch (IOException e) {
485            throw new UncheckedIOException(e);
486          }
487        });
488      } catch (UncheckedIOException e) {
489        throw e.getCause();
490      }
491    }
492
493    public class ContextUriResolver implements IUriResolver {
494
495      /**
496       * {@inheritDoc}
497       * <p>
498       * This method first resolves the provided URI against the static context's base
499       * URI.
500       */
501      @Override
502      public URI resolve(URI uri) {
503        URI baseUri = getStaticContext().getBaseUri();
504
505        URI resolvedUri;
506        if (baseUri == null) {
507          resolvedUri = uri;
508        } else {
509          resolvedUri = ObjectUtils.notNull(baseUri.resolve(uri));
510        }
511
512        IUriResolver resolver = getProxiedDocumentLoader().getUriResolver();
513        return resolver == null ? resolvedUri : resolver.resolve(resolvedUri);
514      }
515    }
516  }
517}