001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.collections;
018
019import static org.apache.juneau.commons.collections.CacheMode.*;
020import static org.apache.juneau.commons.utils.AssertionUtils.*;
021import static org.apache.juneau.commons.utils.SystemUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023
024import java.util.*;
025import java.util.concurrent.*;
026import java.util.concurrent.atomic.*;
027
028import org.apache.juneau.commons.function.*;
029
030/**
031 * Simple in-memory cache for storing and retrieving objects by five-part keys.
032 *
033 * <h5 class='section'>Overview:</h5>
034 * <p>
035 * This class uses {@link java.util.concurrent.ConcurrentHashMap} internally to provide a thread-safe caching layer with automatic
036 * value computation, cache eviction, and statistics tracking for five-part composite keys. It's designed for
037 * caching expensive-to-compute or frequently-accessed objects indexed by five keys.
038 *
039 * <h5 class='section'>Features:</h5>
040 * <ul class='spaced-list'>
041 *    <li>Thread-safe concurrent access without external synchronization
042 *    <li>Five-part composite key support
043 *    <li>Automatic cache eviction when maximum size is reached
044 *    <li>Lazy computation via {@link Function5} supplier pattern
045 *    <li>Default supplier support for simplified access
046 *    <li>Built-in hit/miss statistics tracking
047 *    <li>Optional logging of cache statistics on JVM shutdown
048 *    <li>Can be disabled entirely via builder or system property
049 * </ul>
050 *
051 * <h5 class='section'>See Also:</h5>
052 * <ul>
053 *    <li class='jc'>{@link Cache}
054 *    <li class='jc'>{@link Cache2}
055 *    <li class='jc'>{@link Cache3}
056 *    <li class='jc'>{@link Cache4}
057 *    <li class='jc'>{@link java.util.concurrent.ConcurrentHashMap}
058 *    <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview &gt; juneau-commons</a>
059 * </ul>
060 *
061 * @param <K1> The first key type.
062 * @param <K2> The second key type.
063 * @param <K3> The third key type.
064 * @param <K4> The fourth key type.
065 * @param <K5> The fifth key type.
066 * @param <V> The value type.
067 */
068public class Cache5<K1,K2,K3,K4,K5,V> {
069
070   /**
071    * Builder for creating configured {@link Cache5} instances.
072    *
073    * @param <K1> The first key type.
074    * @param <K2> The second key type.
075    * @param <K3> The third key type.
076    * @param <K4> The fourth key type.
077    * @param <K5> The fifth key type.
078    * @param <V> The value type.
079    */
080   public static class Builder<K1,K2,K3,K4,K5,V> {
081      CacheMode cacheMode;
082      int maxSize;
083      String id;
084      boolean logOnExit;
085      boolean threadLocal;
086      Function5<K1,K2,K3,K4,K5,V> supplier;
087
088      Builder() {
089         cacheMode = env("juneau.cache.mode", CacheMode.FULL);
090         maxSize = env("juneau.cache.maxSize", 1000);
091         logOnExit = env("juneau.cache.logOnExit", false);
092         id = "Cache5";
093      }
094
095      /**
096       * Builds a new {@link Cache5} instance with the configured settings.
097       *
098       * @return A new immutable {@link Cache5} instance.
099       */
100      public Cache5<K1,K2,K3,K4,K5,V> build() {
101         return new Cache5<>(this);
102      }
103
104      /**
105       * Sets the caching mode for this cache.
106       *
107       * <p>
108       * Available modes:
109       * <ul>
110       *    <li>{@link CacheMode#NONE NONE} - No caching (always invoke supplier)
111       *    <li>{@link CacheMode#WEAK WEAK} - Weak caching (uses {@link WeakHashMap})
112       *    <li>{@link CacheMode#FULL FULL} - Full caching (uses {@link ConcurrentHashMap}, default)
113       * </ul>
114       *
115       * @param value The caching mode.
116       * @return This object for method chaining.
117       */
118      public Builder<K1,K2,K3,K4,K5,V> cacheMode(CacheMode value) {
119         cacheMode = value;
120         return this;
121      }
122
123      /**
124       * Conditionally enables logging of cache statistics when the JVM exits.
125       *
126       * <p>
127       * When enabled, the cache will register a shutdown hook that logs the cache name,
128       * total cache hits, and total cache misses (size of cache) to help analyze cache effectiveness.
129       *
130       * @param value Whether to enable logging on exit.
131       * @param idValue The identifier to use in the log message.
132       * @return This object for method chaining.
133       */
134      public Builder<K1,K2,K3,K4,K5,V> logOnExit(boolean value, String idValue) {
135         id = idValue;
136         logOnExit = value;
137         return this;
138      }
139
140      /**
141       * Enables logging of cache statistics when the JVM exits.
142       *
143       * @param value The identifier to use in the log message.
144       * @return This object for method chaining.
145       */
146      public Builder<K1,K2,K3,K4,K5,V> logOnExit(String value) {
147         id = value;
148         logOnExit = true;
149         return this;
150      }
151
152      /**
153       * Specifies the maximum number of entries allowed in this cache.
154       *
155       * @param value The maximum number of cache entries. Must be positive.
156       * @return This object for method chaining.
157       */
158      public Builder<K1,K2,K3,K4,K5,V> maxSize(int value) {
159         maxSize = value;
160         return this;
161      }
162
163      /**
164       * Specifies the default supplier function for computing values when keys are not found.
165       *
166       * @param value The default supplier function. Can be <jk>null</jk>.
167       * @return This object for method chaining.
168       */
169      public Builder<K1,K2,K3,K4,K5,V> supplier(Function5<K1,K2,K3,K4,K5,V> value) {
170         supplier = value;
171         return this;
172      }
173
174      /**
175       * Enables thread-local caching.
176       *
177       * <p>
178       * When enabled, each thread gets its own separate cache instance. This is useful for
179       * thread-unsafe objects that need to be cached per thread.
180       *
181       * <p>
182       * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
183       *
184       * @return This object for method chaining.
185       * @see Cache.Builder#threadLocal()
186       */
187      public Builder<K1,K2,K3,K4,K5,V> threadLocal() {
188         threadLocal = true;
189         return this;
190      }
191
192      /**
193       * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
194       *
195       * <p>
196       * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
197       *
198       * <p>
199       * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
200       * garbage collected when keys are no longer strongly referenced elsewhere.
201       *
202       * @return This object for method chaining.
203       * @see #cacheMode(CacheMode)
204       */
205      public Builder<K1,K2,K3,K4,K5,V> weak() {
206         return cacheMode(WEAK);
207      }
208   }
209
210   /**
211    * Creates a new {@link Builder} for constructing a cache with explicit type parameters.
212    *
213    * <p>
214    * This variant allows you to specify the cache's generic types explicitly without passing
215    * the class objects, which is useful when working with complex parameterized types.
216    *
217    * @param <K1> The first key type.
218    * @param <K2> The second key type.
219    * @param <K3> The third key type.
220    * @param <K4> The fourth key type.
221    * @param <K5> The fifth key type.
222    * @param <V> The value type.
223    * @return A new builder for configuring the cache.
224    */
225   public static <K1,K2,K3,K4,K5,V> Builder<K1,K2,K3,K4,K5,V> create() {
226      return new Builder<>();
227   }
228
229   /**
230    * Creates a new {@link Builder} for constructing a cache.
231    *
232    * @param <K1> The first key type.
233    * @param <K2> The second key type.
234    * @param <K3> The third key type.
235    * @param <K4> The fourth key type.
236    * @param <K5> The fifth key type.
237    * @param <V> The value type.
238    * @param key1 The first key type class (used for type safety).
239    * @param key2 The second key type class (used for type safety).
240    * @param key3 The third key type class (used for type safety).
241    * @param key4 The fourth key type class (used for type safety).
242    * @param key5 The fifth key type class (used for type safety).
243    * @param type The value type class.
244    * @return A new builder for configuring the cache.
245    */
246   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) {
247      return new Builder<>();
248   }
249
250   // Internal map with Tuple5 keys for content-based equality (especially for arrays)
251   // If threadLocal is true, this is null and threadLocalMap is used instead
252   private final java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V> map;
253
254   private final ThreadLocal<java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V>> threadLocalMap;
255
256   private final boolean isThreadLocal;
257
258   private final int maxSize;
259   private final CacheMode cacheMode;
260   private final Function5<K1,K2,K3,K4,K5,V> supplier;
261   private final AtomicInteger cacheHits = new AtomicInteger();
262
263   /**
264    * Constructor.
265    *
266    * @param builder The builder containing configuration settings.
267    */
268   protected Cache5(Builder<K1,K2,K3,K4,K5,V> builder) {
269      this.maxSize = builder.maxSize;
270      this.cacheMode = builder.cacheMode;
271      this.supplier = builder.supplier;
272      this.isThreadLocal = builder.threadLocal;
273
274      if (isThreadLocal) {
275         // Thread-local mode: each thread gets its own map
276         if (builder.cacheMode == WEAK) {
277            this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
278         } else {
279            this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
280         }
281         this.map = null;
282      } else {
283         // Normal mode: shared map across all threads
284         if (builder.cacheMode == WEAK) {
285            this.map = Collections.synchronizedMap(new WeakHashMap<>());
286         } else {
287            this.map = new ConcurrentHashMap<>();
288         }
289         this.threadLocalMap = null;
290      }
291      if (builder.logOnExit) {
292         shutdownMessage(() -> builder.id + ":  hits=" + cacheHits.get() + ", misses: " + size());
293      }
294   }
295
296   /**
297    * Removes all entries from the cache.
298    */
299   public void clear() {
300      getMap().clear();
301   }
302
303   /**
304    * Returns <jk>true</jk> if the cache contains a mapping for the specified five-part key.
305    *
306    * @param key1 The first key.
307    * @param key2 The second key.
308    * @param key3 The third key.
309    * @param key4 The fourth key.
310    * @param key5 The fifth key.
311    * @return <jk>true</jk> if the cache contains the five-part key.
312    */
313   public boolean containsKey(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) {
314      return getMap().containsKey(Tuple5.of(key1, key2, key3, key4, key5));
315   }
316
317   /**
318    * Returns <jk>true</jk> if the cache contains one or more entries with the specified value.
319    *
320    * @param value The value to check.
321    * @return <jk>true</jk> if the cache contains the value.
322    */
323   public boolean containsValue(V value) {
324      // ConcurrentHashMap doesn't allow null values, so null can never be in the cache
325      if (value == null)
326         return false;
327      return getMap().containsValue(value);
328   }
329
330   /**
331    * Retrieves a cached value by five-part key using the default supplier.
332    *
333    * @param key1 First key component. Can be <jk>null</jk>.
334    * @param key2 Second key component. Can be <jk>null</jk>.
335    * @param key3 Third key component. Can be <jk>null</jk>.
336    * @param key4 Fourth key component. Can be <jk>null</jk>.
337    * @param key5 Fifth key component. Can be <jk>null</jk>.
338    * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>.
339    * @throws NullPointerException if no default supplier was configured.
340    *
341    */
342   public V get(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) {
343      return get(key1, key2, key3, key4, key5, () -> supplier.apply(key1, key2, key3, key4, key5));
344   }
345
346   /**
347    * Retrieves a cached value by five-part key, computing it if necessary using the provided supplier.
348    *
349    * @param key1 First key component. Can be <jk>null</jk>.
350    * @param key2 Second key component. Can be <jk>null</jk>.
351    * @param key3 Third key component. Can be <jk>null</jk>.
352    * @param key4 Fourth key component. Can be <jk>null</jk>.
353    * @param key5 Fifth key component. Can be <jk>null</jk>.
354    * @param supplier The supplier to compute the value if it's not in the cache. Must not be <jk>null</jk>.
355    * @return The cached or computed value. May be <jk>null</jk> if the supplier returns <jk>null</jk>.
356    *
357    */
358   public V get(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5, java.util.function.Supplier<V> supplier) {
359      assertArgNotNull("supplier", supplier);
360      if (cacheMode == NONE)
361         return supplier.get();
362      var m = getMap();
363      var wrapped = Tuple5.of(key1, key2, key3, key4, key5);
364      V v = m.get(wrapped);
365      if (v == null) {
366         if (size() > maxSize)
367            clear();
368         v = supplier.get();
369         if (v == null)
370            m.remove(wrapped);
371         else
372            m.putIfAbsent(wrapped, v);
373      } else {
374         cacheHits.incrementAndGet();
375      }
376      return v;
377   }
378
379   /**
380    * Returns the total number of cache hits since this cache was created.
381    *
382    * @return The total number of cache hits since creation.
383    */
384   public int getCacheHits() { return cacheHits.get(); }
385
386   /**
387    * Returns <jk>true</jk> if the cache contains no entries.
388    *
389    * @return <jk>true</jk> if the cache is empty.
390    */
391   public boolean isEmpty() { return getMap().isEmpty(); }
392
393   /**
394    * Associates the specified value with the specified five-part key.
395    *
396    * @param key1 The first key.
397    * @param key2 The second key.
398    * @param key3 The third key.
399    * @param key4 The fourth key.
400    * @param key5 The fifth key.
401    * @param value The value to associate with the five-part key.
402    * @return The previous value associated with the five-part key, or <jk>null</jk> if there was no mapping.
403    *
404    */
405   public V put(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5, V value) {
406      var m = getMap();
407      if (value == null)
408         return m.remove(Tuple5.of(key1, key2, key3, key4, key5));
409      return m.put(Tuple5.of(key1, key2, key3, key4, key5), value);
410   }
411
412   /**
413    * Removes the entry for the specified five-part key from the cache.
414    *
415    * @param key1 The first key.
416    * @param key2 The second key.
417    * @param key3 The third key.
418    * @param key4 The fourth key.
419    * @param key5 The fifth key.
420    * @return The previous value associated with the five-part key, or <jk>null</jk> if there was no mapping.
421    *
422    */
423   public V remove(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) {
424      return getMap().remove(Tuple5.of(key1, key2, key3, key4, key5));
425   }
426
427   /**
428    * Returns the number of entries in the cache.
429    *
430    * @return The number of cached entries.
431    */
432   public int size() {
433      return getMap().size();
434   }
435
436   /**
437    * Gets the map for the current thread.
438    *
439    * @return The map for the current thread.
440    */
441   private Map<Tuple5<K1,K2,K3,K4,K5>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
442}