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