1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.qname;
7   
8   import gov.nist.secauto.metaschema.core.util.ObjectUtils;
9   
10  import java.net.URI;
11  import java.util.Comparator;
12  import java.util.Map;
13  import java.util.Objects;
14  import java.util.Optional;
15  import java.util.concurrent.ConcurrentHashMap;
16  import java.util.concurrent.atomic.AtomicInteger;
17  
18  import edu.umd.cs.findbugs.annotations.NonNull;
19  import edu.umd.cs.findbugs.annotations.Nullable;
20  import nl.talsmasoftware.lazy4j.Lazy;
21  
22  /**
23   * Provides a cache for managing commonly reused qualified names, represented
24   * using the {@link IEnhancedQName} interface.
25   * <p>
26   * The cache operations provided by this cache are thread safe.
27   * <p>
28   * A unique integer index value is assigned for each namespace and localname
29   * pair. This index value can be used to retrieve the qualified name using the
30   * {@link #get(int)} method. This allows the index value to be used in place of
31   * the actual qualified name.
32   * <p>
33   * The {@link IEnhancedQName#getIndexPosition()} method can be used to get the
34   * index value of a given qualified name.
35   */
36  // FIXME: Consider the implications of this cache in a long running process.
37  // Using a global shared instance may result in a very large cache.
38  public final class QNameCache {
39  
40    private static final Comparator<IEnhancedQName> COMPARATOR
41        = Comparator.comparingInt(IEnhancedQName::getIndexPosition);
42  
43    @NonNull
44    private static final Lazy<QNameCache> INSTANCE = ObjectUtils.notNull(Lazy.of(QNameCache::new));
45  
46    @NonNull
47    private final NamespaceCache namespaceCache;
48  
49    private final Map<Integer, QNameRecord> indexToQName = new ConcurrentHashMap<>();
50    private final Map<Integer, Map<String, QNameRecord>> nsIndexToLocalNameToIndex = new ConcurrentHashMap<>();
51  
52    /**
53     * The next available qualified-name index position.
54     */
55    private final AtomicInteger indexCounter = new AtomicInteger();
56  
57    /**
58     * Get the singleton qualified name cache.
59     *
60     * @return the singleton instance
61     */
62    @NonNull
63    public static QNameCache instance() {
64      return ObjectUtils.notNull(INSTANCE.get());
65    }
66  
67    private QNameCache() {
68      // disable construction
69      this(NamespaceCache.instance());
70    }
71  
72    private QNameCache(@NonNull NamespaceCache nsCache) {
73      this.namespaceCache = nsCache;
74    }
75  
76    @NonNull
77    private NamespaceCache getNamespaceCache() {
78      return namespaceCache;
79    }
80  
81    /**
82     * Get a cached qualified name based on the provided namespace and name.
83     * <p>
84     * The qualified name will be added to the cache if it doesn't already exist.
85     *
86     * @param namespace
87     *          the namespace for the new qualified name
88     * @param name
89     *          the local name for the new qualified name
90     * @return the new cached qualified name or the existing cached name if it
91     *         already exists in the cache
92     */
93    @NonNull
94    public IEnhancedQName cachedQNameFor(@NonNull String namespace, @NonNull String name) {
95      int namespacePosition = namespaceCache.indexOf(namespace);
96  
97      Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex
98          .computeIfAbsent(namespacePosition, key -> new ConcurrentHashMap<>());
99  
100     return ObjectUtils.notNull(namespaceNames.computeIfAbsent(name, key -> {
101       assert key != null;
102       QNameRecord record = new QNameRecord(namespacePosition, namespace, key);
103       indexToQName.put(record.getIndexPosition(), record);
104       return record;
105     }));
106   }
107 
108   /**
109    * Get an existing qualified name from the cache based on the provided namespace
110    * and name.
111    *
112    * @param namespace
113    *          the namespace for the qualified name
114    * @param name
115    *          the local name for the qualified name
116    * @return an optional containing the cached qualified name or a {@code null}
117    *         value if the name does not exist in the cache
118    */
119   @NonNull
120   Optional<IEnhancedQName> get(@NonNull String namespace, @NonNull String name) {
121     Optional<Integer> nsPosition = namespaceCache.get(namespace);
122     if (!nsPosition.isPresent()) {
123       throw new IllegalArgumentException(
124           String.format("The namespace '%s' is not recognized.", namespace));
125     }
126 
127     Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex.get(nsPosition.get());
128     return ObjectUtils.notNull(Optional.ofNullable(
129         namespaceNames == null
130             ? null
131             : namespaceNames.get(name)));
132   }
133 
134   /**
135    * Get an existing qualified name from the cache that is assigned to the
136    * provided index value.
137    * <p>
138    * Note: There is a chance that an entry associated index value may not exist at
139    * first, but subsequent calls may find an associated value in the future if one
140    * is created with that value.
141    *
142    * @param index
143    *          the index value for the qualified name
144    * @return the cached qualified name or {@code null} if a cache entry does not
145    *         exist for the index value
146    */
147   @Nullable
148   public IEnhancedQName get(int index) {
149     return indexToQName.get(index);
150   }
151 
152   private final class QNameRecord implements IEnhancedQName {
153     private final int qnameIndexPosition;
154     private final int namespaceIndexPosition;
155     @NonNull
156     private final String namespace;
157     @NonNull
158     private final String localName;
159 
160     public QNameRecord(
161         int namespaceIndexPosition,
162         @NonNull String namespace,
163         @NonNull String localName) {
164       this.qnameIndexPosition = indexCounter.getAndIncrement();
165       this.namespaceIndexPosition = namespaceIndexPosition;
166       this.namespace = namespace;
167       this.localName = localName;
168     }
169 
170     @Override
171     public int getIndexPosition() {
172       return qnameIndexPosition;
173     }
174 
175     @Override
176     public URI getNamespaceAsUri() {
177       return ObjectUtils.notNull(getNamespaceCache().getAsURI(namespaceIndexPosition).get());
178     }
179 
180     @Override
181     public String getNamespace() {
182       return namespace;
183     }
184 
185     @Override
186     public String getLocalName() {
187       return localName;
188     }
189 
190     @Override
191     public int hashCode() {
192       return Objects.hashCode(qnameIndexPosition);
193     }
194 
195     @Override
196     public boolean equals(Object obj) {
197       if (this == obj) {
198         return true;
199       }
200       if (obj == null || getClass() != obj.getClass()) {
201         return false;
202       }
203       QNameRecord other = (QNameRecord) obj;
204       return Objects.equals(qnameIndexPosition, other.getIndexPosition());
205     }
206 
207     @Override
208     public int compareTo(IEnhancedQName other) {
209       return COMPARATOR.compare(this, other);
210     }
211 
212     @Override
213     public String toString() {
214       return toEQName();
215     }
216   }
217 
218 }