1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.qname;
7   
8   import java.net.URI;
9   import java.util.Optional;
10  
11  import javax.xml.XMLConstants;
12  import javax.xml.namespace.QName;
13  
14  import dev.metaschema.core.metapath.StaticContext;
15  import dev.metaschema.core.metapath.StaticMetapathException;
16  import dev.metaschema.core.util.ObjectUtils;
17  import edu.umd.cs.findbugs.annotations.NonNull;
18  import edu.umd.cs.findbugs.annotations.Nullable;
19  
20  /**
21   * An efficient cache-backed representation of a qualified name.
22   * <p>
23   * This implementation uses an underlying integer-based cache to reduce the
24   * memory footprint of qualified names and namespaces by reusing instances with
25   * the same namespace and local name.
26   */
27  public interface IEnhancedQName extends Comparable<IEnhancedQName> {
28    /**
29     * Get the index position of the qualified name.
30     * <p>
31     * This value can be used in place of this object. The object can be retrieved
32     * using this index with the {@link #of(int)} method.
33     *
34     * @return the index position
35     */
36    int getIndexPosition();
37  
38    /**
39     * Get the namespace part of the qualified name.
40     *
41     * @return the namespace
42     */
43    @NonNull
44    String getNamespace();
45  
46    /**
47     * Get the namespace part of the qualified name.
48     *
49     * @return the namespace as a URI
50     */
51    @NonNull
52    URI getNamespaceAsUri();
53  
54    /**
55     * Get the local part of the qualified name.
56     *
57     * @return the local name
58     */
59    @NonNull
60    String getLocalName();
61  
62    /**
63     * Get an existing qualified name by looking up the cached entry using the
64     * provided index value.
65     *
66     * @param index
67     *          the index value to lookup
68     * @return an optional containing the qualified name, if it exists
69     */
70    @NonNull
71    static Optional<IEnhancedQName> of(int index) {
72      return EQNameFactory.instance().get(index);
73    }
74  
75    /**
76     * Get a qualified name using the provided {@link QName} value.
77     *
78     * @param qname
79     *          the qualified name to get
80     * @return the qualified name
81     */
82    @NonNull
83    static IEnhancedQName of(@NonNull QName qname) {
84      return of(
85          ObjectUtils.notNull(qname.getNamespaceURI()),
86          ObjectUtils.notNull(qname.getLocalPart()));
87    }
88  
89    /**
90     * Get a qualified name using the provided local name value with no namespace.
91     *
92     * @param localName
93     *          the qualified name local part
94     * @return the qualified name
95     */
96    @NonNull
97    static IEnhancedQName of(@NonNull String localName) {
98      return of("", localName);
99    }
100 
101   /**
102    * Get a qualified name using the provided namespace and local name.
103    *
104    * @param namespace
105    *          the qualified name namespace part
106    * @param localName
107    *          the qualified name local part
108    * @return the qualified name
109    */
110   @NonNull
111   static IEnhancedQName of(@NonNull URI namespace, @NonNull String localName) {
112     return of(ObjectUtils.notNull(namespace.toASCIIString()), localName);
113   }
114 
115   /**
116    * Get a qualified name using the provided namespace and local name.
117    *
118    * @param namespace
119    *          the qualified name namespace part
120    * @param localName
121    *          the qualified name local part
122    * @return the qualified name
123    */
124   @NonNull
125   static IEnhancedQName of(@NonNull String namespace, @NonNull String localName) {
126     return EQNameFactory.instance().newQName(namespace, localName);
127   }
128 
129   /**
130    * Generate a qualified name for this QName.
131    * <p>
132    * This method uses prefixes associated with well-known namespaces, or will
133    * prepend the namespace if no prefix can be resolved.
134    *
135    * @return the extended qualified-name
136    */
137   @NonNull
138   default String toEQName() {
139     return toEQName((NamespaceToPrefixResolver) null);
140   }
141 
142   /**
143    * Generate a qualified name for this QName, use a prefix provided by the
144    * resolver, or by prepending the namespace if no prefix can be resolved.
145    *
146    * @param resolver
147    *          the resolver to use to lookup the prefix
148    * @return the extended qualified-name
149    */
150   @NonNull
151   default String toEQName(@Nullable NamespaceToPrefixResolver resolver) {
152     String namespace = getNamespace();
153     String prefix = namespace.isEmpty() ? null : WellKnown.getWellKnownPrefixForUri(namespace);
154     if (prefix == null && resolver != null) {
155       prefix = resolver.resolve(namespace);
156     }
157     return toEQName(namespace, getLocalName(), prefix);
158   }
159 
160   /**
161    * Generate a qualified name for this QName. Use a prefix resolved from the
162    * provided static context, or prepend the namespace if no prefix can be
163    * resolved.
164    *
165    * @param staticContext
166    *          the static context to use to lookup the prefix
167    * @return the extended qualified-name
168    */
169   @NonNull
170   default String toEQName(@NonNull StaticContext staticContext) {
171     String namespace = getNamespace();
172     String prefix = namespace.isEmpty() ? null : staticContext.lookupPrefixForNamespace(namespace);
173     return toEQName(namespace, getLocalName(), prefix);
174   }
175 
176   @NonNull
177   private static String toEQName(
178       @NonNull String namespace,
179       @NonNull String localName,
180       @Nullable String prefix) {
181 
182     StringBuilder builder = new StringBuilder();
183     if (prefix == null) {
184       if (!namespace.isEmpty()) {
185         builder.append("Q{")
186             .append(namespace)
187             .append('}');
188       }
189     } else {
190       builder.append(prefix)
191           .append(':');
192     }
193     return ObjectUtils.notNull(builder.append(localName)
194         .toString());
195   }
196 
197   /**
198    * Generate a {@link QName} without a namespace prefix.
199    *
200    * @return the name
201    */
202   @NonNull
203   default QName toQName() {
204     return toQName(XMLConstants.DEFAULT_NS_PREFIX);
205   }
206 
207   /**
208    * Generate a {@link QName} using the provided namespace prefix.
209    *
210    * @param prefix
211    *          the prefix to use
212    * @return the name
213    */
214   @NonNull
215   default QName toQName(@NonNull String prefix) {
216     return new QName(getNamespace(), getLocalName(), prefix);
217   }
218 
219   /**
220    * Provides a callback for resolving namespace prefixes.
221    */
222   @FunctionalInterface
223   interface NamespaceToPrefixResolver {
224     /**
225      * Get the URI string for the provided namespace prefix.
226      *
227      * @param namespace
228      *          the namespace URI
229      * @return the associated prefix or {@code null} if no prefix is associated
230      */
231     @Nullable
232     String resolve(@NonNull String namespace);
233   }
234 
235   /**
236    * Provides a callback for resolving namespace prefixes.
237    */
238   @FunctionalInterface
239   interface PrefixToNamespaceResolver {
240     /**
241      * Get the URI string for the provided namespace prefix.
242      *
243      * @param name
244      *          the name to resolve
245      * @return the URI string or {@code null} if the prefix is unbound
246      * @throws StaticMetapathException
247      *           with the code {@link StaticMetapathException#PREFIX_NOT_EXPANDABLE}
248      *           if a non-empty prefix is provided
249      */
250     @NonNull
251     IEnhancedQName resolve(@NonNull String name);
252   }
253 
254 }