001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019import static org.apache.juneau.commons.collections.CacheMode.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.SystemUtils.*; 022 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.concurrent.atomic.*; 026 027import org.apache.juneau.commons.function.*; 028 029/** 030 * Simple in-memory cache for storing and retrieving objects by three-part keys. 031 * 032 * <h5 class='section'>Overview:</h5> 033 * <p> 034 * This class uses {@link ConcurrentHashMap} internally to provide a thread-safe caching layer with automatic 035 * value computation, cache eviction, and statistics tracking for three-part composite keys. It's designed for 036 * caching expensive-to-compute or frequently-accessed objects indexed by three keys. 037 * 038 * <h5 class='section'>Features:</h5> 039 * <ul class='spaced-list'> 040 * <li>Thread-safe concurrent access without external synchronization 041 * <li>Three-part composite key support 042 * <li>Automatic cache eviction when maximum size is reached 043 * <li>Lazy computation via {@link Function3} supplier pattern 044 * <li>Default supplier support for simplified access 045 * <li>Built-in hit/miss statistics tracking 046 * <li>Optional logging of cache statistics on JVM shutdown 047 * <li>Can be disabled entirely via builder or system property 048 * </ul> 049 * 050 * <h5 class='section'>WEAK Mode Semantics:</h5> 051 * <p> 052 * {@link CacheMode#WEAK} on this class is <b>best-effort</b>, not retention-guaranteed. Each lookup synthesizes 053 * an internal {@code Tuple3} composite key that is never exposed to the caller, so that key is reachable only 054 * from this cache's own backing {@link WeakHashMap}. Consequently a WEAK-mode entry becomes GC-eligible 055 * <b>immediately</b> after insertion and may be reclaimed at the very next garbage collection - effectively 056 * behaving like {@link CacheMode#NONE} once any GC has occurred. This differs from the single-key {@link Cache}, 057 * where WEAK mode keys directly on the caller's own key and therefore retains an entry for as long as that key 058 * remains strongly reachable elsewhere. If guaranteed retention is required, use {@link CacheMode#FULL} (the 059 * default mode) instead of WEAK. See {@link CacheMode#WEAK} for details. 060 * 061 * <h5 class='section'>Usage:</h5> 062 * <p class='bjava'> 063 * <jc>// Create a cache with default supplier</jc> 064 * Cache3<String,String,String,Translation> <jv>translationCache</jv> = Cache3 065 * .<jsm>of</jsm>(String.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>, Translation.<jk>class</jk>) 066 * .maxSize(500) 067 * .supplier((lang, country, key) -> loadTranslation(lang, country, key)) 068 * .build(); 069 * 070 * <jc>// Retrieve using default supplier</jc> 071 * Translation <jv>t1</jv> = <jv>translationCache</jv>.get(<js>"en"</js>, <js>"US"</js>, <js>"greeting"</js>); 072 * 073 * <jc>// Or override the supplier</jc> 074 * Translation <jv>t2</jv> = <jv>translationCache</jv>.get(<js>"fr"</js>, <js>"FR"</js>, <js>"greeting"</js>, () -> customTranslation); 075 * </p> 076 * 077 * <h5 class='section'>See Also:</h5> 078 * <ul> 079 * <li class='jc'>{@link Cache} 080 * <li class='jc'>{@link Cache2} 081 * <li class='jc'>{@link ConcurrentHashMap} 082 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 083 * </ul> 084 * 085 * @param <K1> The first key type. 086 * @param <K2> The second key type. 087 * @param <K3> The third key type. 088 * @param <V> The value type. 089 */ 090@SuppressWarnings({ 091 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 092}) 093public class Cache3<K1,K2,K3,V> { 094 095 /** 096 * Builder for creating configured {@link Cache3} instances. 097 * 098 * @param <K1> The first key type. 099 * @param <K2> The second key type. 100 * @param <K3> The third key type. 101 * @param <V> The value type. 102 */ 103 public static class Builder<K1,K2,K3,V> { 104 105 // Resolved once per JVM. Looking these up per builder is expensive because Settings iterates every 106 // registered PropertySource (including the manifest scanner), and Cache builders are created very frequently. 107 private static final CacheMode DEFAULT_CACHE_MODE = env("juneau.cache.mode", CacheMode.FULL); 108 private static final int DEFAULT_MAX_SIZE = env("juneau.cache.maxSize", 1000); 109 private static final boolean DEFAULT_LOG_ON_EXIT = env("juneau.cache.logOnExit", false); 110 111 CacheMode cacheMode; 112 int maxSize; 113 String id; 114 boolean logOnExit; 115 boolean threadLocal; 116 Function3<K1,K2,K3,V> supplier; 117 118 Builder() { 119 cacheMode = DEFAULT_CACHE_MODE; 120 maxSize = DEFAULT_MAX_SIZE; 121 logOnExit = DEFAULT_LOG_ON_EXIT; 122 id = "Cache3"; 123 } 124 125 /** 126 * Builds a new {@link Cache3} instance with the configured settings. 127 * 128 * @return A new immutable {@link Cache3} instance. 129 */ 130 public Cache3<K1,K2,K3,V> build() { 131 return new Cache3<>(this); 132 } 133 134 /** 135 * Sets the caching mode for this cache. 136 * 137 * <p> 138 * Available modes: 139 * <ul> 140 * <li>{@link CacheMode#NONE NONE} - No caching (always invoke supplier) 141 * <li>{@link CacheMode#WEAK WEAK} - Best-effort weak-key caching (uses {@link WeakHashMap}); on this 142 * composite-key cache an entry may be reclaimed at <b>any</b> GC, not only when the caller's keys 143 * become unreachable - see {@link CacheMode#WEAK} for why 144 * <li>{@link CacheMode#FULL FULL} - Full caching (uses {@link ConcurrentHashMap}, default); use this 145 * mode for guaranteed retention 146 * </ul> 147 * 148 * @param value The caching mode. 149 * @return This object for method chaining. 150 */ 151 public Builder<K1,K2,K3,V> cacheMode(CacheMode value) { 152 cacheMode = value; 153 return this; 154 } 155 156 /** 157 * Conditionally enables logging of cache statistics when the JVM exits. 158 * 159 * <p> 160 * When enabled, the cache will register a shutdown hook that logs the cache name, 161 * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness. 162 * 163 * @param value Whether to enable logging on exit. 164 * @param idValue The identifier to use in the log message. 165 * @return This object for method chaining. 166 */ 167 public Builder<K1,K2,K3,V> logOnExit(boolean value, String idValue) { 168 id = idValue; 169 logOnExit = value; 170 return this; 171 } 172 173 /** 174 * Enables logging of cache statistics when the JVM exits. 175 * 176 * @param value The identifier to use in the log message. 177 * @return This object for method chaining. 178 */ 179 public Builder<K1,K2,K3,V> logOnExit(String value) { 180 id = value; 181 logOnExit = true; 182 return this; 183 } 184 185 /** 186 * Specifies the maximum number of entries allowed in this cache. 187 * 188 * @param value The maximum number of cache entries. Must be positive. 189 * @return This object for method chaining. 190 */ 191 public Builder<K1,K2,K3,V> maxSize(int value) { 192 maxSize = value; 193 return this; 194 } 195 196 /** 197 * Specifies the default supplier function for computing values when keys are not found. 198 * 199 * @param value The default supplier function. Can be <jk>null</jk>, in which case {@link Cache3#get(Object,Object,Object)} will throw a {@link NullPointerException} (use {@link Cache3#get(Object, Object, Object, java.util.function.Supplier)} instead to supply one per call). 200 * @return This object for method chaining. 201 */ 202 public Builder<K1,K2,K3,V> supplier(Function3<K1,K2,K3,V> value) { 203 supplier = value; 204 return this; 205 } 206 207 /** 208 * Enables thread-local caching. 209 * 210 * <p> 211 * When enabled, each thread gets its own separate cache instance. This is useful for 212 * thread-unsafe objects that need to be cached per thread. 213 * 214 * <p> 215 * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API. 216 * 217 * @return This object for method chaining. 218 * @see Cache.Builder#threadLocal() 219 */ 220 public Builder<K1,K2,K3,V> threadLocal() { 221 threadLocal = true; 222 return this; 223 } 224 225 /** 226 * Sets the caching mode to {@link CacheMode#WEAK WEAK}. 227 * 228 * <p> 229 * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>. 230 * 231 * <p> 232 * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be 233 * garbage collected when keys are no longer strongly referenced elsewhere. 234 * 235 * <p> 236 * <b>Note:</b> on this composite-key cache, WEAK is best-effort only - the internal composite key is 237 * never caller-visible, so an entry may be reclaimed at any GC. See {@link CacheMode#WEAK} for details, 238 * and use {@link CacheMode#FULL} instead if guaranteed retention is required. 239 * 240 * @return This object for method chaining. 241 * @see #cacheMode(CacheMode) 242 */ 243 public Builder<K1,K2,K3,V> weak() { 244 return cacheMode(WEAK); 245 } 246 } 247 248 /** 249 * Creates a new {@link Builder} for constructing a cache with explicit type parameters. 250 * 251 * <p> 252 * This variant allows you to specify the cache's generic types explicitly without passing 253 * the class objects, which is useful when working with complex parameterized types. 254 * 255 * @param <K1> The first key type. 256 * @param <K2> The second key type. 257 * @param <K3> The third key type. 258 * @param <V> The value type. 259 * @return A new builder for configuring the cache. 260 */ 261 public static <K1,K2,K3,V> Builder<K1,K2,K3,V> create() { 262 return new Builder<>(); 263 } 264 265 /** 266 * Creates a new {@link Builder} for constructing a cache. 267 * 268 * @param <K1> The first key type. 269 * @param <K2> The second key type. 270 * @param <K3> The third key type. 271 * @param <V> The value type. 272 * @param key1 The first key type class (used for type safety). 273 * @param key2 The second key type class (used for type safety). 274 * @param key3 The third key type class (used for type safety). 275 * @param type The value type class. 276 * @return A new builder for configuring the cache. 277 */ 278 @SuppressWarnings({ 279 "java:S1172" // Parameters required for type inference in public API 280 }) 281 public static <K1,K2,K3,V> Builder<K1,K2,K3,V> of(Class<K1> key1, Class<K2> key2, Class<K3> key3, Class<V> type) { 282 return new Builder<>(); 283 } 284 285 // Internal map with Tuple3 keys for content-based equality (especially for arrays) 286 // If threadLocal is true, this is null and threadLocalMap is used instead 287 private final Map<Tuple3<K1,K2,K3>,V> map; 288 289 @SuppressWarnings({ 290 "java:S5164" // Cleanup method provided: cleanup() 291 }) 292 private final ThreadLocal<Map<Tuple3<K1,K2,K3>,V>> threadLocalMap; 293 294 private final boolean isThreadLocal; 295 296 private final int maxSize; 297 private final CacheMode cacheMode; 298 private final Function3<K1,K2,K3,V> supplier; 299 private final AtomicInteger cacheHits = new AtomicInteger(); 300 301 /** 302 * Constructor. 303 * 304 * @param builder The builder containing configuration settings. 305 */ 306 protected Cache3(Builder<K1,K2,K3,V> builder) { 307 this.maxSize = builder.maxSize; 308 this.cacheMode = builder.cacheMode; 309 this.supplier = builder.supplier; 310 this.isThreadLocal = builder.threadLocal; 311 312 if (isThreadLocal) { 313 // Thread-local mode: each thread gets its own map 314 if (builder.cacheMode == WEAK) { 315 this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>())); 316 } else { 317 this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>()); 318 } 319 this.map = null; 320 } else { 321 // Normal mode: shared map across all threads 322 if (builder.cacheMode == WEAK) { 323 this.map = Collections.synchronizedMap(new WeakHashMap<>()); 324 } else { 325 this.map = new ConcurrentHashMap<>(); 326 } 327 this.threadLocalMap = null; 328 } 329 if (builder.logOnExit) { 330 shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size()); 331 } 332 } 333 334 /** 335 * Removes all entries from the cache. 336 */ 337 public void clear() { 338 getMap().clear(); 339 } 340 341 /** 342 * Cleans up thread-local storage for the current thread. 343 * 344 * <p> 345 * This method should be called when a thread is finished using this cache to prevent memory leaks 346 * in thread pool environments where threads are reused. 347 * Only has an effect if this cache was created with thread-local mode enabled. 348 */ 349 public void cleanup() { 350 if (isThreadLocal && threadLocalMap != null) { 351 threadLocalMap.remove(); 352 } 353 } 354 355 /** 356 * Returns <jk>true</jk> if the cache contains a mapping for the specified key triplet. 357 * 358 * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key). 359 * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key). 360 * @param key3 The third key. Can be <jk>null</jk> (null keys are supported like any other key). 361 * @return <jk>true</jk> if the cache contains the key triplet. 362 */ 363 public boolean containsKey(K1 key1, K2 key2, K3 key3) { 364 return getMap().containsKey(Tuple3.of(key1, key2, key3)); 365 } 366 367 /** 368 * Returns <jk>true</jk> if the cache contains one or more entries with the specified value. 369 * 370 * @param value The value to check. Can be <jk>null</jk>, in which case this method returns <jk>false</jk> (the cache never stores <jk>null</jk> values). 371 * @return <jk>true</jk> if the cache contains the value. 372 */ 373 public boolean containsValue(V value) { 374 // ConcurrentHashMap doesn't allow null values, so null can never be in the cache 375 if (value == null) 376 return false; 377 return getMap().containsValue(value); 378 } 379 380 /** 381 * Retrieves a cached value by key triplet using the default supplier. 382 * 383 * @param key1 First key component. Can be <jk>null</jk> (null keys are supported like any other key). 384 * @param key2 Second key component. Can be <jk>null</jk> (null keys are supported like any other key). 385 * @param key3 Third key component. Can be <jk>null</jk> (null keys are supported like any other key). 386 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 387 * @throws NullPointerException if no default supplier was configured. 388 */ 389 public V get(K1 key1, K2 key2, K3 key3) { 390 return get(key1, key2, key3, () -> supplier.apply(key1, key2, key3)); 391 } 392 393 /** 394 * Retrieves a cached value by key triplet, computing it if necessary using the provided supplier. 395 * 396 * @param key1 First key component. Can be <jk>null</jk> (null keys are supported like any other key). 397 * @param key2 Second key component. Can be <jk>null</jk> (null keys are supported like any other key). 398 * @param key3 Third key component. Can be <jk>null</jk> (null keys are supported like any other key). 399 * @param supplier The supplier to compute the value if it's not in the cache. Must not be <jk>null</jk> or an {@link IllegalArgumentException} is thrown. 400 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 401 */ 402 public V get(K1 key1, K2 key2, K3 key3, java.util.function.Supplier<V> supplier) { 403 reqnn("supplier", supplier); 404 if (cacheMode == NONE) 405 return supplier.get(); 406 Map<Tuple3<K1,K2,K3>,V> m = getMap(); 407 var wrapped = Tuple3.of(key1, key2, key3); 408 V v = m.get(wrapped); 409 if (v == null) { 410 if (size() > maxSize) 411 clear(); 412 v = supplier.get(); 413 if (v == null) 414 m.remove(wrapped); 415 else 416 m.putIfAbsent(wrapped, v); 417 } else { 418 cacheHits.incrementAndGet(); 419 } 420 return v; 421 } 422 423 /** 424 * Returns the total number of cache hits since this cache was created. 425 * 426 * @return The total number of cache hits since creation. 427 */ 428 public int getCacheHits() { return cacheHits.get(); } 429 430 /** 431 * Returns <jk>true</jk> if the cache contains no entries. 432 * 433 * @return <jk>true</jk> if the cache is empty. 434 */ 435 public boolean isEmpty() { return getMap().isEmpty(); } 436 437 /** 438 * Associates the specified value with the specified key triplet. 439 * 440 * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key). 441 * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key). 442 * @param key3 The third key. Can be <jk>null</jk> (null keys are supported like any other key). 443 * @param value The value to associate with the key triplet. 444 * @return The previous value associated with the key triplet, or <jk>null</jk> if there was no mapping. 445 */ 446 public V put(K1 key1, K2 key2, K3 key3, V value) { 447 Map<Tuple3<K1,K2,K3>,V> m = getMap(); 448 if (value == null) 449 return m.remove(Tuple3.of(key1, key2, key3)); 450 return m.put(Tuple3.of(key1, key2, key3), value); 451 } 452 453 /** 454 * Removes the entry for the specified key triplet from the cache. 455 * 456 * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key). 457 * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key). 458 * @param key3 The third key. Can be <jk>null</jk> (null keys are supported like any other key). 459 * @return The previous value associated with the key triplet, or <jk>null</jk> if there was no mapping. 460 */ 461 public V remove(K1 key1, K2 key2, K3 key3) { 462 return getMap().remove(Tuple3.of(key1, key2, key3)); 463 } 464 465 /** 466 * Returns the number of entries in the cache. 467 * 468 * @return The number of cached entries. 469 */ 470 public int size() { 471 return getMap().size(); 472 } 473 474 /** 475 * Gets the map for the current thread. 476 * 477 * @return The map for the current thread. 478 */ 479 private Map<Tuple3<K1,K2,K3>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; } 480}