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