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}