1
2
3
4
5
6 package dev.metaschema.core.metapath;
7
8 import java.io.IOException;
9 import java.io.UncheckedIOException;
10 import java.net.URI;
11 import java.time.Clock;
12 import java.time.Duration;
13 import java.time.LocalDateTime;
14 import java.time.ZoneId;
15 import java.time.ZoneOffset;
16 import java.time.ZonedDateTime;
17 import java.util.ArrayDeque;
18 import java.util.Collections;
19 import java.util.Deque;
20 import java.util.Map;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.stream.Collectors;
23
24 import dev.metaschema.core.configuration.DefaultConfiguration;
25 import dev.metaschema.core.configuration.IConfiguration;
26 import dev.metaschema.core.configuration.IMutableConfiguration;
27 import dev.metaschema.core.metapath.function.CalledContext;
28 import dev.metaschema.core.metapath.function.DateTimeFunctionException;
29 import dev.metaschema.core.metapath.function.IFunction;
30 import dev.metaschema.core.metapath.function.IFunction.FunctionProperty;
31 import dev.metaschema.core.metapath.item.ISequence;
32 import dev.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
33 import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
34 import dev.metaschema.core.model.IUriResolver;
35 import dev.metaschema.core.qname.IEnhancedQName;
36 import dev.metaschema.core.util.ObjectUtils;
37 import edu.umd.cs.findbugs.annotations.NonNull;
38 import edu.umd.cs.findbugs.annotations.Nullable;
39
40
41
42
43
44
45 public class DynamicContext {
46
47 @NonNull
48 private final Map<Integer, ISequence<?>> letVariableMap;
49 @NonNull
50 private final SharedState sharedState;
51 @Nullable
52 private final FocusContext focusContext;
53 @NonNull
54 private final Deque<IExpression> executionStack;
55
56
57
58
59 public DynamicContext() {
60 this(StaticContext.instance());
61 }
62
63
64
65
66
67
68
69 public DynamicContext(@NonNull StaticContext staticContext) {
70 this.letVariableMap = new ConcurrentHashMap<>();
71 this.sharedState = new SharedState(staticContext);
72 this.focusContext = null;
73 this.executionStack = new ArrayDeque<>();
74 }
75
76 private DynamicContext(@NonNull DynamicContext context) {
77 this(context, context.focusContext);
78 }
79
80 private DynamicContext(@NonNull DynamicContext context, @Nullable FocusContext focusContext) {
81 this.letVariableMap = new ConcurrentHashMap<>(context.letVariableMap);
82 this.sharedState = context.sharedState;
83 this.focusContext = focusContext;
84
85 this.executionStack = new ArrayDeque<>(context.executionStack);
86 }
87
88 private static class SharedState {
89 @NonNull
90 private final StaticContext staticContext;
91 @NonNull
92 private final ZonedDateTime currentDateTime;
93 @NonNull
94 private final Map<URI, IDocumentNodeItem> availableDocuments;
95 @NonNull
96 private final Map<CalledContext, ISequence<?>> functionResultCache;
97 @Nullable
98 private CachingLoader documentLoader;
99 @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
121
122
123
124
125
126
127
128
129
130
131
132
133 @NonNull
134 public DynamicContext subContext() {
135 return new DynamicContext(this);
136 }
137
138
139
140
141
142
143
144
145
146
147
148
149 @NonNull
150 public DynamicContext subContext(@NonNull FocusContext focusContext) {
151 return new DynamicContext(this, focusContext);
152 }
153
154
155
156
157
158
159
160
161
162
163 @Nullable
164 public FocusContext getFocusContext() {
165 return focusContext;
166 }
167
168
169
170
171
172
173 @NonNull
174 public StaticContext getStaticContext() {
175 return sharedState.staticContext;
176 }
177
178
179
180
181
182
183 @NonNull
184 public ZoneId getImplicitTimeZone() {
185 return sharedState.implicitTimeZone;
186 }
187
188
189
190
191
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
207
208
209
210
211
212
213
214 public void setImplicitTimeZone(@NonNull ZoneId timezone) {
215 sharedState.implicitTimeZone = timezone;
216 }
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231 public void setImplicitTimeZone(@NonNull IDayTimeDurationItem offset) {
232 setImplicitTimeZone(offset.asZoneOffset());
233 }
234
235
236
237
238
239
240 @NonNull
241 public ZonedDateTime getCurrentDateTime() {
242 return sharedState.currentDateTime;
243 }
244
245
246
247
248
249
250
251 @SuppressWarnings("null")
252 @NonNull
253 public Map<URI, IDocumentNodeItem> getAvailableDocuments() {
254 return Collections.unmodifiableMap(sharedState.availableDocuments);
255 }
256
257
258
259
260
261
262
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
276
277
278
279
280 public void setDocumentLoader(@NonNull IDocumentLoader documentLoader) {
281 this.sharedState.documentLoader = new CachingLoader(documentLoader);
282 }
283
284
285
286
287
288
289
290
291
292
293 @Nullable
294 public ISequence<?> getCachedResult(@NonNull CalledContext callingContext) {
295 return sharedState.functionResultCache.get(callingContext);
296 }
297
298
299
300
301
302
303
304
305
306
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
315
316
317
318
319
320
321
322
323 @NonNull
324 public DynamicContext disablePredicateEvaluation() {
325 this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
326 return this;
327 }
328
329
330
331
332
333
334
335
336
337 @NonNull
338 public DynamicContext enablePredicateEvaluation() {
339 this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
340 return this;
341 }
342
343
344
345
346
347
348 @NonNull
349 public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
350 return sharedState.configuration;
351 }
352
353
354
355
356
357
358
359
360
361
362
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
378
379
380
381
382
383
384
385
386
387
388 @NonNull
389 public IFunction lookupFunction(@NonNull IEnhancedQName name, int arity) {
390 return getStaticContext().lookupFunction(name, arity);
391 }
392
393
394
395
396
397
398
399
400
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
410
411
412
413
414 public void pushExecutionStack(@NonNull IExpression expression) {
415 this.executionStack.push(expression);
416 }
417
418
419
420
421
422
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
433
434
435
436 @NonNull
437 public Deque<IExpression> getExecutionStack() {
438 return new ArrayDeque<>(this.executionStack);
439 }
440
441
442
443
444
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
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
497
498
499
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 }