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