1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath;
7   
8   import java.net.URI;
9   import java.net.URISyntaxException;
10  import java.util.Locale;
11  import java.util.Map;
12  import java.util.concurrent.ConcurrentHashMap;
13  import java.util.stream.Collectors;
14  
15  import javax.xml.XMLConstants;
16  
17  import dev.metaschema.core.datatype.DataTypeService;
18  import dev.metaschema.core.metapath.function.FunctionService;
19  import dev.metaschema.core.metapath.function.IFunction;
20  import dev.metaschema.core.metapath.function.IFunctionResolver;
21  import dev.metaschema.core.metapath.item.IItem;
22  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
23  import dev.metaschema.core.metapath.type.IAtomicOrUnionType;
24  import dev.metaschema.core.metapath.type.IItemType;
25  import dev.metaschema.core.qname.EQNameFactory;
26  import dev.metaschema.core.qname.IEnhancedQName;
27  import dev.metaschema.core.qname.NamespaceCache;
28  import dev.metaschema.core.qname.WellKnown;
29  import dev.metaschema.core.util.CollectionUtil;
30  import dev.metaschema.core.util.CustomCollectors;
31  import dev.metaschema.core.util.ObjectUtils;
32  import edu.umd.cs.findbugs.annotations.NonNull;
33  import edu.umd.cs.findbugs.annotations.Nullable;
34  
35  // add support for default namespace
36  /**
37   * An implementation of the Metapath
38   * <a href="https://www.w3.org/TR/xpath-31/#static_context">static context</a>.
39   */
40  // FIXME: refactor well-known into a new class
41  public final class StaticContext {
42    @Nullable
43    private final URI baseUri;
44    @NonNull
45    private final Map<String, String> knownPrefixToNamespace;
46    @NonNull
47    private final Map<String, String> knownNamespacesToPrefix;
48    @Nullable
49    private final String defaultModelNamespace;
50    @Nullable
51    private final String defaultFunctionNamespace;
52    @Nullable
53    private final String defaultLanguage;
54    private final boolean useWildcardWhenNamespaceNotDefaulted;
55    @NonNull
56    private final IFunctionResolver functionResolver;
57  
58    /**
59     * Get the namespace prefix associated with the provided URI, if the URI is
60     * well-known.
61     * <p>
62     * This method has been deprecated. While
63     * {@link WellKnown#getWellKnownPrefixForUri(String)} can be used in place of
64     * this method.
65     *
66     * @param uri
67     *          the URI to get the prefix for
68     * @return the prefix or {@code null} if the provided URI is not well-known
69     */
70    @Deprecated(since = "2.2.0", forRemoval = true)
71    @Nullable
72    public static String getWellKnownPrefixForUri(@NonNull String uri) {
73      return WellKnown.getWellKnownPrefixForUri(uri);
74    }
75  
76    /**
77     * Create a new static context instance using default values.
78     *
79     * @return a new static context instance
80     */
81    @NonNull
82    public static StaticContext instance() {
83      return builder().build();
84    }
85  
86    private StaticContext(Builder builder) {
87      this.baseUri = builder.baseUri;
88      this.knownPrefixToNamespace = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Map.copyOf(builder.namespaces)));
89      this.knownNamespacesToPrefix = ObjectUtils.notNull(builder.namespaces.entrySet().stream()
90          .map(entry -> Map.entry(entry.getValue(), entry.getKey()))
91          .collect(Collectors.toUnmodifiableMap(
92              Map.Entry::getKey,
93              Map.Entry::getValue,
94              CustomCollectors.useFirstMapper())));
95      this.defaultModelNamespace = builder.defaultModelNamespace;
96      this.defaultFunctionNamespace = builder.defaultFunctionNamespace;
97      this.defaultLanguage = builder.defaultLanguage;
98      this.useWildcardWhenNamespaceNotDefaulted = builder.useWildcardWhenNamespaceNotDefaulted;
99      this.functionResolver = builder.functionResolver;
100   }
101 
102   /**
103    * Get the static base URI to use in resolving URIs handled by the Metapath
104    * processor. This URI, if provided, will be used when a document base URI is
105    * not available.
106    *
107    * @return the base URI or {@code null} if not defined
108    */
109   @Nullable
110   public URI getBaseUri() {
111     return baseUri;
112   }
113 
114   /**
115    * Get the namespace URI associated with the provided {@code prefix}, if any is
116    * bound.
117    * <p>
118    * This method uses the namespaces set by the
119    * {@link Builder#namespace(String, URI)} method, falling back to the well-known
120    * namespace bindings when a prefix match is not found.
121    * <p>
122    * The well-known namespace bindings can be retrieved using the
123    * {@link StaticContext#getWellKnownNamespacesMap()} method.
124    *
125    * @param prefix
126    *          the namespace prefix
127    * @return the namespace URI bound to the prefix, or {@code null} if no
128    *         namespace is bound to the prefix
129    * @see Builder#namespace(String, URI)
130    * @see #getWellKnownNamespacesMap()
131    */
132   @Nullable
133   private String lookupNamespaceURIForPrefix(@NonNull String prefix) {
134     String retval = knownPrefixToNamespace.get(prefix);
135     if (retval == null) {
136       // fall back to well-known namespaces
137       retval = WellKnown.getWellKnownUriForPrefix(prefix);
138     }
139     return retval;
140   }
141 
142   @Nullable
143   private String lookupPrefixForNamespaceURI(@NonNull String namespace) {
144     String retval = knownNamespacesToPrefix.get(namespace);
145     if (retval == null) {
146       // fall back to well-known namespaces
147       retval = WellKnown.getWellKnownPrefixForUri(namespace);
148     }
149     return retval;
150   }
151 
152   /**
153    * Get the namespace associated with the provided {@code prefix} as a string, if
154    * any is bound.
155    *
156    * @param prefix
157    *          the namespace prefix
158    * @return the namespace string bound to the prefix, or {@code null} if no
159    *         namespace is bound to the prefix
160    */
161   @Nullable
162   public String lookupNamespaceForPrefix(@NonNull String prefix) {
163     String result = lookupNamespaceURIForPrefix(prefix);
164     return result == null ? null : result;
165   }
166 
167   /**
168    * Get the prefix associated with the provided {@code namespace} as a string, if
169    * any is bound.
170    *
171    * @param namespace
172    *          the namespace
173    * @return the prefix string bound to the prefix, or {@code null} if no prefix
174    *         is bound to the namespace
175    */
176   @Nullable
177   public String lookupPrefixForNamespace(@NonNull String namespace) {
178     return lookupPrefixForNamespaceURI(namespace);
179   }
180 
181   /**
182    * Get the default namespace for assembly, field, or flag references that have
183    * no namespace prefix.
184    *
185    * @return the namespace if defined or {@code null} otherwise
186    */
187   @Nullable
188   private String getDefaultModelNamespace() {
189     return defaultModelNamespace;
190   }
191 
192   /**
193    * Get the default namespace for function references that have no namespace
194    * prefix.
195    *
196    * @return the namespace if defined or {@code null} otherwise
197    */
198   @Nullable
199   private String getDefaultFunctionNamespace() {
200     return defaultFunctionNamespace;
201   }
202 
203   /**
204    * Get the default language to be used by functions like fn:lang() when
205    * processing language-sensitive operations.
206    * <p>
207    * If no default language is configured, the JVM's default locale language code
208    * is returned (e.g., "en" for English systems, "fr" for French systems).
209    *
210    * @return the default language code, or the JVM's default locale language if
211    *         not configured
212    * @see <a href=
213    *      "https://www.w3.org/TR/xpath-functions-31/#func-default-language">XPath
214    *      3.1 fn:default-language</a>
215    */
216   @NonNull
217   public String getDefaultLanguage() {
218     return defaultLanguage == null
219         ? ObjectUtils.notNull(Locale.getDefault().getLanguage())
220         : defaultLanguage;
221   }
222 
223   /**
224    * Parse the name of an atomic type.
225    * <p>
226    * This method will attempt to identify the namespace corresponding to a given
227    * prefix.
228    * <p>
229    * The prefix will be resolved using the following lookup order, advancing to
230    * the next when a {@code null} value is returned:
231    * <ol>
232    * <li>Lookup the prefix using the namespaces registered with the static
233    * context.
234    * <li>Lookup the prefix in the well-known namespaces.
235    * </ol>
236    * <p>
237    * If an empty prefix is provided, the {@link MetapathConstants#NS_METAPATH}
238    * namespace will be used.
239    *
240    * @param name
241    *          the name
242    * @return the parsed qualified name
243    * @throws StaticMetapathException
244    *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
245    *           if a non-empty prefix is provided
246    */
247   @NonNull
248   public IEnhancedQName parseAtomicTypeName(@NonNull String name) {
249     return EQNameFactory.instance().parseName(
250         name,
251         this::resolveAtomicTypePrefix);
252   }
253 
254   private String resolveAtomicTypePrefix(@NonNull String prefix) {
255     String ns = lookupNamespaceForPrefix(prefix);
256     if (ns == null) {
257       checkForUnknownPrefix(prefix);
258       // use the default data type namespace
259       ns = MetapathConstants.NS_METAPATH;
260     }
261     return ns;
262   }
263 
264   /**
265    * Lookup the atomic type with the provided name in the static context.
266    * <p>
267    * This method will first attempt to expand the namespace prefix for a lexical
268    * QName. A {@link StaticMetapathException} with the code
269    * {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE} if the prefix is not
270    * known to the static context.
271    * <p>
272    * Once the qualified name has been produced, the atomic type will be retrieved
273    * from the available atomic types. If the atomic type was not found, a
274    * {@link StaticMetapathException} with the code
275    * {@link StaticMetapathException#UNKNOWN_TYPE} will be thrown. Otherwise, the
276    * type information is returned for the matching atomic type.
277    *
278    * @param name
279    *          the namespace qualified or lexical name of the data type.
280    * @return the data type information
281    * @throws StaticMetapathException
282    *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
283    *           if the lexical name was not able to be expanded or the code
284    *           {@link StaticMetapathException#NO_FUNCTION_MATCH} if a matching
285    *           type was not found
286    */
287   @NonNull
288   public IAtomicOrUnionType<?> lookupAtomicType(@NonNull String name) {
289     IEnhancedQName qname = parseAtomicTypeName(name);
290     return lookupAtomicType(qname);
291   }
292 
293   /**
294    * Lookup a known Metapath atomic type based on the type's qualified name.
295    *
296    * @param qname
297    *          the qualified name
298    * @return the type
299    * @throws StaticMetapathException
300    *           with the code {@link StaticMetapathException#UNKNOWN_TYPE} if the
301    *           type was not found
302    */
303   @NonNull
304   public static IAtomicOrUnionType<?> lookupAtomicType(@NonNull IEnhancedQName qname) {
305     IAtomicOrUnionType<?> retval = DataTypeService.instance().getAtomicTypeByQNameIndex(qname.getIndexPosition());
306     if (retval == null) {
307       throw new StaticMetapathException(
308           StaticMetapathException.UNKNOWN_TYPE,
309           String.format("The atomic type named '%s' was not found.", qname));
310     }
311     return retval;
312   }
313 
314   /**
315    * Lookup a known Metapath atomic type based on the type's item class.
316    *
317    * @param <T>
318    *          the Java type of the item to get the type information for
319    * @param clazz
320    *          the item class associated with the atomic type
321    * @return the type
322    * @throws StaticMetapathException
323    *           with the code {@link StaticMetapathException#UNKNOWN_TYPE} if the
324    *           type was not found
325    */
326   @NonNull
327   public static <T extends IAnyAtomicItem> IAtomicOrUnionType<T> lookupAtomicType(Class<T> clazz) {
328     IAtomicOrUnionType<T> retval = DataTypeService.instance().getAtomicTypeByItemClass(clazz);
329     if (retval == null) {
330       throw new StaticMetapathException(
331           StaticMetapathException.UNKNOWN_TYPE,
332           String.format("The atomic type for item class '%s' was not found.", clazz.getName()));
333     }
334     return retval;
335   }
336 
337   /**
338    * Lookup a known Metapath item type based on the type's item class.
339    *
340    * @param clazz
341    *          the item class associated with the atomic type
342    * @return the type
343    * @throws StaticMetapathException
344    *           with the code {@link StaticMetapathException#UNKNOWN_TYPE} if the
345    *           type was not found
346    */
347   @NonNull
348   public static IItemType lookupItemType(Class<? extends IItem> clazz) {
349     IItemType retval = DataTypeService.instance().getItemTypeByItemClass(clazz);
350     if (retval == null) {
351       throw new StaticMetapathException(
352           StaticMetapathException.UNKNOWN_TYPE,
353           String.format("The item type for item class '%s' was not found.", clazz.getName()));
354     }
355     return retval;
356   }
357 
358   /**
359    * Parse a function name.
360    * <p>
361    * This method will attempt to identify the namespace corresponding to a given
362    * prefix.
363    * <p>
364    * The prefix will be resolved using the following lookup order, advancing to
365    * the next when a {@code null} value is returned:
366    * <ol>
367    * <li>Lookup the prefix using the namespaces registered with the static
368    * context.
369    * <li>Lookup the prefix in the well-known namespaces.
370    * </ol>
371    * If an empty prefix is provided, the
372    * {@link Builder#defaultFunctionNamespace(String)} namespace will be used.
373    *
374    * @param name
375    *          the name
376    * @return the parsed qualified name
377    */
378   @NonNull
379   public IEnhancedQName parseFunctionName(@NonNull String name) {
380     return EQNameFactory.instance().parseName(
381         name,
382         this::resolveFunctionPrefix);
383   }
384 
385   @NonNull
386   private String resolveFunctionPrefix(@NonNull String prefix) {
387     String ns = lookupNamespaceForPrefix(prefix);
388     if (ns == null) {
389       checkForUnknownPrefix(prefix);
390       // use the default namespace, since the namespace was omitted
391       ns = getDefaultFunctionNamespace();
392     }
393     return ns == null ? XMLConstants.NULL_NS_URI : ns;
394   }
395 
396   /**
397    * Checks if the provided prefix is not-empty, which means the prefix was not
398    * resolvable.
399    *
400    * @param prefix
401    *          the lexical prefix to check
402    * @throws StaticMetapathException
403    *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
404    *           if a non-empty prefix is provided
405    */
406   private static void checkForUnknownPrefix(@NonNull String prefix) {
407     if (!prefix.isEmpty()) {
408       throw new StaticMetapathException(
409           StaticMetapathException.PREFIX_NOT_EXPANDABLE,
410           String.format("The namespace prefix '%s' is not expandable.",
411               prefix));
412     }
413   }
414 
415   /**
416    * Lookup a known Metapath function based on the function's name and arity.
417    * <p>
418    * This method will first attempt to expand the namespace prefix for a lexical
419    * QName. A {@link StaticMetapathException} with the code
420    * {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE} if the prefix is not
421    * known to the static context.
422    * <p>
423    * Once the qualified name has been produced, the function will be retrieved
424    * from the available functions. If the function was not found, a
425    * {@link StaticMetapathException} with the code
426    * {@link StaticMetapathException#UNKNOWN_TYPE} will be thrown. Otherwise, the
427    * data type information is returned for the matching data type.
428    *
429    * @param name
430    *          the qualified or lexical name of the function
431    * @param arity
432    *          the number of arguments
433    * @return the type
434    * @throws StaticMetapathException
435    *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
436    *           if the lexical name was not able to be expanded or the code
437    *           {@link StaticMetapathException#NO_FUNCTION_MATCH} if a matching
438    *           function was not found
439    */
440   @NonNull
441   public IFunction lookupFunction(@NonNull String name, int arity) {
442     IEnhancedQName qname = parseFunctionName(name);
443     return lookupFunction(qname, arity);
444   }
445 
446   /**
447    * Lookup a known Metapath function based on the function's name and arity.
448    *
449    * @param qname
450    *          the qualified name of the function
451    * @param arity
452    *          the number of arguments
453    * @return the function
454    * @throws StaticMetapathException
455    *           with the code {@link StaticMetapathException#NO_FUNCTION_MATCH} if
456    *           a matching function was not found
457    */
458   @NonNull
459   public IFunction lookupFunction(@NonNull IEnhancedQName qname, int arity) {
460     return functionResolver.getFunction(qname, arity);
461   }
462 
463   /**
464    * Parse a flag name.
465    * <p>
466    * This method will attempt to identify the namespace corresponding to a given
467    * prefix.
468    * <p>
469    * The prefix will be resolved using the following lookup order, advancing to
470    * the next when a {@code null} value is returned:
471    * <ol>
472    * <li>Lookup the prefix using the namespaces registered with the static
473    * context.
474    * <li>Lookup the prefix in the well-known namespaces.
475    * </ol>
476    * If an empty prefix is provided, the {@link XMLConstants#NULL_NS_URI}
477    * namespace will be used.
478    *
479    * @param name
480    *          the name
481    * @return the parsed qualified name
482    * @throws StaticMetapathException
483    *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
484    *           if a non-empty prefix is provided
485    */
486   @NonNull
487   public IEnhancedQName parseFlagName(@NonNull String name) {
488     return EQNameFactory.instance().parseName(
489         name,
490         this::resolveBasicPrefix);
491   }
492 
493   private String resolveBasicPrefix(@NonNull String prefix) {
494     String ns = lookupNamespaceForPrefix(prefix);
495     if (ns == null) {
496       checkForUnknownPrefix(prefix);
497     }
498     return ns == null ? XMLConstants.NULL_NS_URI : ns;
499   }
500 
501   /**
502    * Parse a model name.
503    * <p>
504    * This method will attempt to identify the namespace corresponding to a given
505    * prefix.
506    * <p>
507    * The prefix will be resolved using the following lookup order, advancing to
508    * the next when a {@code null} value is returned:
509    * <ol>
510    * <li>Lookup the prefix using the namespaces registered with the static
511    * context.
512    * <li>Lookup the prefix in the well-known namespaces.
513    * </ol>
514    * If an empty prefix is provided, the
515    * {@link Builder#defaultModelNamespace(String)} namespace will be used.
516    *
517    * @param name
518    *          the name
519    * @return the parsed qualified name
520    */
521   @NonNull
522   public IEnhancedQName parseModelName(@NonNull String name) {
523     return EQNameFactory.instance().parseName(
524         name,
525         this::resolveModelReferencePrefix);
526   }
527 
528   @NonNull
529   private String resolveModelReferencePrefix(@NonNull String prefix) {
530     String ns = lookupNamespaceForPrefix(prefix);
531     if (ns == null) {
532       checkForUnknownPrefix(prefix);
533       ns = getDefaultModelNamespace();
534     }
535     return ns == null ? XMLConstants.NULL_NS_URI : ns;
536   }
537 
538   /**
539    * Parse a variable name.
540    * <p>
541    * This method will attempt to identify the namespace corresponding to a given
542    * prefix.
543    * <p>
544    * The prefix will be resolved using the following lookup order, advancing to
545    * the next when a {@code null} value is returned:
546    * <ol>
547    * <li>Lookup the prefix using the namespaces registered with the static
548    * context.
549    * <li>Lookup the prefix in the well-known namespaces.
550    * </ol>
551    * If an empty prefix is provided, the {@link XMLConstants#NULL_NS_URI}
552    * namespace will be used.
553    *
554    * @param name
555    *          the name
556    * @return the parsed qualified name
557    */
558   @NonNull
559   public IEnhancedQName parseVariableName(@NonNull String name) {
560     return EQNameFactory.instance().parseName(
561         name,
562         this::resolveBasicPrefix);
563   }
564 
565   /**
566    * Get a new static context builder that is pre-populated with the setting of
567    * this static context.
568    *
569    * @return a new builder
570    */
571   @NonNull
572   public Builder buildFrom() {
573     Builder builder = builder();
574     builder.baseUri = this.baseUri;
575     builder.namespaces.putAll(this.knownPrefixToNamespace);
576     builder.defaultModelNamespace = this.defaultModelNamespace;
577     builder.defaultFunctionNamespace = this.defaultFunctionNamespace;
578     builder.defaultLanguage = this.defaultLanguage;
579     return builder;
580   }
581 
582   /**
583    * Indicates if a name match should use a wildcard for the namespace if the
584    * namespace does not have a value and the default model namespace is
585    * {@code null}.
586    *
587    * @return {@code true} if a wildcard match on the name space should be used or
588    *         {@code false} otherwise
589    */
590   public boolean isUseWildcardWhenNamespaceNotDefaulted() {
591     return useWildcardWhenNamespaceNotDefaulted && getDefaultModelNamespace() == null;
592   }
593 
594   /**
595    * Create a new static context builder that allows for fine-grained adjustments
596    * when creating a new static context.
597    *
598    * @return a new builder
599    */
600   @NonNull
601   public static Builder builder() {
602     return new Builder();
603   }
604 
605   /**
606    * A builder used to generate the static context.
607    */
608   public static final class Builder {
609     private boolean useWildcardWhenNamespaceNotDefaulted; // false
610     @Nullable
611     private URI baseUri;
612     @NonNull
613     private final Map<String, String> namespaces = new ConcurrentHashMap<>();
614     @Nullable
615     private String defaultModelNamespace;
616     @Nullable
617     private String defaultFunctionNamespace = MetapathConstants.NS_METAPATH_FUNCTIONS;
618     @Nullable
619     private String defaultLanguage;
620     @NonNull
621     private IFunctionResolver functionResolver = FunctionService.getInstance();
622 
623     private Builder() {
624       // avoid direct construction
625     }
626 
627     /**
628      * Sets the static base URI to use in resolving URIs handled by the Metapath
629      * processor, when a document base URI is not available. There is only a single
630      * base URI. Subsequent calls to this method will change the base URI.
631      *
632      * @param uri
633      *          the base URI to use
634      * @return this builder
635      */
636     @NonNull
637     public Builder baseUri(@NonNull URI uri) {
638       this.baseUri = uri;
639       return this;
640     }
641 
642     /**
643      * Adds a new prefix to namespace URI binding to the mapping of
644      * <a href="https://www.w3.org/TR/xpath-31/#dt-static-namespaces">statically
645      * known namespaces</a>.
646      * <p>
647      * A namespace set by this method can be resolved using the
648      * {@link StaticContext#lookupNamespaceForPrefix(String)} method.
649      * <p>
650      * Well-known namespace bindings are used by default, which are provided by
651      * {@link WellKnown}.
652      *
653      * @param prefix
654      *          the prefix to associate with the namespace, which may be
655      * @param uri
656      *          the namespace URI
657      * @return this builder
658      * @see StaticContext#lookupNamespaceForPrefix(String)
659      */
660     @NonNull
661     public Builder namespace(@NonNull String prefix, @NonNull URI uri) {
662       return namespace(prefix, ObjectUtils.notNull(uri.toASCIIString()));
663     }
664 
665     /**
666      * A convenience method for {@link #namespace(String, URI)}.
667      *
668      * @param prefix
669      *          the prefix to associate with the namespace, which may be
670      * @param uri
671      *          the namespace URI
672      * @return this builder
673      * @throws IllegalArgumentException
674      *           if the provided prefix or URI is invalid
675      * @see StaticContext#lookupNamespaceForPrefix(String)
676      */
677     @NonNull
678     public Builder namespace(@NonNull String prefix, @NonNull String uri) {
679       if (MetapathConstants.PREFIX_METAPATH.equals(prefix)) {
680         // check for https://www.w3.org/TR/xpath-31/#ERRXPST0070 for "meta"
681         throw new IllegalArgumentException(
682             "Redefining the prefix '" + MetapathConstants.PREFIX_METAPATH + "' is not allowed.");
683       }
684       this.namespaces.put(prefix, uri);
685       NamespaceCache.instance().indexOf(uri);
686       return this;
687     }
688 
689     /**
690      * Defines the default namespace to use for assembly, field, or flag references
691      * that have no namespace prefix.
692      *
693      * @param namespace
694      *          the namespace URI
695      * @return this builder
696      */
697     @NonNull
698     public Builder defaultModelNamespace(@NonNull URI namespace) {
699       String uri = ObjectUtils.notNull(namespace.toASCIIString());
700       this.defaultModelNamespace = uri;
701       NamespaceCache.instance().indexOf(uri);
702       return this;
703     }
704 
705     /**
706      * A convenience method for {@link #defaultModelNamespace(URI)}.
707      *
708      * @param uri
709      *          the namespace URI
710      * @return this builder
711      * @throws IllegalArgumentException
712      *           if the provided URI is invalid
713      */
714     @NonNull
715     public Builder defaultModelNamespace(@NonNull String uri) {
716       try {
717         this.defaultModelNamespace = new URI(uri).toASCIIString();
718       } catch (URISyntaxException ex) {
719         throw new IllegalArgumentException(ex);
720       }
721       NamespaceCache.instance().indexOf(uri);
722       return this;
723     }
724 
725     /**
726      * Defines the default namespace to use for assembly, field, or flag references
727      * that have no namespace prefix.
728      *
729      * @param namespace
730      *          the namespace URI
731      * @return this builder
732      */
733     @NonNull
734     public Builder defaultFunctionNamespace(@NonNull URI namespace) {
735       String uri = ObjectUtils.notNull(namespace.toASCIIString());
736       this.defaultFunctionNamespace = uri;
737       NamespaceCache.instance().indexOf(uri);
738       return this;
739     }
740 
741     /**
742      * A convenience method for {@link #defaultFunctionNamespace(URI)}.
743      *
744      * @param uri
745      *          the namespace URI
746      * @return this builder
747      * @throws IllegalArgumentException
748      *           if the provided URI is invalid
749      */
750     @NonNull
751     public Builder defaultFunctionNamespace(@NonNull String uri) {
752       try {
753         this.defaultFunctionNamespace = new URI(uri).toASCIIString();
754       } catch (URISyntaxException ex) {
755         throw new IllegalArgumentException(ex);
756       }
757       NamespaceCache.instance().indexOf(uri);
758       return this;
759     }
760 
761     /**
762      * Defines the default language to be used by functions like fn:lang() when
763      * processing language-sensitive operations.
764      * <p>
765      * If not set, the JVM's default locale language code will be used (e.g., "en"
766      * for English systems, "fr" for French systems).
767      *
768      * @param language
769      *          the language code (e.g., "en", "fr", "de")
770      * @return this builder
771      * @see <a href=
772      *      "https://www.w3.org/TR/xpath-functions-31/#func-default-language">XPath
773      *      3.1 fn:default-language</a>
774      */
775     @NonNull
776     public Builder defaultLanguage(@NonNull String language) {
777       this.defaultLanguage = language;
778       return this;
779     }
780 
781     /**
782      * Set the name matching behavior for when a model node has no namespace.
783      *
784      * @param value
785      *          {@code true} if on or {@code false} otherwise
786      * @return this builder
787      */
788     public Builder useWildcardWhenNamespaceNotDefaulted(boolean value) {
789       this.useWildcardWhenNamespaceNotDefaulted = value;
790       return this;
791     }
792 
793     /**
794      * Set the function resolver used to lookup function implementations.
795      * <p>
796      * By default, the {@link FunctionService} is used to load function
797      * implementations using the service provider interface.
798      *
799      * @param resolver
800      *          the resolver to use instead of the default resolver
801      * @return this builder
802      */
803     public Builder functionResolver(@NonNull IFunctionResolver resolver) {
804       this.functionResolver = resolver;
805       return this;
806     }
807 
808     /**
809      * Construct a new static context using the information provided to the builder.
810      *
811      * @return the new static context
812      */
813     @NonNull
814     public StaticContext build() {
815       return new StaticContext(this);
816     }
817   }
818 }