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