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