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 two-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 two-part composite keys. It's designed for
036 * caching expensive-to-compute or frequently-accessed objects indexed by two keys.
037 *
038 * <h5 class='section'>Features:</h5>
039 * <ul class='spaced-list'>
040 *    <li>Thread-safe concurrent access without external synchronization
041 *    <li>Two-part composite key support
042 *    <li>Automatic cache eviction when maximum size is reached
043 *    <li>Lazy computation via {@link Function2} 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 Tuple2} 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'>Usage:</h5>
062 * <p class='bjava'>
063 *    <jc>// Create a cache with default supplier</jc>
064 *    Cache2&lt;ClassLoader,Class&lt;?&gt;,ClassMeta&gt; <jv>metaCache</jv> = Cache2
065 *       .<jsm>of</jsm>(ClassLoader.<jk>class</jk>, Class.<jk>class</jk>, ClassMeta.<jk>class</jk>)
066 *       .maxSize(500)
067 *       .supplier((cl, c) -&gt; createClassMeta(cl, c))
068 *       .build();
069 *
070 *    <jc>// Retrieve using default supplier</jc>
071 *    ClassMeta <jv>meta1</jv> = <jv>metaCache</jv>.get(<jv>classLoader</jv>, String.<jk>class</jk>);
072 *
073 *    <jc>// Or override the supplier</jc>
074 *    ClassMeta <jv>meta2</jv> = <jv>metaCache</jv>.get(<jv>classLoader</jv>, Integer.<jk>class</jk>, () -&gt; customMeta);
075 * </p>
076 *
077 * <h5 class='section'>Cache Behavior:</h5>
078 * <ul class='spaced-list'>
079 *    <li>When a key pair is requested:
080 *       <ul>
081 *          <li>If the key exists in the cache, the cached value is returned (cache hit)
082 *          <li>If the key doesn't exist, the supplier is invoked to compute the value
083 *          <li>The computed value is stored in the cache and returned (cache miss)
084 *       </ul>
085 *    <li>When the cache exceeds {@link Builder#maxSize(int)}, the entire cache is cleared
086 *    <li>If the cache is disabled, the supplier is always invoked without caching
087 *    <li>Null keys always bypass the cache and invoke the supplier
088 * </ul>
089 *
090 * <h5 class='section'>Environment Variables:</h5>
091 * <p>
092 * The following system properties can be used to configure default cache behavior:
093 * <ul class='spaced-list'>
094 *    <li><c>juneau.cache.mode</c> - Cache mode: NONE/WEAK/FULL (default: FULL, case-insensitive)
095 *    <li><c>juneau.cache.maxSize</c> - Maximum cache size before eviction (default: 1000)
096 *    <li><c>juneau.cache.logOnExit</c> - Log cache statistics on shutdown (default: <jk>false</jk>)
097 * </ul>
098 *
099 * <h5 class='section'>Thread Safety:</h5>
100 * <p>
101 * This class is thread-safe and can be safely used from multiple threads without external synchronization.
102 * However, note that when the cache is cleared due to exceeding max size, there's a small window where
103 * multiple threads might compute the same value. This is acceptable for most use cases as it only affects
104 * performance, not correctness.
105 *
106 * <h5 class='section'>Performance Considerations:</h5>
107 * <ul class='spaced-list'>
108 *    <li>Cache operations are O(1) average time complexity
109 *    <li>The {@link #get(Object, Object, java.util.function.Supplier)} method uses
110 *       {@link ConcurrentHashMap#putIfAbsent(Object, Object)}
111 *       to minimize redundant computation in concurrent scenarios
112 *    <li>When max size is exceeded, the entire cache is cleared in a single operation
113 *    <li>Statistics tracking uses {@link AtomicInteger} for thread-safe counting without locking
114 * </ul>
115 *
116 * <h5 class='section'>Examples:</h5>
117 * <p class='bjava'>
118 *    <jc>// Simple cache with defaults</jc>
119 *    Cache2&lt;String,String,Config&gt; <jv>cache</jv> = Cache2.<jsm>of</jsm>(String.<jk>class</jk>, String.<jk>class</jk>, Config.<jk>class</jk>).build();
120 *
121 *    <jc>// Cache with custom configuration</jc>
122 *    Cache2&lt;String,Integer,User&gt; <jv>userCache</jv> = Cache2
123 *       .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>)
124 *       .maxSize(500)
125 *       .logOnExit()
126 *       .supplier((tenant, id) -&gt; userService.findUser(tenant, id))
127 *       .build();
128 *
129 *    <jc>// Disabled cache for testing</jc>
130 *    Cache2&lt;String,String,Object&gt; <jv>disabledCache</jv> = Cache2
131 *       .<jsm>of</jsm>(String.<jk>class</jk>, String.<jk>class</jk>, Object.<jk>class</jk>)
132 *       .disableCaching()
133 *       .build();
134 * </p>
135 *
136 * <h5 class='section'>See Also:</h5>
137 * <ul>
138 *    <li class='jc'>{@link Cache}
139 *    <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview &gt; juneau-commons</a>
140 * </ul>
141 *
142 * @param <K1> The first key type. Can be an array type for content-based key matching.
143 * @param <K2> The second key type. Can be an array type for content-based key matching.
144 * @param <V> The value type.
145 */
146@SuppressWarnings({
147   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
148})
149public class Cache2<K1,K2,V> {
150
151   /**
152    * Builder for creating configured {@link Cache2} instances.
153    *
154    * <h5 class='section'>Example:</h5>
155    * <p class='bjava'>
156    *    Cache2&lt;String,Integer,User&gt; <jv>cache</jv> = Cache2
157    *       .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>)
158    *       .maxSize(200)
159    *       .logOnExit()
160    *       .build();
161    * </p>
162    *
163    * <h5 class='section'>See Also:</h5>
164    * <ul>
165    *    <li class='jm'>{@link Cache2#of(Class, Class, Class)}
166    * </ul>
167    *
168    * @param <K1> The first key type.
169    * @param <K2> The second key type.
170    * @param <V> The value type.
171    */
172   public static class Builder<K1,K2,V> {
173
174      // Resolved once per JVM.  Looking these up per builder is expensive because Settings iterates every
175      // registered PropertySource (including the manifest scanner), and Cache builders are created very frequently.
176      private static final CacheMode DEFAULT_CACHE_MODE = env("juneau.cache.mode", CacheMode.FULL);
177      private static final int DEFAULT_MAX_SIZE = env("juneau.cache.maxSize", 1000);
178      private static final boolean DEFAULT_LOG_ON_EXIT = env("juneau.cache.logOnExit", false);
179
180      CacheMode cacheMode;
181      int maxSize;
182      String id;
183      boolean logOnExit;
184      boolean threadLocal;
185      Function2<K1,K2,V> supplier;
186
187      Builder() {
188         cacheMode = DEFAULT_CACHE_MODE;
189         maxSize = DEFAULT_MAX_SIZE;
190         logOnExit = DEFAULT_LOG_ON_EXIT;
191         id = "Cache2";
192      }
193
194      /**
195       * Builds a new {@link Cache2} instance with the configured settings.
196       *
197       * @return A new immutable {@link Cache2} instance.
198       */
199      public Cache2<K1,K2,V> build() {
200         return new Cache2<>(this);
201      }
202
203      /**
204       * Sets the caching mode for this cache.
205       *
206       * <p>
207       * Available modes:
208       * <ul>
209       *    <li>{@link CacheMode#NONE NONE} - No caching (always invoke supplier)
210       *    <li>{@link CacheMode#WEAK WEAK} - Best-effort weak-key caching (uses {@link WeakHashMap}); on this
211       *       composite-key cache an entry may be reclaimed at <b>any</b> GC, not only when the caller's keys
212       *       become unreachable - see {@link CacheMode#WEAK} for why
213       *    <li>{@link CacheMode#FULL FULL} - Full caching (uses {@link ConcurrentHashMap}, default); use this
214       *       mode for guaranteed retention
215       * </ul>
216       *
217       * @param value The caching mode.
218       * @return This object for method chaining.
219       */
220      public Builder<K1,K2,V> cacheMode(CacheMode value) {
221         cacheMode = value;
222         return this;
223      }
224
225      /**
226       * Conditionally enables logging of cache statistics when the JVM exits.
227       *
228       * <p>
229       * When enabled, the cache will register a shutdown hook that logs the cache name,
230       * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness.
231       *
232       * @param value Whether to enable logging on exit.
233       * @param idValue The identifier to use in the log message.
234       * @return This object for method chaining.
235       */
236      public Builder<K1,K2,V> logOnExit(boolean value, String idValue) {
237         id = idValue;
238         logOnExit = value;
239         return this;
240      }
241
242      /**
243       * Enables logging of cache statistics when the JVM exits.
244       *
245       * <p>
246       * When enabled, the cache will register a shutdown hook that logs the cache name,
247       * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness.
248       *
249       * <p>
250       * Example output:
251       * <p class='bconsole'>
252       *    ClassMeta cache:  hits=1523, misses: 47
253       * </p>
254       *
255       * <p>
256       * This is useful for:
257       * <ul>
258       *    <li>Performance tuning and identifying caching opportunities
259       *    <li>Determining optimal max size values
260       *    <li>Monitoring cache efficiency in production
261       * </ul>
262       *
263       * @param value The identifier to use in the log message.
264       * @return This object for method chaining.
265       */
266      public Builder<K1,K2,V> logOnExit(String value) {
267         id = value;
268         logOnExit = true;
269         return this;
270      }
271
272      /**
273       * Specifies the maximum number of entries allowed in this cache.
274       *
275       * <p>
276       * When the cache size exceeds this value, the <em>entire</em> cache is cleared to make room for new entries.
277       * This is a simple eviction strategy that avoids the overhead of LRU/LFU tracking.
278       *
279       * <p>
280       * Default value: 1000 (or value of system property <c>juneau.cache.maxSize</c>)
281       *
282       * <h5 class='section'>Notes:</h5>
283       * <ul>
284       *    <li>Setting this too low may cause excessive cache clearing and reduce effectiveness
285       *    <li>Setting this too high may consume excessive memory
286       *    <li>For unbounded caching, use {@link Integer#MAX_VALUE} (not recommended for production)
287       * </ul>
288       *
289       * @param value The maximum number of cache entries. Must be positive.
290       * @return This object for method chaining.
291       */
292      public Builder<K1,K2,V> maxSize(int value) {
293         maxSize = value;
294         return this;
295      }
296
297      /**
298       * Specifies the default supplier function for computing values when keys are not found.
299       *
300       * <p>
301       * This supplier will be used by {@link Cache2#get(Object, Object)} when a key pair is not in the cache.
302       * Individual lookups can override this supplier using
303       * {@link Cache2#get(Object, Object, java.util.function.Supplier)}.
304       *
305       * <h5 class='section'>Example:</h5>
306       * <p class='bjava'>
307       *    Cache2&lt;String,Integer,User&gt; <jv>cache</jv> = Cache2
308       *       .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>)
309       *       .supplier((tenant, id) -&gt; userService.findUser(tenant, id))
310       *       .build();
311       *
312       *    <jc>// Uses default supplier</jc>
313       *    User <jv>u</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123);
314       * </p>
315       *
316       * @param value The default supplier function. Can be <jk>null</jk>, in which case {@link Cache2#get(Object,Object)} will throw a {@link NullPointerException} (use {@link Cache2#get(Object, Object, java.util.function.Supplier)} instead to supply one per call).
317       * @return This object for method chaining.
318       */
319      public Builder<K1,K2,V> supplier(Function2<K1,K2,V> value) {
320         supplier = value;
321         return this;
322      }
323
324      /**
325       * Enables thread-local caching.
326       *
327       * <p>
328       * When enabled, each thread gets its own separate cache instance. This is useful for
329       * thread-unsafe objects that need to be cached per thread.
330       *
331       * <p>
332       * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
333       *
334       * @return This object for method chaining.
335       * @see Cache.Builder#threadLocal()
336       */
337      public Builder<K1,K2,V> threadLocal() {
338         threadLocal = true;
339         return this;
340      }
341
342      /**
343       * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
344       *
345       * <p>
346       * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
347       *
348       * <p>
349       * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
350       * garbage collected when keys are no longer strongly referenced elsewhere.
351       *
352       * <p>
353       * <b>Note:</b> on this composite-key cache, WEAK is best-effort only - the internal composite key is
354       * never caller-visible, so an entry may be reclaimed at any GC. See {@link CacheMode#WEAK} for details,
355       * and use {@link CacheMode#FULL} instead if guaranteed retention is required.
356       *
357       * @return This object for method chaining.
358       * @see #cacheMode(CacheMode)
359       */
360      public Builder<K1,K2,V> weak() {
361         return cacheMode(WEAK);
362      }
363
364   }
365
366   /**
367    * Creates a new {@link Builder} for constructing a cache with explicit type parameters.
368    *
369    * <p>
370    * This variant allows you to specify the cache's generic types explicitly without passing
371    * the class objects, which is useful when working with complex parameterized types.
372    *
373    * <h5 class='section'>Example:</h5>
374    * <p class='bjava'>
375    *    <jc>// Working with complex generic types</jc>
376    *    Cache2&lt;Class&lt;?&gt;,Class&lt;? extends Annotation&gt;,List&lt;Annotation&gt;&gt; <jv>cache</jv> =
377    *       Cache2.&lt;Class&lt;?&gt;,Class&lt;? extends Annotation&gt;,List&lt;Annotation&gt;&gt;<jsm>create</jsm>()
378    *          .supplier((k1, k2) -&gt; findAnnotations(k1, k2))
379    *          .build();
380    * </p>
381    *
382    * @param <K1> The first key type.
383    * @param <K2> The second key type.
384    * @param <V> The value type.
385    * @return A new builder for configuring the cache.
386    */
387   public static <K1,K2,V> Builder<K1,K2,V> create() {
388      return new Builder<>();
389   }
390
391   /**
392    * Creates a new {@link Builder} for constructing a cache.
393    *
394    * <h5 class='section'>Example:</h5>
395    * <p class='bjava'>
396    *    Cache2&lt;String,Integer,User&gt; <jv>cache</jv> = Cache2
397    *       .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>)
398    *       .maxSize(100)
399    *       .build();
400    * </p>
401    *
402    * @param <K1> The first key type.
403    * @param <K2> The second key type.
404    * @param <V> The value type.
405    * @param key1 The first key type class (used for type safety).
406    * @param key2 The second key type class (used for type safety).
407    * @param type The value type class.
408    * @return A new builder for configuring the cache.
409    */
410   @SuppressWarnings({
411      "java:S1172" // Parameters required for type inference in public API
412   })
413   public static <K1,K2,V> Builder<K1,K2,V> of(Class<K1> key1, Class<K2> key2, Class<V> type) {
414      return new Builder<>();
415   }
416
417   // Internal map with Tuple2 keys for content-based equality (especially for arrays)
418   // If threadLocal is true, this is null and threadLocalMap is used instead
419   private final Map<Tuple2<K1,K2>,V> map;
420
421   @SuppressWarnings({
422      "java:S5164" // Cleanup method provided: cleanup()
423   })
424   private final ThreadLocal<Map<Tuple2<K1,K2>,V>> threadLocalMap;
425
426   private final boolean isThreadLocal;
427
428   private final int maxSize;
429   private final CacheMode cacheMode;
430   private final Function2<K1,K2,V> supplier;
431   private final AtomicInteger cacheHits = new AtomicInteger();
432
433   /**
434    * Constructor.
435    *
436    * @param builder The builder containing configuration settings.
437    */
438   protected Cache2(Builder<K1,K2,V> builder) {
439      this.maxSize = builder.maxSize;
440      this.cacheMode = builder.cacheMode;
441      this.supplier = builder.supplier;
442      this.isThreadLocal = builder.threadLocal;
443
444      if (isThreadLocal) {
445         // Thread-local mode: each thread gets its own map
446         if (builder.cacheMode == WEAK) {
447            this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
448         } else {
449            this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
450         }
451         this.map = null;
452      } else {
453         // Normal mode: shared map across all threads
454         if (builder.cacheMode == WEAK) {
455            this.map = Collections.synchronizedMap(new WeakHashMap<>());
456         } else {
457            this.map = new ConcurrentHashMap<>();
458         }
459         this.threadLocalMap = null;
460      }
461      if (builder.logOnExit) {
462         shutdownMessage(() -> builder.id + ":  hits=" + cacheHits.get() + ", misses: " + size());
463      }
464   }
465
466   /**
467    * Removes all entries from the cache.
468    */
469   public void clear() {
470      getMap().clear();
471   }
472
473   /**
474    * Cleans up thread-local storage for the current thread.
475    *
476    * <p>
477    * This method should be called when a thread is finished using this cache to prevent memory leaks
478    * in thread pool environments where threads are reused.
479    * Only has an effect if this cache was created with thread-local mode enabled.
480    */
481   public void cleanup() {
482      if (isThreadLocal && threadLocalMap != null) {
483         threadLocalMap.remove();
484      }
485   }
486
487   /**
488    * Returns <jk>true</jk> if the cache contains a mapping for the specified key pair.
489    *
490    * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key).
491    * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key).
492    * @return <jk>true</jk> if the cache contains the key pair.
493    */
494   public boolean containsKey(K1 key1, K2 key2) {
495      return getMap().containsKey(Tuple2.of(key1, key2));
496   }
497
498   /**
499    * Returns <jk>true</jk> if the cache contains one or more entries with the specified value.
500    *
501    * @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).
502    * @return <jk>true</jk> if the cache contains the value.
503    */
504   public boolean containsValue(V value) {
505      // ConcurrentHashMap doesn't allow null values, so null can never be in the cache
506      if (value == null)
507         return false;
508      return getMap().containsValue(value);
509   }
510
511   /**
512    * Retrieves a cached value by key pair using the default supplier.
513    *
514    * <p>
515    * This method uses the default supplier configured via {@link Builder#supplier(Function2)}.
516    * If no default supplier was configured, this method will throw a {@link NullPointerException}.
517    *
518    * <h5 class='section'>Example:</h5>
519    * <p class='bjava'>
520    *    Cache2&lt;String,Integer,User&gt; <jv>cache</jv> = Cache2
521    *       .<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>)
522    *       .supplier((tenant, id) -&gt; userService.findUser(tenant, id))
523    *       .build();
524    *
525    *    <jc>// Uses default supplier</jc>
526    *    User <jv>u</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123);
527    * </p>
528    *
529    * @param key1 First key component. Can be <jk>null</jk> (null keys are supported like any other key).
530    * @param key2 Second key component. Can be <jk>null</jk> (null keys are supported like any other key).
531    * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>.
532    * @throws NullPointerException if no default supplier was configured.
533    */
534   public V get(K1 key1, K2 key2) {
535      return get(key1, key2, () -> supplier.apply(key1, key2));
536   }
537
538   /**
539    * Retrieves a cached value by key pair, computing it if necessary using the provided supplier.
540    *
541    * <p>
542    * This method implements the cache-aside pattern:
543    * <ol>
544    *    <li>If the key pair exists in the cache, return the cached value (cache hit)
545    *    <li>If the key pair doesn't exist, invoke the supplier to compute the value
546    *    <li>Store the computed value in the cache using
547    *       {@link ConcurrentHashMap#putIfAbsent(Object, Object)}
548    *    <li>Return the value
549    * </ol>
550    *
551    * <h5 class='section'>Behavior:</h5>
552    * <ul class='spaced-list'>
553    *    <li>If the cache is disabled, always invokes the supplier without caching
554    *    <li>If the cache exceeds {@link Builder#maxSize(int)}, clears all entries before storing the new value
555    *    <li>Thread-safe: Multiple threads can safely call this method concurrently
556    *    <li>The supplier may be called multiple times for the same key pair in concurrent scenarios
557    *       (due to {@link ConcurrentHashMap#putIfAbsent(Object, Object)} semantics)
558    * </ul>
559    *
560    * <h5 class='section'>Example:</h5>
561    * <p class='bjava'>
562    *    Cache2&lt;String,Integer,User&gt; <jv>cache</jv> = Cache2.<jsm>of</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>, User.<jk>class</jk>).build();
563    *
564    *    <jc>// First call: fetches user and caches it</jc>
565    *    User <jv>u1</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123, () -&gt; userService.findUser(<js>"tenant1"</js>, 123));
566    *
567    *    <jc>// Second call: returns cached user instantly</jc>
568    *    User <jv>u2</jv> = <jv>cache</jv>.get(<js>"tenant1"</js>, 123, () -&gt; userService.findUser(<js>"tenant1"</js>, 123));
569    *
570    *    <jsm>assert</jsm> <jv>u1</jv> == <jv>u2</jv>;  <jc>// Same instance</jc>
571    * </p>
572    *
573    * @param key1 First key component. Can be <jk>null</jk> (null keys are supported like any other key).
574    * @param key2 Second key component. Can be <jk>null</jk> (null keys are supported like any other key).
575    * @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.
576    * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>.
577    */
578   public V get(K1 key1, K2 key2, java.util.function.Supplier<V> supplier) {
579      reqnn("supplier", supplier);
580      if (cacheMode == NONE)
581         return supplier.get();
582      var m = getMap();
583      var wrapped = Tuple2.of(key1, key2);
584      V v = m.get(wrapped);
585      if (v == null) {
586         if (size() > maxSize)
587            clear();
588         v = supplier.get();
589         if (v == null)
590            m.remove(wrapped);
591         else
592            m.putIfAbsent(wrapped, v);
593      } else {
594         cacheHits.incrementAndGet();
595      }
596      return v;
597   }
598
599   /**
600    * Returns the total number of cache hits since this cache was created.
601    *
602    * <p>
603    * A cache hit occurs when {@link #get(Object, Object)} or
604    * {@link #get(Object, Object, java.util.function.Supplier)} finds an existing cached value
605    * for the requested key pair, avoiding the need to invoke the supplier.
606    *
607    * <h5 class='section'>Cache Effectiveness:</h5>
608    * <p>
609    * You can calculate the cache hit ratio using:
610    * <p class='bjava'>
611    *    <jk>int</jk> <jv>hits</jv> = <jv>cache</jv>.getCacheHits();
612    *    <jk>int</jk> <jv>misses</jv> = <jv>cache</jv>.size();
613    *    <jk>int</jk> <jv>total</jv> = <jv>hits</jv> + <jv>misses</jv>;
614    *    <jk>double</jk> <jv>hitRatio</jv> = (<jk>double</jk>) <jv>hits</jv> / <jv>total</jv>;  <jc>// 0.0 to 1.0</jc>
615    * </p>
616    *
617    * <h5 class='section'>Notes:</h5>
618    * <ul>
619    *    <li>This counter is never reset, even when {@link #clear()} is called
620    *    <li>Thread-safe using {@link AtomicInteger}
621    *    <li>Returns 0 if the cache is disabled
622    * </ul>
623    *
624    * @return The total number of cache hits since creation.
625    */
626   public int getCacheHits() { return cacheHits.get(); }
627
628   /**
629    * Returns <jk>true</jk> if the cache contains no entries.
630    *
631    * @return <jk>true</jk> if the cache is empty.
632    */
633   public boolean isEmpty() { return getMap().isEmpty(); }
634
635   /**
636    * Associates the specified value with the specified key pair.
637    *
638    * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key).
639    * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key).
640    * @param value The value to associate with the key pair.
641    * @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping.
642    */
643   public V put(K1 key1, K2 key2, V value) {
644      var m = getMap();
645      if (value == null)
646         return m.remove(Tuple2.of(key1, key2));
647      return m.put(Tuple2.of(key1, key2), value);
648   }
649
650   /**
651    * Removes the entry for the specified key pair from the cache.
652    *
653    * @param key1 The first key. Can be <jk>null</jk> (null keys are supported like any other key).
654    * @param key2 The second key. Can be <jk>null</jk> (null keys are supported like any other key).
655    * @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping.
656    */
657   public V remove(K1 key1, K2 key2) {
658      return getMap().remove(Tuple2.of(key1, key2));
659   }
660
661   /**
662    * Returns the number of entries in the cache.
663    *
664    * @return The number of cached entries.
665    */
666   public int size() {
667      return getMap().size();
668   }
669
670   /**
671    * Gets the map for the current thread.
672    *
673    * @return The map for the current thread.
674    */
675   private Map<Tuple2<K1,K2>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
676}