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.AssertionUtils.*; 021import static org.apache.juneau.commons.utils.SystemUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023 024import java.util.*; 025import java.util.concurrent.*; 026import java.util.concurrent.atomic.*; 027 028import org.apache.juneau.commons.function.*; 029 030/** 031 * Simple in-memory cache for storing and retrieving objects by five-part keys. 032 * 033 * <h5 class='section'>Overview:</h5> 034 * <p> 035 * This class uses {@link java.util.concurrent.ConcurrentHashMap} internally to provide a thread-safe caching layer with automatic 036 * value computation, cache eviction, and statistics tracking for five-part composite keys. It's designed for 037 * caching expensive-to-compute or frequently-accessed objects indexed by five keys. 038 * 039 * <h5 class='section'>Features:</h5> 040 * <ul class='spaced-list'> 041 * <li>Thread-safe concurrent access without external synchronization 042 * <li>Five-part composite key support 043 * <li>Automatic cache eviction when maximum size is reached 044 * <li>Lazy computation via {@link Function5} supplier pattern 045 * <li>Default supplier support for simplified access 046 * <li>Built-in hit/miss statistics tracking 047 * <li>Optional logging of cache statistics on JVM shutdown 048 * <li>Can be disabled entirely via builder or system property 049 * </ul> 050 * 051 * <h5 class='section'>See Also:</h5> 052 * <ul> 053 * <li class='jc'>{@link Cache} 054 * <li class='jc'>{@link Cache2} 055 * <li class='jc'>{@link Cache3} 056 * <li class='jc'>{@link Cache4} 057 * <li class='jc'>{@link java.util.concurrent.ConcurrentHashMap} 058 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 059 * </ul> 060 * 061 * @param <K1> The first key type. 062 * @param <K2> The second key type. 063 * @param <K3> The third key type. 064 * @param <K4> The fourth key type. 065 * @param <K5> The fifth key type. 066 * @param <V> The value type. 067 */ 068public class Cache5<K1,K2,K3,K4,K5,V> { 069 070 /** 071 * Builder for creating configured {@link Cache5} instances. 072 * 073 * @param <K1> The first key type. 074 * @param <K2> The second key type. 075 * @param <K3> The third key type. 076 * @param <K4> The fourth key type. 077 * @param <K5> The fifth key type. 078 * @param <V> The value type. 079 */ 080 public static class Builder<K1,K2,K3,K4,K5,V> { 081 CacheMode cacheMode; 082 int maxSize; 083 String id; 084 boolean logOnExit; 085 boolean threadLocal; 086 Function5<K1,K2,K3,K4,K5,V> supplier; 087 088 Builder() { 089 cacheMode = env("juneau.cache.mode", CacheMode.FULL); 090 maxSize = env("juneau.cache.maxSize", 1000); 091 logOnExit = env("juneau.cache.logOnExit", false); 092 id = "Cache5"; 093 } 094 095 /** 096 * Builds a new {@link Cache5} instance with the configured settings. 097 * 098 * @return A new immutable {@link Cache5} instance. 099 */ 100 public Cache5<K1,K2,K3,K4,K5,V> build() { 101 return new Cache5<>(this); 102 } 103 104 /** 105 * Sets the caching mode for this cache. 106 * 107 * <p> 108 * Available modes: 109 * <ul> 110 * <li>{@link CacheMode#NONE NONE} - No caching (always invoke supplier) 111 * <li>{@link CacheMode#WEAK WEAK} - Weak caching (uses {@link WeakHashMap}) 112 * <li>{@link CacheMode#FULL FULL} - Full caching (uses {@link ConcurrentHashMap}, default) 113 * </ul> 114 * 115 * @param value The caching mode. 116 * @return This object for method chaining. 117 */ 118 public Builder<K1,K2,K3,K4,K5,V> cacheMode(CacheMode value) { 119 cacheMode = value; 120 return this; 121 } 122 123 /** 124 * Conditionally enables logging of cache statistics when the JVM exits. 125 * 126 * <p> 127 * When enabled, the cache will register a shutdown hook that logs the cache name, 128 * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness. 129 * 130 * @param value Whether to enable logging on exit. 131 * @param idValue The identifier to use in the log message. 132 * @return This object for method chaining. 133 */ 134 public Builder<K1,K2,K3,K4,K5,V> logOnExit(boolean value, String idValue) { 135 id = idValue; 136 logOnExit = value; 137 return this; 138 } 139 140 /** 141 * Enables logging of cache statistics when the JVM exits. 142 * 143 * @param value The identifier to use in the log message. 144 * @return This object for method chaining. 145 */ 146 public Builder<K1,K2,K3,K4,K5,V> logOnExit(String value) { 147 id = value; 148 logOnExit = true; 149 return this; 150 } 151 152 /** 153 * Specifies the maximum number of entries allowed in this cache. 154 * 155 * @param value The maximum number of cache entries. Must be positive. 156 * @return This object for method chaining. 157 */ 158 public Builder<K1,K2,K3,K4,K5,V> maxSize(int value) { 159 maxSize = value; 160 return this; 161 } 162 163 /** 164 * Specifies the default supplier function for computing values when keys are not found. 165 * 166 * @param value The default supplier function. Can be <jk>null</jk>. 167 * @return This object for method chaining. 168 */ 169 public Builder<K1,K2,K3,K4,K5,V> supplier(Function5<K1,K2,K3,K4,K5,V> value) { 170 supplier = value; 171 return this; 172 } 173 174 /** 175 * Enables thread-local caching. 176 * 177 * <p> 178 * When enabled, each thread gets its own separate cache instance. This is useful for 179 * thread-unsafe objects that need to be cached per thread. 180 * 181 * <p> 182 * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API. 183 * 184 * @return This object for method chaining. 185 * @see Cache.Builder#threadLocal() 186 */ 187 public Builder<K1,K2,K3,K4,K5,V> threadLocal() { 188 threadLocal = true; 189 return this; 190 } 191 192 /** 193 * Sets the caching mode to {@link CacheMode#WEAK WEAK}. 194 * 195 * <p> 196 * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>. 197 * 198 * <p> 199 * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be 200 * garbage collected when keys are no longer strongly referenced elsewhere. 201 * 202 * @return This object for method chaining. 203 * @see #cacheMode(CacheMode) 204 */ 205 public Builder<K1,K2,K3,K4,K5,V> weak() { 206 return cacheMode(WEAK); 207 } 208 } 209 210 /** 211 * Creates a new {@link Builder} for constructing a cache with explicit type parameters. 212 * 213 * <p> 214 * This variant allows you to specify the cache's generic types explicitly without passing 215 * the class objects, which is useful when working with complex parameterized types. 216 * 217 * @param <K1> The first key type. 218 * @param <K2> The second key type. 219 * @param <K3> The third key type. 220 * @param <K4> The fourth key type. 221 * @param <K5> The fifth key type. 222 * @param <V> The value type. 223 * @return A new builder for configuring the cache. 224 */ 225 public static <K1,K2,K3,K4,K5,V> Builder<K1,K2,K3,K4,K5,V> create() { 226 return new Builder<>(); 227 } 228 229 /** 230 * Creates a new {@link Builder} for constructing a cache. 231 * 232 * @param <K1> The first key type. 233 * @param <K2> The second key type. 234 * @param <K3> The third key type. 235 * @param <K4> The fourth key type. 236 * @param <K5> The fifth key type. 237 * @param <V> The value type. 238 * @param key1 The first key type class (used for type safety). 239 * @param key2 The second key type class (used for type safety). 240 * @param key3 The third key type class (used for type safety). 241 * @param key4 The fourth key type class (used for type safety). 242 * @param key5 The fifth key type class (used for type safety). 243 * @param type The value type class. 244 * @return A new builder for configuring the cache. 245 */ 246 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) { 247 return new Builder<>(); 248 } 249 250 // Internal map with Tuple5 keys for content-based equality (especially for arrays) 251 // If threadLocal is true, this is null and threadLocalMap is used instead 252 private final java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V> map; 253 254 private final ThreadLocal<java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V>> threadLocalMap; 255 256 private final boolean isThreadLocal; 257 258 private final int maxSize; 259 private final CacheMode cacheMode; 260 private final Function5<K1,K2,K3,K4,K5,V> supplier; 261 private final AtomicInteger cacheHits = new AtomicInteger(); 262 263 /** 264 * Constructor. 265 * 266 * @param builder The builder containing configuration settings. 267 */ 268 protected Cache5(Builder<K1,K2,K3,K4,K5,V> builder) { 269 this.maxSize = builder.maxSize; 270 this.cacheMode = builder.cacheMode; 271 this.supplier = builder.supplier; 272 this.isThreadLocal = builder.threadLocal; 273 274 if (isThreadLocal) { 275 // Thread-local mode: each thread gets its own map 276 if (builder.cacheMode == WEAK) { 277 this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>())); 278 } else { 279 this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>()); 280 } 281 this.map = null; 282 } else { 283 // Normal mode: shared map across all threads 284 if (builder.cacheMode == WEAK) { 285 this.map = Collections.synchronizedMap(new WeakHashMap<>()); 286 } else { 287 this.map = new ConcurrentHashMap<>(); 288 } 289 this.threadLocalMap = null; 290 } 291 if (builder.logOnExit) { 292 shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size()); 293 } 294 } 295 296 /** 297 * Removes all entries from the cache. 298 */ 299 public void clear() { 300 getMap().clear(); 301 } 302 303 /** 304 * Returns <jk>true</jk> if the cache contains a mapping for the specified five-part key. 305 * 306 * @param key1 The first key. 307 * @param key2 The second key. 308 * @param key3 The third key. 309 * @param key4 The fourth key. 310 * @param key5 The fifth key. 311 * @return <jk>true</jk> if the cache contains the five-part key. 312 */ 313 public boolean containsKey(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) { 314 return getMap().containsKey(Tuple5.of(key1, key2, key3, key4, key5)); 315 } 316 317 /** 318 * Returns <jk>true</jk> if the cache contains one or more entries with the specified value. 319 * 320 * @param value The value to check. 321 * @return <jk>true</jk> if the cache contains the value. 322 */ 323 public boolean containsValue(V value) { 324 // ConcurrentHashMap doesn't allow null values, so null can never be in the cache 325 if (value == null) 326 return false; 327 return getMap().containsValue(value); 328 } 329 330 /** 331 * Retrieves a cached value by five-part key using the default supplier. 332 * 333 * @param key1 First key component. Can be <jk>null</jk>. 334 * @param key2 Second key component. Can be <jk>null</jk>. 335 * @param key3 Third key component. Can be <jk>null</jk>. 336 * @param key4 Fourth key component. Can be <jk>null</jk>. 337 * @param key5 Fifth key component. Can be <jk>null</jk>. 338 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 339 * @throws NullPointerException if no default supplier was configured. 340 * 341 */ 342 public V get(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) { 343 return get(key1, key2, key3, key4, key5, () -> supplier.apply(key1, key2, key3, key4, key5)); 344 } 345 346 /** 347 * Retrieves a cached value by five-part key, computing it if necessary using the provided supplier. 348 * 349 * @param key1 First key component. Can be <jk>null</jk>. 350 * @param key2 Second key component. Can be <jk>null</jk>. 351 * @param key3 Third key component. Can be <jk>null</jk>. 352 * @param key4 Fourth key component. Can be <jk>null</jk>. 353 * @param key5 Fifth key component. Can be <jk>null</jk>. 354 * @param supplier The supplier to compute the value if it's not in the cache. Must not be <jk>null</jk>. 355 * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>. 356 * 357 */ 358 public V get(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5, java.util.function.Supplier<V> supplier) { 359 assertArgNotNull("supplier", supplier); 360 if (cacheMode == NONE) 361 return supplier.get(); 362 var m = getMap(); 363 var wrapped = Tuple5.of(key1, key2, key3, key4, key5); 364 V v = m.get(wrapped); 365 if (v == null) { 366 if (size() > maxSize) 367 clear(); 368 v = supplier.get(); 369 if (v == null) 370 m.remove(wrapped); 371 else 372 m.putIfAbsent(wrapped, v); 373 } else { 374 cacheHits.incrementAndGet(); 375 } 376 return v; 377 } 378 379 /** 380 * Returns the total number of cache hits since this cache was created. 381 * 382 * @return The total number of cache hits since creation. 383 */ 384 public int getCacheHits() { return cacheHits.get(); } 385 386 /** 387 * Returns <jk>true</jk> if the cache contains no entries. 388 * 389 * @return <jk>true</jk> if the cache is empty. 390 */ 391 public boolean isEmpty() { return getMap().isEmpty(); } 392 393 /** 394 * Associates the specified value with the specified five-part key. 395 * 396 * @param key1 The first key. 397 * @param key2 The second key. 398 * @param key3 The third key. 399 * @param key4 The fourth key. 400 * @param key5 The fifth key. 401 * @param value The value to associate with the five-part key. 402 * @return The previous value associated with the five-part key, or <jk>null</jk> if there was no mapping. 403 * 404 */ 405 public V put(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5, V value) { 406 var m = getMap(); 407 if (value == null) 408 return m.remove(Tuple5.of(key1, key2, key3, key4, key5)); 409 return m.put(Tuple5.of(key1, key2, key3, key4, key5), value); 410 } 411 412 /** 413 * Removes the entry for the specified five-part key from the cache. 414 * 415 * @param key1 The first key. 416 * @param key2 The second key. 417 * @param key3 The third key. 418 * @param key4 The fourth key. 419 * @param key5 The fifth key. 420 * @return The previous value associated with the five-part key, or <jk>null</jk> if there was no mapping. 421 * 422 */ 423 public V remove(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) { 424 return getMap().remove(Tuple5.of(key1, key2, key3, key4, key5)); 425 } 426 427 /** 428 * Returns the number of entries in the cache. 429 * 430 * @return The number of cached entries. 431 */ 432 public int size() { 433 return getMap().size(); 434 } 435 436 /** 437 * Gets the map for the current thread. 438 * 439 * @return The map for the current thread. 440 */ 441 private Map<Tuple5<K1,K2,K3,K4,K5>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; } 442}