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