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 >= -PT14H and <= PT13H 226 * @throws DateTimeFunctionException 227 * with the code 228 * {@link DateTimeFunctionException#INVALID_TIME_ZONE_VALUE_ERROR} if 229 * the offset is < -PT14H or > 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}