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