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.lazy(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    @SuppressWarnings("PMD.ShortMethodName")
94    @NonNull
95    public IEnhancedQName cachedQNameFor(@NonNull String namespace, @NonNull String name) {
96      int namespacePosition = namespaceCache.indexOf(namespace);
97  
98      Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex
99          .computeIfAbsent(namespacePosition, key -> new ConcurrentHashMap<>());
100 
101     return ObjectUtils.notNull(namespaceNames.computeIfAbsent(name, key -> {
102       assert key != null;
103       QNameRecord record = new QNameRecord(namespacePosition, namespace, key);
104       indexToQName.put(record.getIndexPosition(), record);
105       return record;
106     }));
107   }
108 
109   /**
110    * Get an existing qualified name from the cache based on the provided namespace
111    * and name.
112    *
113    * @param namespace
114    *          the namespace for the qualified name
115    * @param name
116    *          the local name for the qualified name
117    * @return an optional containing the cached qualified name or a {@code null}
118    *         value if the name does not exist in the cache
119    */
120   @NonNull
121   Optional<IEnhancedQName> get(@NonNull String namespace, @NonNull String name) {
122     Optional<Integer> nsPosition = namespaceCache.get(namespace);
123     if (!nsPosition.isPresent()) {
124       throw new IllegalArgumentException(
125           String.format("The namespace '%s' is not recognized.", namespace));
126     }
127 
128     Map<String, QNameRecord> namespaceNames = nsIndexToLocalNameToIndex.get(nsPosition.get());
129     return ObjectUtils.notNull(Optional.ofNullable(
130         namespaceNames == null
131             ? null
132             : namespaceNames.get(name)));
133   }
134 
135   /**
136    * Get an existing qualified name from the cache that is assigned to the
137    * provided index value.
138    * <p>
139    * Note: There is a chance that an entry associated index value may not exist at
140    * first, but subsequent calls may find an associated value in the future if one
141    * is created with that value.
142    *
143    * @param index
144    *          the index value for the qualified name
145    * @return the cached qualified name or {@code null} if a cache entry does not
146    *         exist for the index value
147    */
148   @Nullable
149   public IEnhancedQName get(int index) {
150     return indexToQName.get(index);
151   }
152 
153   private final class QNameRecord implements IEnhancedQName {
154     private final int qnameIndexPosition;
155     private final int namespaceIndexPosition;
156     @NonNull
157     private final String namespace;
158     @NonNull
159     private final String localName;
160 
161     public QNameRecord(
162         int namespaceIndexPosition,
163         @NonNull String namespace,
164         @NonNull String localName) {
165       this.qnameIndexPosition = indexCounter.getAndIncrement();
166       this.namespaceIndexPosition = namespaceIndexPosition;
167       this.namespace = namespace;
168       this.localName = localName;
169     }
170 
171     @Override
172     public int getIndexPosition() {
173       return qnameIndexPosition;
174     }
175 
176     @Override
177     public URI getNamespaceAsUri() {
178       return ObjectUtils.notNull(getNamespaceCache().getAsURI(namespaceIndexPosition).get());
179     }
180 
181     @Override
182     public String getNamespace() {
183       return namespace;
184     }
185 
186     @Override
187     public String getLocalName() {
188       return localName;
189     }
190 
191     @Override
192     public int hashCode() {
193       return Objects.hashCode(qnameIndexPosition);
194     }
195 
196     @SuppressWarnings("PMD.OnlyOneReturn")
197     @Override
198     public boolean equals(Object obj) {
199       if (this == obj) {
200         return true;
201       }
202       if (obj == null || getClass() != obj.getClass()) {
203         return false;
204       }
205       QNameRecord other = (QNameRecord) obj;
206       return Objects.equals(qnameIndexPosition, other.getIndexPosition());
207     }
208 
209     @Override
210     public int compareTo(IEnhancedQName other) {
211       return COMPARATOR.compare(this, other);
212     }
213 
214     @Override
215     public String toString() {
216       return toEQName();
217     }
218   }
219 
220 }