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.metapath.EQNameUtils.IEQNamePrefixResolver;
9   import gov.nist.secauto.metaschema.core.util.CollectionUtil;
10  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
11  
12  import java.net.URI;
13  import java.util.Map;
14  import java.util.concurrent.ConcurrentHashMap;
15  import java.util.stream.Collectors;
16  
17  import javax.xml.XMLConstants;
18  
19  import edu.umd.cs.findbugs.annotations.NonNull;
20  import edu.umd.cs.findbugs.annotations.Nullable;
21  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22  
23  // add support for default namespace
24  /**
25   * The implementation of a Metapath
26   * <a href="https://www.w3.org/TR/xpath-31/#static_context">static context</a>.
27   */
28  public final class StaticContext {
29    @NonNull
30    private static final Map<String, URI> WELL_KNOWN_NAMESPACES;
31    @NonNull
32    private static final Map<String, String> WELL_KNOWN_URI_TO_PREFIX;
33  
34    static {
35      Map<String, URI> knownNamespaces = new ConcurrentHashMap<>();
36      knownNamespaces.put(
37          MetapathConstants.PREFIX_METAPATH,
38          MetapathConstants.NS_METAPATH);
39      knownNamespaces.put(
40          MetapathConstants.PREFIX_XML_SCHEMA,
41          MetapathConstants.NS_XML_SCHEMA);
42      knownNamespaces.put(
43          MetapathConstants.PREFIX_XPATH_FUNCTIONS,
44          MetapathConstants.NS_METAPATH_FUNCTIONS);
45      knownNamespaces.put(
46          MetapathConstants.PREFIX_XPATH_FUNCTIONS_MATH,
47          MetapathConstants.NS_METAPATH_FUNCTIONS_MATH);
48      knownNamespaces.put(
49          MetapathConstants.PREFIX_XPATH_FUNCTIONS_ARRAY,
50          MetapathConstants.NS_METAPATH_FUNCTIONS_ARRAY);
51      knownNamespaces.put(
52          MetapathConstants.PREFIX_XPATH_FUNCTIONS_MAP,
53          MetapathConstants.NS_METAPATH_FUNCTIONS_MAP);
54      WELL_KNOWN_NAMESPACES = CollectionUtil.unmodifiableMap(knownNamespaces);
55  
56      WELL_KNOWN_URI_TO_PREFIX = ObjectUtils.notNull(WELL_KNOWN_NAMESPACES.entrySet().stream()
57          .collect(Collectors.toUnmodifiableMap(
58              entry -> entry.getValue().toASCIIString(),
59              Map.Entry::getKey,
60              (v1, v2) -> v2)));
61    }
62  
63    @Nullable
64    private final URI baseUri;
65    @NonNull
66    private final Map<String, URI> knownNamespaces;
67    @Nullable
68    private final URI defaultModelNamespace;
69    @Nullable
70    private final URI defaultFunctionNamespace;
71    private final boolean useWildcardWhenNamespaceNotDefaulted;
72  
73    /**
74     * Get the mapping of prefix to namespace URI for all well-known namespaces
75     * provided by default to the static context.
76     * <p>
77     * These namespaces can be overridden using the
78     * {@link Builder#namespace(String, URI)} method.
79     *
80     * @return the mapping of prefix to namespace URI for all well-known namespaces
81     */
82    @SuppressFBWarnings("MS_EXPOSE_REP")
83    public static Map<String, URI> getWellKnownNamespacesMap() {
84      return WELL_KNOWN_NAMESPACES;
85    }
86  
87    /**
88     * Get the mapping of namespace URIs to prefixes for all well-known namespaces
89     * provided by default to the static context.
90     *
91     * @return the mapping of namespace URI to prefix for all well-known namespaces
92     */
93    @SuppressFBWarnings("MS_EXPOSE_REP")
94    public static Map<String, String> getWellKnownURIToPrefixMap() {
95      return WELL_KNOWN_URI_TO_PREFIX;
96    }
97  
98    /**
99     * Get the namespace prefix associated with the provided URI, if the URI is
100    * well-known.
101    *
102    * @param uri
103    *          the URI to get the prefix for
104    * @return the prefix or {@code null} if the provided URI is not well-known
105    */
106   @Nullable
107   public static String getWellKnownPrefixForUri(@NonNull String uri) {
108     return WELL_KNOWN_URI_TO_PREFIX.get(uri);
109   }
110 
111   /**
112    * Create a new static context instance using default values.
113    *
114    * @return a new static context instance
115    */
116   @NonNull
117   public static StaticContext instance() {
118     return builder().build();
119   }
120 
121   private StaticContext(Builder builder) {
122     this.baseUri = builder.baseUri;
123     this.knownNamespaces = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Map.copyOf(builder.namespaces)));
124     this.defaultModelNamespace = builder.defaultModelNamespace;
125     this.defaultFunctionNamespace = builder.defaultFunctionNamespace;
126     this.useWildcardWhenNamespaceNotDefaulted = builder.useWildcardWhenNamespaceNotDefaulted;
127   }
128 
129   /**
130    * Get the static base URI to use in resolving URIs handled by the Metapath
131    * processor. This URI, if provided, will be used when a document base URI is
132    * not available.
133    *
134    * @return the base URI or {@code null} if not defined
135    */
136   @Nullable
137   public URI getBaseUri() {
138     synchronized (this) {
139       return baseUri;
140     }
141   }
142 
143   /**
144    * Get the namespace URI associated with the provided {@code prefix}, if any is
145    * bound.
146    * <p>
147    * This method uses the namespaces set by the
148    * {@link Builder#namespace(String, URI)} method, falling back to the well-known
149    * namespace bindings when a prefix match is not found.
150    * <p>
151    * The well-known namespace bindings can be retrieved using the
152    * {@link StaticContext#getWellKnownNamespacesMap()} method.
153    *
154    * @param prefix
155    *          the namespace prefix
156    * @return the namespace URI bound to the prefix, or {@code null} if no
157    *         namespace is bound to the prefix
158    * @see Builder#namespace(String, URI)
159    * @see #getWellKnownNamespacesMap()
160    */
161   @Nullable
162   public URI lookupNamespaceURIForPrefix(@NonNull String prefix) {
163     URI retval = knownNamespaces.get(prefix);
164     if (retval == null) {
165       // fall back to well-known namespaces
166       retval = WELL_KNOWN_NAMESPACES.get(prefix);
167     }
168     return retval;
169   }
170 
171   /**
172    * Get the namespace associated with the provided {@code prefix} as a string, if
173    * any is bound.
174    *
175    * @param prefix
176    *          the namespace prefix
177    * @return the namespace string bound to the prefix, or {@code null} if no
178    *         namespace is bound to the prefix
179    */
180   @Nullable
181   public String lookupNamespaceForPrefix(@NonNull String prefix) {
182     URI result = lookupNamespaceURIForPrefix(prefix);
183     return result == null ? null : result.toASCIIString();
184   }
185 
186   /**
187    * Get the default namespace for assembly, field, or flag references that have
188    * no namespace prefix.
189    *
190    * @return the namespace if defined or {@code null} otherwise
191    */
192   @Nullable
193   public URI getDefaultModelNamespace() {
194     return defaultModelNamespace;
195   }
196 
197   /**
198    * Get the default namespace for function references that have no namespace
199    * prefix.
200    *
201    * @return the namespace if defined or {@code null} otherwise
202    */
203   @Nullable
204   public URI getDefaultFunctionNamespace() {
205     return defaultFunctionNamespace;
206   }
207 
208   /**
209    * Get a prefix resolver for use with Metapath function names that will attempt
210    * to identify the namespace corresponding to a given prefix.
211    * <p>
212    * This will use the following lookup order, advancing to the next when a
213    * {@code null} value is returned:
214    * <ol>
215    * <li>Lookup the prefix using
216    * {@link StaticContext#lookupNamespaceForPrefix(String)}</li>
217    * <li>Return the result of
218    * {@link StaticContext#getDefaultFunctionNamespace()}</li>
219    * <li>Return {@link XMLConstants#NULL_NS_URI}</li>
220    * </ol>
221    *
222    * @return the resolver
223    */
224   @NonNull
225   public IEQNamePrefixResolver getFunctionPrefixResolver() {
226     return this::resolveFunctionPrefix;
227   }
228 
229   @NonNull
230   private String resolveFunctionPrefix(@NonNull String prefix) {
231     String ns = lookupNamespaceForPrefix(prefix);
232     if (ns == null) {
233       URI uri = getDefaultFunctionNamespace();
234       if (uri != null) {
235         ns = uri.toASCIIString();
236       }
237     }
238     return ns == null ? XMLConstants.NULL_NS_URI : ns;
239   }
240 
241   /**
242    * Get a prefix resolver for use with Metapath flag node names that will attempt
243    * to identify the namespace corresponding to a given prefix.
244    * <p>
245    * This will use the following lookup order, advancing to the next when a
246    * {@code null} value is returned:
247    * <ol>
248    * <li>Lookup the prefix using
249    * {@link StaticContext#lookupNamespaceForPrefix(String)}</li>
250    * <li>Return {@link XMLConstants#NULL_NS_URI}</li>
251    * </ol>
252    *
253    * @return the resolver
254    */
255   @NonNull
256   public IEQNamePrefixResolver getFlagPrefixResolver() {
257     return this::resolveFlagReferencePrefix;
258   }
259 
260   @NonNull
261   private String resolveFlagReferencePrefix(@NonNull String prefix) {
262     String ns = lookupNamespaceForPrefix(prefix);
263     return ns == null ? XMLConstants.NULL_NS_URI : ns;
264   }
265 
266   /**
267    * Get a prefix resolver for use with Metapath model node names that will
268    * attempt to identify the namespace corresponding to a given prefix.
269    * <p>
270    * This will use the following lookup order, advancing to the next when a
271    * {@code null} value is returned:
272    * <ol>
273    * <li>Lookup the prefix using
274    * {@link StaticContext#lookupNamespaceForPrefix(String)}</li>
275    * <li>Return the result of
276    * {@link StaticContext#getDefaultModelNamespace()}</li>
277    * <li>Return {@link XMLConstants#NULL_NS_URI}</li>
278    * </ol>
279    *
280    * @return the resolver
281    */
282   @NonNull
283   public IEQNamePrefixResolver getModelPrefixResolver() {
284     return this::resolveModelReferencePrefix;
285   }
286 
287   @NonNull
288   private String resolveModelReferencePrefix(@NonNull String prefix) {
289     String ns = lookupNamespaceForPrefix(prefix);
290     if (ns == null) {
291       URI uri = getDefaultModelNamespace();
292       if (uri != null) {
293         ns = uri.toASCIIString();
294       }
295     }
296     return ns == null ? XMLConstants.NULL_NS_URI : ns;
297   }
298 
299   /**
300    * Get a prefix resolver for use with Metapath variable names that will attempt
301    * to identify the namespace corresponding to a given prefix.
302    * <p>
303    * This will use the following lookup order, advancing to the next when a
304    * {@code null} value is returned:
305    * <ol>
306    * <li>Lookup the prefix using
307    * {@link StaticContext#lookupNamespaceForPrefix(String)}</li>
308    * <li>Return {@link XMLConstants#NULL_NS_URI}</li>
309    * </ol>
310    *
311    * @return the resolver
312    */
313   @NonNull
314   public IEQNamePrefixResolver getVariablePrefixResolver() {
315     return this::resolveVariablePrefix;
316   }
317 
318   @NonNull
319   private String resolveVariablePrefix(@NonNull String prefix) {
320     String ns = lookupNamespaceForPrefix(prefix);
321     return ns == null ? XMLConstants.NULL_NS_URI : ns;
322   }
323 
324   /**
325    * Get a new static context builder that is pre-populated with the setting of
326    * this static context.
327    *
328    * @return a new builder
329    */
330   @NonNull
331   public Builder buildFrom() {
332     Builder builder = builder();
333     builder.baseUri = this.baseUri;
334     builder.namespaces.putAll(this.knownNamespaces);
335     builder.defaultModelNamespace = this.defaultModelNamespace;
336     builder.defaultFunctionNamespace = this.defaultFunctionNamespace;
337     return builder;
338   }
339 
340   /**
341    * Indicates if a name match should use a wildcard for the namespace is the
342    * namespace does not have a value and the {@link #getDefaultModelNamespace()}
343    * is {@code null}.
344    *
345    * @return {@code true} if a wildcard match on the name space should be used or
346    *         {@code false} otherwise
347    */
348   public boolean isUseWildcardWhenNamespaceNotDefaulted() {
349     return useWildcardWhenNamespaceNotDefaulted && getDefaultModelNamespace() == null;
350   }
351 
352   /**
353    * Create a new static context builder that allows for fine-grained adjustments
354    * when creating a new static context.
355    *
356    * @return a new builder
357    */
358   @NonNull
359   public static Builder builder() {
360     return new Builder();
361   }
362 
363   /**
364    * A builder used to generate the static context.
365    */
366   public static final class Builder {
367     public boolean useWildcardWhenNamespaceNotDefaulted; // false
368     @Nullable
369     private URI baseUri;
370     @NonNull
371     private final Map<String, URI> namespaces = new ConcurrentHashMap<>();
372     @Nullable
373     private URI defaultModelNamespace;
374     @Nullable
375     private URI defaultFunctionNamespace = MetapathConstants.NS_METAPATH_FUNCTIONS;
376 
377     private Builder() {
378       namespaces.put(
379           MetapathConstants.PREFIX_METAPATH,
380           MetapathConstants.NS_METAPATH);
381       namespaces.put(
382           MetapathConstants.PREFIX_XML_SCHEMA,
383           MetapathConstants.NS_XML_SCHEMA);
384       namespaces.put(
385           MetapathConstants.PREFIX_XPATH_FUNCTIONS,
386           MetapathConstants.NS_METAPATH_FUNCTIONS);
387       namespaces.put(
388           MetapathConstants.PREFIX_XPATH_FUNCTIONS_MATH,
389           MetapathConstants.NS_METAPATH_FUNCTIONS_MATH);
390     }
391 
392     /**
393      * Sets the static base URI to use in resolving URIs handled by the Metapath
394      * processor, when a document base URI is not available. There is only a single
395      * base URI. Subsequent calls to this method will change the base URI.
396      *
397      * @param uri
398      *          the base URI to use
399      * @return this builder
400      */
401     @NonNull
402     public Builder baseUri(@NonNull URI uri) {
403       this.baseUri = uri;
404       return this;
405     }
406 
407     /**
408      * Adds a new prefix to namespace URI binding to the mapping of
409      * <a href="https://www.w3.org/TR/xpath-31/#dt-static-namespaces">statically
410      * known namespaces</a>.
411      * <p>
412      * A namespace set by this method can be resolved using the
413      * {@link StaticContext#lookupNamespaceForPrefix(String)} method.
414      * <p>
415      * Well-known namespace bindings are used by default, which can be retrieved
416      * using the {@link StaticContext#getWellKnownNamespacesMap()} method.
417      *
418      * @param prefix
419      *          the prefix to associate with the namespace, which may be
420      * @param uri
421      *          the namespace URI
422      * @return this builder
423      * @see StaticContext#lookupNamespaceForPrefix(String)
424      * @see StaticContext#lookupNamespaceURIForPrefix(String)
425      * @see StaticContext#getWellKnownNamespacesMap()
426      */
427     @NonNull
428     public Builder namespace(@NonNull String prefix, @NonNull URI uri) {
429       this.namespaces.put(prefix, uri);
430       return this;
431     }
432 
433     /**
434      * A convenience method for {@link #namespace(String, URI)}.
435      *
436      * @param prefix
437      *          the prefix to associate with the namespace, which may be
438      * @param uri
439      *          the namespace URI
440      * @return this builder
441      * @throws IllegalArgumentException
442      *           if the provided URI is invalid
443      * @see StaticContext#lookupNamespaceForPrefix(String)
444      * @see StaticContext#lookupNamespaceURIForPrefix(String)
445      * @see StaticContext#getWellKnownNamespacesMap()
446      */
447     @NonNull
448     public Builder namespace(@NonNull String prefix, @NonNull String uri) {
449       return namespace(prefix, ObjectUtils.notNull(URI.create(uri)));
450     }
451 
452     /**
453      * Defines the default namespace to use for assembly, field, or flag references
454      * that have no namespace prefix.
455      *
456      * @param uri
457      *          the namespace URI
458      * @return this builder
459      * @see StaticContext#getDefaultModelNamespace()
460      */
461     @NonNull
462     public Builder defaultModelNamespace(@NonNull URI uri) {
463       this.defaultModelNamespace = uri;
464       return this;
465     }
466 
467     /**
468      * A convenience method for {@link #defaultModelNamespace(URI)}.
469      *
470      * @param uri
471      *          the namespace URI
472      * @return this builder
473      * @throws IllegalArgumentException
474      *           if the provided URI is invalid
475      * @see StaticContext#getDefaultModelNamespace()
476      */
477     @NonNull
478     public Builder defaultModelNamespace(@NonNull String uri) {
479       return defaultModelNamespace(ObjectUtils.notNull(URI.create(uri)));
480     }
481 
482     /**
483      * Defines the default namespace to use for assembly, field, or flag references
484      * that have no namespace prefix.
485      *
486      * @param uri
487      *          the namespace URI
488      * @return this builder
489      * @see StaticContext#getDefaultFunctionNamespace()
490      */
491     @NonNull
492     public Builder defaultFunctionNamespace(@NonNull URI uri) {
493       this.defaultFunctionNamespace = uri;
494       return this;
495     }
496 
497     /**
498      * A convenience method for {@link #defaultFunctionNamespace(URI)}.
499      *
500      * @param uri
501      *          the namespace URI
502      * @return this builder
503      * @throws IllegalArgumentException
504      *           if the provided URI is invalid
505      * @see StaticContext#getDefaultFunctionNamespace()
506      */
507     @NonNull
508     public Builder defaultFunctionNamespace(@NonNull String uri) {
509       return defaultFunctionNamespace(ObjectUtils.notNull(URI.create(uri)));
510     }
511 
512     /**
513      * Set the name matching behavior for when a model node has no namespace.
514      *
515      * @param value
516      *          {@code true} if on or {@code false} otherwise
517      * @return this builder
518      */
519     public Builder useWildcardWhenNamespaceNotDefaulted(boolean value) {
520       this.useWildcardWhenNamespaceNotDefaulted = value;
521       return this;
522     }
523 
524     /**
525      * Construct a new static context using the information provided to the builder.
526      *
527      * @return the new static context
528      */
529     @NonNull
530     public StaticContext build() {
531       return new StaticContext(this);
532     }
533   }
534 }