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.Comparator;
10  import java.util.Map;
11  import java.util.Objects;
12  import java.util.Optional;
13  import java.util.concurrent.ConcurrentHashMap;
14  import java.util.concurrent.atomic.AtomicInteger;
15  
16  import dev.metaschema.core.util.ObjectUtils;
17  import edu.umd.cs.findbugs.annotations.NonNull;
18  import edu.umd.cs.findbugs.annotations.Nullable;
19  import nl.talsmasoftware.lazy4j.Lazy;
20  
21  /**
22   * Provides a cache for managing commonly reused qualified names, represented
23   * using the {@link IEnhancedQName} interface.
24   * <p>
25   * The cache operations provided by this cache are thread safe.
26   * <p>
27   * A unique integer index value is assigned for each namespace and localname
28   * pair. This index value can be used to retrieve the qualified name using the
29   * {@link #get(int)} method. This allows the index value to be used in place of
30   * the actual qualified name.
31   * <p>
32   * The {@link IEnhancedQName#getIndexPosition()} method can be used to get the
33   * index value of a given qualified name.
34   */
35  // FIXME: Consider the implications of this cache in a long running process.
36  // Using a global shared instance may result in a very large cache.
37  public final class QNameCache {
38  
39    private static final Comparator<IEnhancedQName> COMPARATOR
40        = Comparator.comparingInt(IEnhancedQName::getIndexPosition);
41  
42    @NonNull
43    private static final Lazy<QNameCache> INSTANCE = ObjectUtils.notNull(Lazy.of(QNameCache::new));
44  
45    @NonNull
46    private final NamespaceCache namespaceCache;
47  
48    private final Map<Integer, QNameRecord> indexToQName = new ConcurrentHashMap<>();
49    private final Map<Integer, Map<String, QNameRecord>> nsIndexToLocalNameToIndex = new ConcurrentHashMap<>();
50  
51    /**
52     * The next available qualified-name index position.
53     */
54    private final AtomicInteger indexCounter = new AtomicInteger();
55  
56    /**
57     * Get the singleton qualified name cache.
58     *
59     * @return the singleton instance
60     */
61    @NonNull
62    public static QNameCache instance() {
63      return ObjectUtils.notNull(INSTANCE.get());
64    }
65  
66    private QNameCache() {
67      // disable construction
68      this(NamespaceCache.instance());
69    }
70  
71    private QNameCache(@NonNull NamespaceCache nsCache) {
72      this.namespaceCache = nsCache;
73    }
74  
75    @NonNull
76    private NamespaceCache getNamespaceCache() {
77      return namespaceCache;
78    }
79  
80    /**
81     * Get a cached qualified name based on the provided namespace and name.
82     * <p>
83     * The qualified name will be added to the cache if it doesn't already exist.
84     *
85     * @param namespace
86     *          the namespace for the new qualified name
87     * @param name
88     *          the local name for the new qualified name
89     * @return the new cached qualified name or the existing cached name if it
90     *         already exists in the cache
91     */
92    @NonNull
93    public IEnhancedQName cachedQNameFor(@NonNull String namespace, @NonNull String name) {
94      int namespacePosition = namespaceCache.indexOf(namespace);
95  
96      Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex
97          .computeIfAbsent(namespacePosition, key -> new ConcurrentHashMap<>());
98  
99      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 }