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