001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.qname;
007
008import java.net.URI;
009import java.util.Optional;
010
011import javax.xml.XMLConstants;
012import javax.xml.namespace.QName;
013
014import dev.metaschema.core.metapath.StaticContext;
015import dev.metaschema.core.metapath.StaticMetapathException;
016import dev.metaschema.core.util.ObjectUtils;
017import edu.umd.cs.findbugs.annotations.NonNull;
018import edu.umd.cs.findbugs.annotations.Nullable;
019
020/**
021 * An efficient cache-backed representation of a qualified name.
022 * <p>
023 * This implementation uses an underlying integer-based cache to reduce the
024 * memory footprint of qualified names and namespaces by reusing instances with
025 * the same namespace and local name.
026 */
027public interface IEnhancedQName extends Comparable<IEnhancedQName> {
028  /**
029   * Get the index position of the qualified name.
030   * <p>
031   * This value can be used in place of this object. The object can be retrieved
032   * using this index with the {@link #of(int)} method.
033   *
034   * @return the index position
035   */
036  int getIndexPosition();
037
038  /**
039   * Get the namespace part of the qualified name.
040   *
041   * @return the namespace
042   */
043  @NonNull
044  String getNamespace();
045
046  /**
047   * Get the namespace part of the qualified name.
048   *
049   * @return the namespace as a URI
050   */
051  @NonNull
052  URI getNamespaceAsUri();
053
054  /**
055   * Get the local part of the qualified name.
056   *
057   * @return the local name
058   */
059  @NonNull
060  String getLocalName();
061
062  /**
063   * Get an existing qualified name by looking up the cached entry using the
064   * provided index value.
065   *
066   * @param index
067   *          the index value to lookup
068   * @return an optional containing the qualified name, if it exists
069   */
070  @NonNull
071  static Optional<IEnhancedQName> of(int index) {
072    return EQNameFactory.instance().get(index);
073  }
074
075  /**
076   * Get a qualified name using the provided {@link QName} value.
077   *
078   * @param qname
079   *          the qualified name to get
080   * @return the qualified name
081   */
082  @NonNull
083  static IEnhancedQName of(@NonNull QName qname) {
084    return of(
085        ObjectUtils.notNull(qname.getNamespaceURI()),
086        ObjectUtils.notNull(qname.getLocalPart()));
087  }
088
089  /**
090   * Get a qualified name using the provided local name value with no namespace.
091   *
092   * @param localName
093   *          the qualified name local part
094   * @return the qualified name
095   */
096  @NonNull
097  static IEnhancedQName of(@NonNull String localName) {
098    return of("", localName);
099  }
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}