001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util;
017
018import java.time.Duration;
019import java.util.concurrent.ExecutionException;
020import java.util.function.Supplier;
021
022import com.google.common.base.Ticker;
023import com.google.common.cache.CacheBuilder;
024import com.google.common.cache.CacheLoader;
025import com.google.common.cache.LoadingCache;
026
027/**
028 * A cached value. It can be constructed with {@link #withInitial} with a {@link Supplier} to pass in order to compute the value to cache on the first call of {@link #get()}.
029 * <br>It can also be constructed with {@link #withExpiryDuration} with the {@link Supplier} and an expiry {@link Duration} to automatically force the reload of the value.
030 * <br>The supplier is ensured to be called once (until {@link #uncache} is called, or after the expiry duration has elapsed since the last computing), even with multiple callers at the same time.
031 * <br>The returned value by the supplier <b>must not be null</b>
032 * @param <T> The type of the cached value
033 */
034public class CachedValue<T>
035{
036    private static final Object __ALL_CACHES_ONLY_KEY = new Object();
037    
038    private LoadingCache<Object, T> _loadingCache;
039    
040    CachedValue(Supplier<T> supplier, Duration expiryDuration, Ticker ticker)
041    {
042        LoadingCache<Object, T> loadingCache = _cacheBuilder(expiryDuration, ticker)
043                .build(_cacheLoader(supplier));
044        _loadingCache = loadingCache;
045    }
046    
047    /**
048     * Creates a cached value. The value will be computed by invoking
049     * the given supplier when the {@link #get()} method will be
050     * called the first time.
051     * @param <T> The type of the cached value
052     * @param supplier The supplier which will be invoked only once in order to compute the value to cache. The value <b>must not be null</b>
053     * @return The {@link CachedValue}
054     */
055    public static <T> CachedValue<T> withInitial(Supplier<T> supplier)
056    {
057        return new CachedValue<>(supplier, null, null);
058    }
059    
060    /**
061     * Creates a cached value. The value will be computed by invoking
062     * the given supplier when the {@link #get()} method will be
063     * called the very first time, and every first time after the given {@link Duration} is expired.
064     * @param <T> The type of the cached value
065     * @param supplier The supplier which will be invoked only once in order to compute the value to cache. The value <b>must not be null</b>
066     * @param expiryDuration The expiry duration
067     * @return The {@link CachedValue}
068     */
069    public static <T> CachedValue<T> withExpiryDuration(Supplier<T> supplier, Duration expiryDuration)
070    {
071        return new CachedValue<>(supplier, expiryDuration, null);
072    }
073    
074    CacheBuilder<Object, Object> _cacheBuilder(Duration expiryDuration, Ticker ticker)
075    {
076        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder()
077                .initialCapacity(1)
078                // key will always be __ALL_CACHES_ONLY_KEY
079                .weakKeys();
080        
081        if (expiryDuration != null)
082        {
083            cacheBuilder.expireAfterWrite(expiryDuration);
084        }
085        if (ticker != null)
086        {
087            cacheBuilder.ticker(ticker);
088        }
089        
090        return cacheBuilder;
091    }
092    
093    private static <T> CacheLoader<Object, T> _cacheLoader(Supplier<T> supplier)
094    {
095        return new CacheLoader<>()
096        {
097            @Override
098            public T load(Object key) throws Exception
099            {
100                // key should always be __ALL_CACHES_ONLY_KEY, and is useless
101                T value = supplier.get();
102                if (value == null)
103                {
104                    throw new IllegalArgumentException("The given supplier cannot return a null value.");
105                }
106                return value;
107            }
108        };
109    }
110    
111    /**
112     * Returns the value. If it is the first time it is called, then it is computed
113     * by invoking the supplier function. Otherwise, the cached value is returned.
114     * @return the value
115     */
116    public T get()
117    {
118        try
119        {
120            return _loadingCache.get(__ALL_CACHES_ONLY_KEY);
121        }
122        catch (ExecutionException e)
123        {
124            throw new IllegalStateException("An unexpected error occured when loading the value from the supplier function.", e);
125        }
126    }
127    
128    /**
129     * Uncache the value. Mostly to force to re-compute the value. 
130     */
131    public void uncache()
132    {
133        _loadingCache.invalidate(__ALL_CACHES_ONLY_KEY);
134    }
135}