001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.metapath;
007
008import java.net.URI;
009import java.net.URISyntaxException;
010import java.util.Locale;
011import java.util.Map;
012import java.util.concurrent.ConcurrentHashMap;
013import java.util.stream.Collectors;
014
015import javax.xml.XMLConstants;
016
017import dev.metaschema.core.datatype.DataTypeService;
018import dev.metaschema.core.metapath.function.FunctionService;
019import dev.metaschema.core.metapath.function.IFunction;
020import dev.metaschema.core.metapath.function.IFunctionResolver;
021import dev.metaschema.core.metapath.item.IItem;
022import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
023import dev.metaschema.core.metapath.type.IAtomicOrUnionType;
024import dev.metaschema.core.metapath.type.IItemType;
025import dev.metaschema.core.qname.EQNameFactory;
026import dev.metaschema.core.qname.IEnhancedQName;
027import dev.metaschema.core.qname.NamespaceCache;
028import dev.metaschema.core.qname.WellKnown;
029import dev.metaschema.core.util.CollectionUtil;
030import dev.metaschema.core.util.CustomCollectors;
031import dev.metaschema.core.util.ObjectUtils;
032import edu.umd.cs.findbugs.annotations.NonNull;
033import edu.umd.cs.findbugs.annotations.Nullable;
034
035// add support for default namespace
036/**
037 * An implementation of the Metapath
038 * <a href="https://www.w3.org/TR/xpath-31/#static_context">static context</a>.
039 */
040// FIXME: refactor well-known into a new class
041public final class StaticContext {
042  @Nullable
043  private final URI baseUri;
044  @NonNull
045  private final Map<String, String> knownPrefixToNamespace;
046  @NonNull
047  private final Map<String, String> knownNamespacesToPrefix;
048  @Nullable
049  private final String defaultModelNamespace;
050  @Nullable
051  private final String defaultFunctionNamespace;
052  @Nullable
053  private final String defaultLanguage;
054  private final boolean useWildcardWhenNamespaceNotDefaulted;
055  @NonNull
056  private final IFunctionResolver functionResolver;
057
058  /**
059   * Get the namespace prefix associated with the provided URI, if the URI is
060   * well-known.
061   * <p>
062   * This method has been deprecated. While
063   * {@link WellKnown#getWellKnownPrefixForUri(String)} can be used in place of
064   * this method.
065   *
066   * @param uri
067   *          the URI to get the prefix for
068   * @return the prefix or {@code null} if the provided URI is not well-known
069   */
070  @Deprecated(since = "2.2.0", forRemoval = true)
071  @Nullable
072  public static String getWellKnownPrefixForUri(@NonNull String uri) {
073    return WellKnown.getWellKnownPrefixForUri(uri);
074  }
075
076  /**
077   * Create a new static context instance using default values.
078   *
079   * @return a new static context instance
080   */
081  @NonNull
082  public static StaticContext instance() {
083    return builder().build();
084  }
085
086  private StaticContext(Builder builder) {
087    this.baseUri = builder.baseUri;
088    this.knownPrefixToNamespace = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Map.copyOf(builder.namespaces)));
089    this.knownNamespacesToPrefix = ObjectUtils.notNull(builder.namespaces.entrySet().stream()
090        .map(entry -> Map.entry(entry.getValue(), entry.getKey()))
091        .collect(Collectors.toUnmodifiableMap(
092            Map.Entry::getKey,
093            Map.Entry::getValue,
094            CustomCollectors.useFirstMapper())));
095    this.defaultModelNamespace = builder.defaultModelNamespace;
096    this.defaultFunctionNamespace = builder.defaultFunctionNamespace;
097    this.defaultLanguage = builder.defaultLanguage;
098    this.useWildcardWhenNamespaceNotDefaulted = builder.useWildcardWhenNamespaceNotDefaulted;
099    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}