001/*
002 *  Copyright 2019 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.plugins.core.impl.cache;
017
018import java.lang.reflect.Field;
019import java.time.Duration;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.concurrent.Callable;
026import java.util.concurrent.ExecutionException;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029
030import org.ametys.core.cache.Cache;
031import org.ametys.core.cache.CacheException;
032import org.ametys.core.cache.CacheStats;
033import org.ametys.core.util.SizeUtils;
034import org.ametys.runtime.i18n.I18nizableText;
035
036import com.google.common.cache.CacheBuilder;
037import com.google.common.cache.Weigher;
038import com.google.common.util.concurrent.ExecutionError;
039import com.google.common.util.concurrent.UncheckedExecutionException;
040
041/**
042 * Implementation of AmetysCache with Guava library
043 *
044 * @param <K> the key
045 * @param <V> the value
046 */
047public class GuavaCache<K, V> implements Cache<K, V>
048{
049
050    /** the cache itself, using Guava implementation */
051    protected com.google.common.cache.Cache<K, Optional<V>> _cache;
052
053    /** Id of the cache */
054    protected String _id;
055    
056    /** Label of the cache */
057    protected I18nizableText _label;
058
059    /** Description of the cache */
060    protected I18nizableText _description;
061
062    /** Maximum size of the cache in bytes */
063    protected long _size;
064    
065    /** True if the size of the cache can be computed, false otherwise (e.g., the cache store a component) */
066    protected boolean _computableSize;
067
068    /** The length of time after an entry is created that it should be automatically*/
069    protected Duration _duration;
070    
071    /** True if the put or putAll have been called (even without values), false otherwises. become false again when the cache is reseted or if all values have been cleaned.  */
072    protected boolean _isInitialized;
073    
074    /** True if the cache can be transmitted in sub-requests of DispatchGenerator, false otherwises. */
075    protected boolean _isDispatchable;
076    
077   
078    /**
079     * Implementation of AmetysCache with Guava library
080     * @param id id of the cache
081     * @param label label of the cache
082     * @param description description
083     * @param size maximum size of the cache
084     * @param computableSize true if the size of the cache can be computed
085     * @param duration the length of time after an entry is created that it should be automatically removed
086     * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator
087     */
088    public GuavaCache(String id, I18nizableText label, I18nizableText description, long size, boolean computableSize, Duration duration, boolean isDispatchable)
089    {
090        _label = label;
091        _description = description;
092        _id = id;
093        _size = size;
094        _computableSize = computableSize;
095        _duration = duration;
096        _isInitialized = false;
097        _isDispatchable = isDispatchable;
098        resetCache();
099    }
100
101    public void resetCache()
102    {
103        CacheBuilder cacheBuilder = com.google.common.cache.CacheBuilder.newBuilder();
104        
105        if (_duration != null)
106        {
107            cacheBuilder = cacheBuilder.expireAfterWrite(_duration);
108        }
109        _cache = cacheBuilder.recordStats()
110                .maximumWeight(_size)
111                .weigher(new Weigher<K, Optional<V>>()
112                {
113                    public int weigh(K k, Optional<V> v)
114                    {
115                        if (_computableSize)
116                        {
117                            return (int) (SizeUtils.sizeOf(k) + SizeUtils.sizeOf(v.orElse(null)));
118                        }
119                        else
120                        {
121                            return 0;
122                        }
123                    }
124                }).build();
125        _isInitialized = false;
126    }
127
128    public V get(K key, Function<K, V> function) throws CacheException
129    {
130        if (key instanceof AbstractCacheKey)
131        {
132            if (((AbstractCacheKey) key).isPartialKey())
133            {
134                throw new CacheException("Complex key is not valid because it contains null values");
135            }
136        }
137        try
138        {
139            return _cache.get(key, new Callable<Optional<V>>()
140            {
141                @Override
142                public Optional<V> call()
143                {
144                    return Optional.ofNullable(function.apply(key));
145                }
146            }).orElse(null);
147        }
148        catch (ExecutionException | ExecutionError | UncheckedExecutionException e)
149        {
150            // assume that the real exception is always e.getCause() as LocalCache wrap the exception into a ExecutionException, ExecutionError or UncheckedExecutionException
151            throw new CacheException("An error occurred while computing the new value for key " + key, e.getCause());
152        }
153    }
154
155    public V get(K key)
156    {
157        Optional<V> optional = _cache.getIfPresent(key);
158        if (optional != null)
159        {
160            return optional.orElse(null);
161        }
162        else
163        {
164            return null;
165        }
166    }
167
168    public void put(K key, V value)
169    {
170        if (key instanceof AbstractCacheKey)
171        {
172            if (((AbstractCacheKey) key).isPartialKey())
173            {
174                throw new RuntimeException("complex key is not valid because it contains null values");
175            }
176        }
177        _cache.put(key, Optional.ofNullable(value));
178        _isInitialized = true;
179    }
180
181    public void putAll(Map<K, V> map)
182    {
183        map.forEach((k, v) -> 
184        {
185            _cache.put(k, Optional.ofNullable(v));
186        }); 
187        _isInitialized = true;
188    }
189
190    public CacheStats getCacheStats()
191    {
192        return new GuavaCacheStats(_cache.stats());
193    }
194
195    public long getMemorySize() throws CacheException
196    {
197        if (!_computableSize)
198        {
199            return -1;
200        }
201        try
202        {
203            long sum = 0;
204            Field localCacheField = _cache.getClass().getDeclaredField("localCache"); // NoSuchFieldException
205            localCacheField.setAccessible(true);
206            var localCache = localCacheField.get(_cache); // IllegalAccessException
207
208            Field segmentsField = localCache.getClass().getDeclaredField("segments"); // NoSuchFieldException
209            segmentsField.setAccessible(true);
210            final Object[] segments = (Object[]) segmentsField.get(localCache); // IllegalAccessException
211
212            for (int i = 0; i < segments.length; i++)
213            {
214                Field totalWeight = segments[i].getClass().getDeclaredField("totalWeight"); // NoSuchFieldException
215                totalWeight.setAccessible(true);
216                long totalWeightValue = (long) totalWeight.get(segments[i]); // IllegalAccessException
217                sum += totalWeightValue;
218            }
219
220            return sum;
221            
222        }
223        catch (Exception e)
224        {
225            throw new CacheException("Cannot compute object size", e);
226        }
227    }
228
229    public long getNumberOfElements()
230    {
231        return _cache.size();
232    }
233
234    public void invalidate(K key)
235    {
236        List<K> keyToRemove = new ArrayList<>();
237
238        if (key instanceof AbstractCacheKey)
239        {
240            AbstractCacheKey invalidationKey = (AbstractCacheKey) key;
241            if (invalidationKey.isPartialKey())
242            {
243                // Remove null element
244                List<Object> keyList = invalidationKey.getFields();
245
246                keyToRemove = _cache.asMap().keySet().stream().filter(k ->
247                {
248                    AbstractCacheKey abstractK = (AbstractCacheKey) k;
249                    boolean isEquals = true;
250                    
251                    for (int i = 0; i < keyList.size(); i++)
252                    {
253                        isEquals = isEquals && (keyList.get(i) == null || keyList.get(i).equals(abstractK.getFields().get(i)));
254                    }
255                    
256                    return isEquals;
257                    
258                }).collect(Collectors.toList());
259            }
260            else
261            {
262                keyToRemove.add(key);
263            }
264        }
265        else
266        {
267            keyToRemove.add(key);
268        }
269        _cache.invalidateAll(keyToRemove);
270
271    }
272
273    public void invalidateAll()
274    {
275        _cache.invalidateAll();
276        _isInitialized = false;
277    }
278
279    public I18nizableText getDescription()
280    {
281        return _description;
282    }
283
284    public I18nizableText getLabel()
285    {
286        return _label;
287    }
288
289    public String getId()
290    {
291        return _id;
292    }
293
294    public long getMaxSize()
295    {
296        return _size;
297    }
298
299    public boolean hasKey(K key)
300    {
301        if (key instanceof AbstractCacheKey)
302        {
303            AbstractCacheKey invalidationKey = (AbstractCacheKey) key;
304            if (invalidationKey.isPartialKey())
305            {
306                // we ignore null element
307                List<Object> keyList = invalidationKey.getFields();
308                
309                return _cache.asMap().keySet().stream()
310                    .anyMatch(k -> {
311                        boolean hasKey = true;
312                        AbstractCacheKey abstractKey = (AbstractCacheKey) k;
313                        List<Object> fields = abstractKey.getFields();
314                        int i = 0;
315                        while (hasKey && i < keyList.size())
316                        {
317                            hasKey = keyList.get(i) == null || keyList.get(i).equals(fields.get(i));
318                            i++;
319                        }
320                        return hasKey;
321                    });
322            }
323            else
324            {
325                return _cache.asMap().containsKey(key);
326            }
327        }
328        else
329        {
330            return _cache.asMap().containsKey(key);
331        }
332    }
333    
334    public Map<K, V> asMap()
335    {
336        Map<K, V> map = new HashMap<>();
337        
338        Map<K, Optional<V>> maps = _cache.asMap();
339        maps.forEach((k, v) -> 
340        {
341            map.put(k, v.orElse(null));
342        });
343        return map;
344    }
345    
346    public boolean isComputableSize()
347    {
348        return _computableSize;
349    }
350
351    public boolean isInitialized()
352    {
353        return _isInitialized;
354    }
355    
356    public boolean isDispatchable()
357    {
358        return _isDispatchable;
359    }
360
361    public Map<K, V> getAll(AbstractCacheKey filterKey)
362    {
363        return this.asMap()
364                .entrySet()
365                .stream()
366                .filter(entry -> _checkKey(entry.getKey(), filterKey.getFields()))
367                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
368    }
369    
370    private boolean _checkKey(K key, List<Object> fields)
371    {
372        if (key instanceof AbstractCacheKey)
373        {
374            if (((AbstractCacheKey) key).getFields().size() != fields.size())
375            {
376                throw new RuntimeException("Complex key lengths does not match");
377            }
378        }
379        else
380        {
381            throw new RuntimeException("Key is not a complex key");
382        }
383        
384        for (int i = 0; i < fields.size(); i++) 
385        {
386            if (fields.get(i) != null && !fields.get(i).equals(((AbstractCacheKey) key).getFields().get(i)))
387            {
388                return false;
389            }
390        }
391        
392        return true;
393    }
394}