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.DefaultFunction.CallingContext;
14  import gov.nist.secauto.metaschema.core.metapath.function.IFunction.FunctionProperty;
15  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
16  import gov.nist.secauto.metaschema.core.model.IUriResolver;
17  import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  
20  import java.io.IOException;
21  import java.net.URI;
22  import java.time.Clock;
23  import java.time.ZoneId;
24  import java.time.ZonedDateTime;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.TimeUnit;
30  
31  import edu.umd.cs.findbugs.annotations.NonNull;
32  import edu.umd.cs.findbugs.annotations.Nullable;
33  
34  // TODO: add support for in-scope namespaces
35  /**
36   * The implementation of a Metapath
37   * <a href="https://www.w3.org/TR/xpath-31/#eval_context">dynamic context</a>.
38   */
39  public class DynamicContext { // NOPMD - intentional data class
40    @NonNull
41    private final Map<Integer, ISequence<?>> letVariableMap;
42    @NonNull
43    private final SharedState sharedState;
44  
45    /**
46     * Construct a new dynamic context with a default static context.
47     */
48    public DynamicContext() {
49      this(StaticContext.instance());
50    }
51  
52    /**
53     * Construct a new Metapath dynamic context using the provided static context.
54     *
55     * @param staticContext
56     *          the Metapath static context
57     */
58    public DynamicContext(@NonNull StaticContext staticContext) {
59      this.letVariableMap = new ConcurrentHashMap<>();
60      this.sharedState = new SharedState(staticContext);
61    }
62  
63    private DynamicContext(@NonNull DynamicContext context) {
64      this.letVariableMap = new ConcurrentHashMap<>(context.letVariableMap);
65      this.sharedState = context.sharedState;
66    }
67  
68    private static class SharedState {
69      @NonNull
70      private final StaticContext staticContext;
71      @NonNull
72      private final ZoneId implicitTimeZone;
73      @NonNull
74      private final ZonedDateTime currentDateTime;
75      @NonNull
76      private final Map<URI, IDocumentNodeItem> availableDocuments;
77      @NonNull
78      private final Map<CallingContext, ISequence<?>> functionResultCache;
79      @Nullable
80      private CachingLoader documentLoader;
81      @NonNull
82      private final IMutableConfiguration<MetapathEvaluationFeature<?>> configuration;
83  
84      public SharedState(@NonNull StaticContext staticContext) {
85        this.staticContext = staticContext;
86  
87        Clock clock = Clock.systemDefaultZone();
88  
89        this.implicitTimeZone = ObjectUtils.notNull(clock.getZone());
90        this.currentDateTime = ObjectUtils.notNull(ZonedDateTime.now(clock));
91        this.availableDocuments = new HashMap<>();
92        this.functionResultCache = ObjectUtils.notNull(Caffeine.newBuilder()
93            .maximumSize(5000)
94            .expireAfterAccess(10, TimeUnit.MINUTES)
95            .<CallingContext, ISequence<?>>build().asMap());
96        this.configuration = new DefaultConfiguration<>();
97        this.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
98      }
99    }
100 
101   /**
102    * Generate a new dynamic context that is a copy of this dynamic context.
103    * <p>
104    * This method can be used to create a new sub-context where changes can be made
105    * without affecting this context. This is useful for setting information that
106    * is only used in a limited evaluation sub-scope, such as for handling variable
107    * assignment.
108    *
109    * @return a new dynamic context
110    */
111   @NonNull
112   public DynamicContext subContext() {
113     return new DynamicContext(this);
114   }
115 
116   /**
117    * Get the static context associated with this dynamic context.
118    *
119    * @return the associated static context
120    */
121   @NonNull
122   public StaticContext getStaticContext() {
123     return sharedState.staticContext;
124   }
125 
126   /**
127    * Get the default time zone used for evaluation.
128    *
129    * @return the time zone identifier object
130    */
131   @NonNull
132   public ZoneId getImplicitTimeZone() {
133     return sharedState.implicitTimeZone;
134   }
135 
136   /**
137    * Get the current date and time.
138    *
139    * @return the current date and time
140    */
141   @NonNull
142   public ZonedDateTime getCurrentDateTime() {
143     return sharedState.currentDateTime;
144   }
145 
146   /**
147    * Get the mapping of loaded documents from the document URI to the document
148    * node.
149    *
150    * @return the map of document URIs to document nodes
151    */
152   @SuppressWarnings("null")
153   @NonNull
154   public Map<URI, IDocumentNodeItem> getAvailableDocuments() {
155     return Collections.unmodifiableMap(sharedState.availableDocuments);
156   }
157 
158   /**
159    * Get the document loader assigned to this dynamic context.
160    *
161    * @return the loader
162    * @throws DynamicMetapathException
163    *           with an error code
164    *           {@link DynamicMetapathException#DYNAMIC_CONTEXT_ABSENT} if a
165    *           document loader is not configured for this dynamic context
166    */
167   @NonNull
168   public IDocumentLoader getDocumentLoader() {
169     IDocumentLoader retval = sharedState.documentLoader;
170     if (retval == null) {
171       throw new DynamicMetapathException(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT,
172           "No document loader configured for the dynamic context.");
173     }
174     return retval;
175   }
176 
177   /**
178    * Assign a document loader to this dynamic context.
179    *
180    * @param documentLoader
181    *          the document loader to assign
182    */
183   public void setDocumentLoader(@NonNull IDocumentLoader documentLoader) {
184     this.sharedState.documentLoader = new CachingLoader(documentLoader);
185   }
186 
187   /**
188    * Get the cached function call result for evaluating a function that has the
189    * property {@link FunctionProperty#DETERMINISTIC}.
190    *
191    * @param callingContext
192    *          the function calling context information that distinguishes the call
193    *          from any other call
194    * @return the cached result sequence for the function call
195    */
196   @Nullable
197   public ISequence<?> getCachedResult(@NonNull CallingContext callingContext) {
198     return sharedState.functionResultCache.get(callingContext);
199   }
200 
201   /**
202    * Cache a function call result for a that has the property
203    * {@link FunctionProperty#DETERMINISTIC}.
204    *
205    * @param callingContext
206    *          the calling context information that distinguishes the call from any
207    *          other call
208    * @param result
209    *          the function call result
210    */
211   public void cacheResult(@NonNull CallingContext callingContext, @NonNull ISequence<?> result) {
212     ISequence<?> old = sharedState.functionResultCache.put(callingContext, result);
213     assert old == null;
214   }
215 
216   /**
217    * Used to disable the evaluation of predicate expressions during Metapath
218    * evaluation.
219    * <p>
220    * This can be useful for determining the potential targets identified by a
221    * Metapath expression as a partial evaluation, without evaluating that these
222    * targets match the predicate.
223    *
224    * @return this dynamic context
225    */
226   @NonNull
227   public DynamicContext disablePredicateEvaluation() {
228     this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
229     return this;
230   }
231 
232   /**
233    * Used to enable the evaluation of predicate expressions during Metapath
234    * evaluation.
235    * <p>
236    * This is the default behavior if unchanged.
237    *
238    * @return this dynamic context
239    */
240   @NonNull
241   public DynamicContext enablePredicateEvaluation() {
242     this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
243     return this;
244   }
245 
246   /**
247    * Get the Metapath evaluation configuration.
248    *
249    * @return the configuration
250    */
251   @NonNull
252   public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
253     return sharedState.configuration;
254   }
255 
256   /**
257    * Get the sequence value assigned to a let variable with the provided qualified
258    * name.
259    *
260    * @param name
261    *          the variable qualified name
262    * @return the non-null variable value
263    * @throws MetapathException
264    *           of the variable has not been assigned or if the variable value is
265    *           {@code null}
266    */
267   @NonNull
268   public ISequence<?> getVariableValue(@NonNull IEnhancedQName name) {
269     ISequence<?> retval = letVariableMap.get(name.getIndexPosition());
270     if (retval == null) {
271       if (letVariableMap.containsKey(name.getIndexPosition())) {
272         throw new MetapathException(String.format("Variable '%s' has null contents.", name));
273       }
274       throw new StaticMetapathException(
275           StaticMetapathException.NOT_DEFINED,
276           String.format("Variable '%s' not defined in the dynamic context.", name));
277     }
278     return retval;
279   }
280 
281   /**
282    * Bind the variable {@code name} to the sequence {@code value}.
283    *
284    * @param name
285    *          the name of the variable to bind
286    * @param boundValue
287    *          the value to bind to the variable
288    * @return this dynamic context
289    */
290   @NonNull
291   public DynamicContext bindVariableValue(@NonNull IEnhancedQName name, @NonNull ISequence<?> boundValue) {
292     letVariableMap.put(name.getIndexPosition(), boundValue);
293     return this;
294   }
295 
296   private class CachingLoader implements IDocumentLoader {
297     @NonNull
298     private final IDocumentLoader proxy;
299 
300     public CachingLoader(@NonNull IDocumentLoader proxy) {
301       this.proxy = proxy;
302     }
303 
304     @Override
305     public IUriResolver getUriResolver() {
306       return new ContextUriResolver();
307     }
308 
309     @Override
310     public void setUriResolver(@NonNull IUriResolver resolver) {
311       // we delegate to the document loader proxy, so the resolver should be set there
312       throw new UnsupportedOperationException("Set the resolver on the proxy");
313     }
314 
315     @NonNull
316     protected IDocumentLoader getProxiedDocumentLoader() {
317       return proxy;
318     }
319 
320     @Override
321     public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
322       IDocumentNodeItem retval = sharedState.availableDocuments.get(uri);
323       if (retval == null) {
324         retval = getProxiedDocumentLoader().loadAsNodeItem(uri);
325         sharedState.availableDocuments.put(uri, retval);
326       }
327       return retval;
328     }
329 
330     public class ContextUriResolver implements IUriResolver {
331 
332       /**
333        * {@inheritDoc}
334        * <p>
335        * This method first resolves the provided URI against the static context's base
336        * URI.
337        */
338       @Override
339       public URI resolve(URI uri) {
340         URI baseUri = getStaticContext().getBaseUri();
341 
342         URI resolvedUri;
343         if (baseUri == null) {
344           resolvedUri = uri;
345         } else {
346           resolvedUri = ObjectUtils.notNull(baseUri.resolve(uri));
347         }
348 
349         IUriResolver resolver = getProxiedDocumentLoader().getUriResolver();
350         return resolver == null ? resolvedUri : resolver.resolve(resolvedUri);
351       }
352     }
353   }
354 
355 }