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 }