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.Comparator;
010import java.util.Map;
011import java.util.Objects;
012import java.util.Optional;
013import java.util.concurrent.ConcurrentHashMap;
014import java.util.concurrent.atomic.AtomicInteger;
015
016import dev.metaschema.core.util.ObjectUtils;
017import edu.umd.cs.findbugs.annotations.NonNull;
018import edu.umd.cs.findbugs.annotations.Nullable;
019import nl.talsmasoftware.lazy4j.Lazy;
020
021/**
022 * Provides a cache for managing commonly reused qualified names, represented
023 * using the {@link IEnhancedQName} interface.
024 * <p>
025 * The cache operations provided by this cache are thread safe.
026 * <p>
027 * A unique integer index value is assigned for each namespace and localname
028 * pair. This index value can be used to retrieve the qualified name using the
029 * {@link #get(int)} method. This allows the index value to be used in place of
030 * the actual qualified name.
031 * <p>
032 * The {@link IEnhancedQName#getIndexPosition()} method can be used to get the
033 * index value of a given qualified name.
034 */
035// FIXME: Consider the implications of this cache in a long running process.
036// Using a global shared instance may result in a very large cache.
037public final class QNameCache {
038
039  private static final Comparator<IEnhancedQName> COMPARATOR
040      = Comparator.comparingInt(IEnhancedQName::getIndexPosition);
041
042  @NonNull
043  private static final Lazy<QNameCache> INSTANCE = ObjectUtils.notNull(Lazy.of(QNameCache::new));
044
045  @NonNull
046  private final NamespaceCache namespaceCache;
047
048  private final Map<Integer, QNameRecord> indexToQName = new ConcurrentHashMap<>();
049  private final Map<Integer, Map<String, QNameRecord>> nsIndexToLocalNameToIndex = new ConcurrentHashMap<>();
050
051  /**
052   * The next available qualified-name index position.
053   */
054  private final AtomicInteger indexCounter = new AtomicInteger();
055
056  /**
057   * Get the singleton qualified name cache.
058   *
059   * @return the singleton instance
060   */
061  @NonNull
062  public static QNameCache instance() {
063    return ObjectUtils.notNull(INSTANCE.get());
064  }
065
066  private QNameCache() {
067    // disable construction
068    this(NamespaceCache.instance());
069  }
070
071  private QNameCache(@NonNull NamespaceCache nsCache) {
072    this.namespaceCache = nsCache;
073  }
074
075  @NonNull
076  private NamespaceCache getNamespaceCache() {
077    return namespaceCache;
078  }
079
080  /**
081   * Get a cached qualified name based on the provided namespace and name.
082   * <p>
083   * The qualified name will be added to the cache if it doesn't already exist.
084   *
085   * @param namespace
086   *          the namespace for the new qualified name
087   * @param name
088   *          the local name for the new qualified name
089   * @return the new cached qualified name or the existing cached name if it
090   *         already exists in the cache
091   */
092  @NonNull
093  public IEnhancedQName cachedQNameFor(@NonNull String namespace, @NonNull String name) {
094    int namespacePosition = namespaceCache.indexOf(namespace);
095
096    Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex
097        .computeIfAbsent(namespacePosition, key -> new ConcurrentHashMap<>());
098
099    return ObjectUtils.notNull(namespaceNames.computeIfAbsent(name, key -> {
100      assert key != null;
101      QNameRecord record = new QNameRecord(namespacePosition, namespace, key);
102      indexToQName.put(record.getIndexPosition(), record);
103      return record;
104    }));
105  }
106
107  /**
108   * Get an existing qualified name from the cache based on the provided namespace
109   * and name.
110   *
111   * @param namespace
112   *          the namespace for the qualified name
113   * @param name
114   *          the local name for the qualified name
115   * @return an optional containing the cached qualified name or a {@code null}
116   *         value if the name does not exist in the cache
117   */
118  @NonNull
119  Optional<IEnhancedQName> get(@NonNull String namespace, @NonNull String name) {
120    Optional<Integer> nsPosition = namespaceCache.get(namespace);
121    if (!nsPosition.isPresent()) {
122      throw new IllegalArgumentException(
123          String.format("The namespace '%s' is not recognized.", namespace));
124    }
125
126    Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex.get(nsPosition.get());
127    return ObjectUtils.notNull(Optional.ofNullable(
128        namespaceNames == null
129            ? null
130            : namespaceNames.get(name)));
131  }
132
133  /**
134   * Get an existing qualified name from the cache that is assigned to the
135   * provided index value.
136   * <p>
137   * Note: There is a chance that an entry associated index value may not exist at
138   * first, but subsequent calls may find an associated value in the future if one
139   * is created with that value.
140   *
141   * @param index
142   *          the index value for the qualified name
143   * @return the cached qualified name or {@code null} if a cache entry does not
144   *         exist for the index value
145   */
146  @Nullable
147  public IEnhancedQName get(int index) {
148    return indexToQName.get(index);
149  }
150
151  private final class QNameRecord implements IEnhancedQName {
152    private final int qnameIndexPosition;
153    private final int namespaceIndexPosition;
154    @NonNull
155    private final String namespace;
156    @NonNull
157    private final String localName;
158
159    public QNameRecord(
160        int namespaceIndexPosition,
161        @NonNull String namespace,
162        @NonNull String localName) {
163      this.qnameIndexPosition = indexCounter.getAndIncrement();
164      this.namespaceIndexPosition = namespaceIndexPosition;
165      this.namespace = namespace;
166      this.localName = localName;
167    }
168
169    @Override
170    public int getIndexPosition() {
171      return qnameIndexPosition;
172    }
173
174    @Override
175    public URI getNamespaceAsUri() {
176      return ObjectUtils.notNull(getNamespaceCache().getAsURI(namespaceIndexPosition).get());
177    }
178
179    @Override
180    public String getNamespace() {
181      return namespace;
182    }
183
184    @Override
185    public String getLocalName() {
186      return localName;
187    }
188
189    @Override
190    public int hashCode() {
191      return Objects.hashCode(qnameIndexPosition);
192    }
193
194    @Override
195    public boolean equals(Object obj) {
196      if (this == obj) {
197        return true;
198      }
199      if (obj == null || getClass() != obj.getClass()) {
200        return false;
201      }
202      QNameRecord other = (QNameRecord) obj;
203      return Objects.equals(qnameIndexPosition, other.getIndexPosition());
204    }
205
206    @Override
207    public int compareTo(IEnhancedQName other) {
208      return COMPARATOR.compare(this, other);
209    }
210
211    @Override
212    public String toString() {
213      return toEQName();
214    }
215  }
216
217}