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