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