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