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