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