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
349
350
351
352
353
354
355 @NonNull
356 public DynamicContext enableAtomizeNoDataAsEmpty() {
357 this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_ATOMIZE_NO_DATA_AS_EMPTY);
358 return this;
359 }
360
361
362
363
364
365
366
367
368 @NonNull
369 public DynamicContext disableAtomizeNoDataAsEmpty() {
370 this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_ATOMIZE_NO_DATA_AS_EMPTY);
371 return this;
372 }
373
374
375
376
377
378
379 @NonNull
380 public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
381 return sharedState.configuration;
382 }
383
384
385
386
387
388
389
390
391
392
393
394
395 @NonNull
396 public ISequence<?> getVariableValue(@NonNull IEnhancedQName name) {
397 ISequence<?> retval = letVariableMap.get(name.getIndexPosition());
398 if (retval == null) {
399 throw new StaticMetapathException(
400 StaticMetapathException.NOT_DEFINED,
401 String.format("Variable '%s' not defined in the dynamic context.", name))
402 .registerEvaluationContext(this);
403 }
404 return retval;
405 }
406
407
408
409
410
411
412
413
414
415
416
417
418
419 @NonNull
420 public IFunction lookupFunction(@NonNull IEnhancedQName name, int arity) {
421 return getStaticContext().lookupFunction(name, arity);
422 }
423
424
425
426
427
428
429
430
431
432
433 @NonNull
434 public DynamicContext bindVariableValue(@NonNull IEnhancedQName name, @NonNull ISequence<?> boundValue) {
435 letVariableMap.put(name.getIndexPosition(), boundValue);
436 return this;
437 }
438
439
440
441
442
443
444
445 public void pushExecutionStack(@NonNull IExpression expression) {
446 this.executionStack.push(expression);
447 }
448
449
450
451
452
453
454
455 public void popExecutionStack(@NonNull IExpression expression) {
456 IExpression popped = this.executionStack.pop();
457 if (!expression.equals(popped)) {
458 throw new IllegalStateException("Popped expression does not match expected expression");
459 }
460 }
461
462
463
464
465
466
467 @NonNull
468 public Deque<IExpression> getExecutionStack() {
469 return new ArrayDeque<>(this.executionStack);
470 }
471
472
473
474
475
476
477 @NonNull
478 public String formatExecutionStackTrace() {
479 return ObjectUtils.notNull(getExecutionStack().stream()
480 .map(IExpression::toCSTString)
481 .collect(Collectors.joining("\n-> ")));
482 }
483
484 private class CachingLoader implements IDocumentLoader {
485 @NonNull
486 private final IDocumentLoader proxy;
487
488 public CachingLoader(@NonNull IDocumentLoader proxy) {
489 this.proxy = proxy;
490 }
491
492 @Override
493 public IUriResolver getUriResolver() {
494 return new ContextUriResolver();
495 }
496
497 @Override
498 public void setUriResolver(@NonNull IUriResolver resolver) {
499
500 throw new UnsupportedOperationException("Set the resolver on the proxy");
501 }
502
503 @NonNull
504 protected IDocumentLoader getProxiedDocumentLoader() {
505 return proxy;
506 }
507
508 @Override
509 public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
510 URI normalizedUri = uri.normalize();
511 try {
512 return sharedState.availableDocuments.computeIfAbsent(normalizedUri, key -> {
513 try {
514 return getProxiedDocumentLoader().loadAsNodeItem(key);
515 } catch (IOException e) {
516 throw new UncheckedIOException(e);
517 }
518 });
519 } catch (UncheckedIOException e) {
520 throw e.getCause();
521 }
522 }
523
524 public class ContextUriResolver implements IUriResolver {
525
526
527
528
529
530
531
532 @Override
533 public URI resolve(URI uri) {
534 URI baseUri = getStaticContext().getBaseUri();
535
536 URI resolvedUri;
537 if (baseUri == null) {
538 resolvedUri = uri;
539 } else {
540 resolvedUri = ObjectUtils.notNull(baseUri.resolve(uri));
541 }
542
543 IUriResolver resolver = getProxiedDocumentLoader().getUriResolver();
544 return resolver == null ? resolvedUri : resolver.resolve(resolvedUri);
545 }
546 }
547 }
548 }