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.core.cache;
017
018import java.lang.ref.WeakReference;
019import java.time.Duration;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.stream.Collectors;
026
027import javax.servlet.http.HttpServletRequest;
028
029import org.apache.avalon.framework.CascadingRuntimeException;
030import org.apache.avalon.framework.activity.Initializable;
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.cocoon.components.ContextHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.commons.lang3.Strings;
042import org.apache.commons.lang3.tuple.Pair;
043
044import org.ametys.core.ui.Callable;
045import org.ametys.plugins.core.impl.cache.GuavaCacheStats;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048import org.ametys.runtime.request.RequestListener;
049import org.ametys.runtime.request.RequestListenerManager;
050
051
052/**
053 * Component that handle all the caches
054 */
055public abstract class AbstractCacheManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable, RequestListener
056{
057
058    /** The type of cache */
059    public enum CacheType
060    {
061        /** A cache used for a request */
062        REQUEST,
063        /** A cache stored in memory */
064        MEMORY
065    }
066
067    /** Role */
068    public static final String ROLE = AbstractCacheManager.class.getPackageName() + ".CacheManager";
069
070    /** Map linking id with persistent caches */
071    protected Map<String, Cache> _memoryCaches = new HashMap<>();
072
073    /** Map linking id with CacheInfo of a request cache */
074    protected Map<String, CacheInfo> _requestsCacheInfos = new HashMap<>();
075
076    /** Map linking id with CacheStats of a request cache */
077    protected Map<String, CacheStats> _requestsCacheStats = new HashMap<>();
078
079    /** HashMap linking an id with a List of WeakReference of Caches. this mean values can be destroyed by the garbage collector */
080    protected Map<String, List<WeakReference<Cache>>> _requestCaches = new HashMap<>();
081
082    /** Avalon context */
083    protected Context _context;
084
085    /** RequestListener manager */
086    protected RequestListenerManager _requestListenerManager;
087
088    @Override
089    public void initialize() throws Exception
090    {
091        _requestListenerManager.registerListener(this);
092    }
093    
094    
095    @Override
096    public void service(ServiceManager serviceManager) throws ServiceException
097    {
098        _requestListenerManager = (RequestListenerManager) serviceManager.lookup(RequestListenerManager.ROLE);
099    }
100    
101
102    public void requestEnded(HttpServletRequest req)
103    {
104        _requestsCacheInfos.keySet().forEach(id ->
105        {
106            Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null;
107            if (requestCache != null)
108            {
109                requestCache.getId();
110                CacheStats cacheStats = requestCache.getCacheStats();
111                CacheStats totalCacheStats = _requestsCacheStats.get(id);
112                totalCacheStats = totalCacheStats.plus(cacheStats);
113                _requestsCacheStats.put(id, totalCacheStats);
114            }
115        });
116        
117    }
118    
119    /**
120     * Called whenever a request caches' statistics need to be manually refreshed
121     * @param req the processed request
122     * @param id the if of the cache to refresh
123     */
124    public void refreshStats(Request req, String id)
125    {
126        Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null;
127        if (requestCache != null)
128        {
129            requestCache.getId();
130            CacheStats cacheStats = requestCache.getCacheStats();
131            CacheStats totalCacheStats = _requestsCacheStats.get(id);
132            totalCacheStats = totalCacheStats.plus(cacheStats);
133            _requestsCacheStats.put(id, totalCacheStats);
134        }
135    }
136    
137    public void requestStarted(HttpServletRequest req)
138    {
139        // Nothing to do on start
140    }
141    
142    public void contextualize(Context context) throws ContextException
143    {
144        _context = context;
145    }
146
147    /**
148     * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType,
149     * Create a CacheInfo to create the cache later otherwise
150     * @param id id of the cache
151     * @param name name of the cache
152     * @param description description
153     * @param computableSize true if the size of the cache can be computed
154     * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches
155     * @throws CacheException if a cache already exists for the id
156     */
157    public void createMemoryCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration) throws CacheException
158    {
159        _createCache(id, name, description, CacheType.MEMORY, computableSize, duration, false);
160    }
161
162    /**
163     * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType,
164     * Create a CacheInfo to create the cache later otherwise
165     * @param id id of the cache
166     * @param name name of the cache
167     * @param description description
168     * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator
169     * @throws CacheException if a cache already exists for the id
170     */
171    public void createRequestCache(String id, I18nizableText name, I18nizableText description, boolean isDispatchable) throws CacheException
172    {
173        _createCache(id, name, description, CacheType.REQUEST, false, null, isDispatchable);
174    }
175    
176    /**
177     * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType,
178     * Create a CacheInfo to create the cache later otherwise
179     * @param id id of the cache
180     * @param name name of the cache
181     * @param description description
182     * @param cacheType type of the cache (REQUEST or MEMORY)
183     * @param computableSize true if the size of the cache can be computed
184     * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches
185     * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator
186     * @throws CacheException if a cache already exists for the id
187     */
188    protected void _createCache(String id, I18nizableText name, I18nizableText description, CacheType cacheType, boolean computableSize, Duration duration, boolean isDispatchable) throws CacheException
189    {
190        if (this._memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id))
191        {
192            throw new CacheException("The cache '" + id + "' already exists");
193        }
194
195        if (cacheType == CacheType.MEMORY)
196        {
197            Cache ametysCache = _createCache(id, name, description, computableSize, duration, false);
198            _memoryCaches.put(id, ametysCache);
199        }
200        else
201        {
202            _requestsCacheInfos.put(id, new CacheInfo(name, description, isDispatchable));
203            _requestsCacheStats.put(id, new GuavaCacheStats());
204            synchronized (_requestCaches)
205            {
206                _requestCaches.put(id, new ArrayList<>());
207            }
208        }
209    }
210    
211    /**
212     * Remove the cache identified by the given id.
213     * @param id id of the cache
214     * @param cacheType type of the cache
215     * @throws CacheException if the cache does not exist for the id and type
216     */
217    public synchronized void removeCache(String id, CacheType cacheType) throws CacheException
218    {
219        switch (cacheType)
220        {
221            case MEMORY:
222                if (_memoryCaches.containsKey(id))
223                {
224                    _memoryCaches.remove(id);
225                    return;
226                }
227                break;
228            case REQUEST:
229                if (_requestsCacheInfos.containsKey(id))
230                {
231                    _requestsCacheInfos.remove(id);
232                    _requestCaches.remove(id);
233                    _requestsCacheStats.remove(id);
234                    return;
235                }
236                break;
237            default:
238                throw new IllegalStateException("Unknown CacheType " + cacheType);
239        }
240        
241        throw new CacheException("The cache '" + id + "' does not exist");
242    }
243
244    /**
245     * Get the cache by id. If it's a request cache, create it and store it in request and in _requestCaches map.
246     * @param <K> the type of the keys in cache
247     * @param <V> the type of the values in cache
248     * @param id id of the cache
249     * @return the cache related to the id
250     * @throws CacheException if no cache exist for the id
251     */
252    @SuppressWarnings("unchecked")
253    public <K, V> Cache<K, V> get(String id) throws CacheException
254    {
255        if (!_memoryCaches.containsKey(id) && !_requestsCacheInfos.containsKey(id))
256        {
257            throw new CacheException("Cache " + id + " does not exist ");
258        }
259
260        if (_memoryCaches.containsKey(id))
261        {
262            return _memoryCaches.get(id);
263        }
264        else
265        {
266            Request request = null;
267            try
268            {
269                request = ContextHelper.getRequest(_context);
270            }
271            catch (CascadingRuntimeException e)
272            {
273                // Nothing... request is null
274                getLogger().debug("No request available when getting cache {}", id, e);
275            }
276
277            Cache<K, V> requestCache = request != null ? (Cache<K, V>) request.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null;
278            if (requestCache == null)
279            {
280                CacheInfo cacheInfo = _requestsCacheInfos.get(id);
281                requestCache = _createCache(id, cacheInfo.getName(), cacheInfo.getDescription(), false, null, cacheInfo.isDispatchable());
282                synchronized (_requestCaches)
283                {
284                    _requestCaches.get(id).add(new WeakReference<>(requestCache));
285                }
286                if (request != null)
287                {
288                    request.setAttribute(AbstractCacheManager.ROLE + "$" + id, requestCache);
289                }
290            }
291
292            return requestCache;
293        }
294    }
295    
296    /**
297     * Get all the memory caches identified by Id
298     * @return all memory caches
299     */
300    public List<Cache> getAllMemoryCaches()
301    {
302        return new ArrayList<>(_memoryCaches.values());
303    }
304
305    /**
306     * Get all caches classified by Identifier and CacheType. All caches includes all running request caches in any existing request.
307     * @return all cache
308     */
309    public Map<Pair<String, CacheType>, List<Cache>> getAllCaches()
310    {
311        Map<Pair<String, CacheType>, List<Cache>> caches = new HashMap<>();
312
313        _memoryCaches.forEach((id, cache) -> caches.put(Pair.of(id, CacheType.MEMORY), Collections.singletonList(cache)));
314
315        // clean weak references
316        synchronized (_requestCaches)
317        {
318            // Clean the list of destroyed request
319            _requestCaches.forEach((id, cacheList) -> _requestCaches.put(id, cacheList.stream().filter(wr -> wr.get() != null).collect(Collectors.toList())));
320            
321            _requestCaches.forEach((id, cacheList) -> caches.put(Pair.of(id, CacheType.REQUEST), cacheList.stream().map(wr -> wr.get()).collect(Collectors.toList())));
322        }
323        
324        return caches;
325    }
326
327    /**
328     * Get list of memory caches in JSON format
329     * @return the memory caches in JSON format
330     */
331    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
332    public List<Map<String, Object>> getCachesAsJSONMap()
333    {
334        List<Map<String, Object>> properties = new ArrayList<>();
335        _memoryCaches.forEach((k, v) ->
336        {
337            properties.add(v.toJSONMap(getLogger()));
338        });
339        _requestsCacheStats.forEach((k, v) ->
340        {
341            properties.add(this.toJSONMap(k, _requestsCacheInfos.get(k), v));
342        });
343        return properties;
344    }
345    
346    /**
347     * Returns true if the cache with given id exists
348     * @param id id of the cache
349     * @return true if the cache with given id exists
350     */
351    public boolean hasCache(String id)
352    {
353        return _memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id);
354    }
355
356    /**
357     * set new max size to the cache related to given id
358     * @param id the id of cache
359     * @param size the size of the cache in bytes
360     * @return true if success
361     * @throws CacheException throw CacheException if the key is null or invalid
362     * @throws UnsupportedOperationException not implemented yet
363     */
364    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
365    public boolean setSize(String id, long size) throws CacheException, UnsupportedOperationException
366    {
367        throw new UnsupportedOperationException("NOT IMPLEMENTED YET");
368    }
369
370    /**
371     * Create a new cache
372     * @param <K> Key type of the cache
373     * @param <V> Value type of the cache
374     * @param id the id of the cache
375     * @param name the name of the cache
376     * @param description the description of the cache
377     * @param computableSize true if the size of the cache can be computed
378     * @param duration the length of time after an entry is created that it should be automatically removed
379     * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator
380     * @return new cache
381     */
382    protected abstract <K, V> Cache<K, V> _createCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration, boolean isDispatchable);
383
384    /**
385     * Encapsulation of name and description of a cache
386     */
387    protected static final class CacheInfo
388    {
389
390        private I18nizableText _name;
391
392        private I18nizableText _description;
393
394        private boolean _isDispatchable;
395        
396        /**
397         * Create new CacheInfo with name and description
398         * @param name the name of the CacheInfo
399         * @param description the description of the CacheInfo
400         * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator
401         */
402        public CacheInfo(I18nizableText name, I18nizableText description, boolean isDispatchable)
403        {
404            _name = name;
405            _description = description;
406            _isDispatchable = isDispatchable;
407        }
408
409        /**
410         * Get the name of the CacheInfo
411         * @return the name of the CacheInfo
412         */
413        public I18nizableText getName()
414        {
415            return _name;
416        }
417
418        /**
419         * Get the description of the CacheInfo
420         * @return the description of the CacheInfo
421         */
422        public I18nizableText getDescription()
423        {
424            return _description;
425        }
426        
427        /**
428         * Can the cache be transmitted in sub-requests of DispatchGenerator
429         * @return true if the cache can be transmitted in sub-requests of DispatchGenerator
430         */
431        public boolean isDispatchable()
432        {
433            return _isDispatchable;
434        }
435
436    }
437    
438    private Map<String, Object> toJSONMap(String id, CacheInfo cacheInfo, CacheStats cacheStats)
439    {
440        Map<String, Object> properties = new HashMap<>();
441        properties.put("id", id);
442        properties.put("label", cacheInfo.getName());
443        properties.put("description", cacheInfo.getDescription());
444        properties.put("computableSize", false);
445        properties.put("access", cacheStats.requestCount());
446        properties.put("hit", cacheStats.hitCount());
447        properties.put("hitRate", cacheStats.hitRate());
448        properties.put("miss", cacheStats.missCount());
449        properties.put("missRate", cacheStats.missRate());
450        properties.put("nbEviction", cacheStats.evictionCount());
451        properties.put("type", CacheType.REQUEST);
452        properties.put("currentSize", 0);
453        properties.put("maxSize", 0);
454        return properties;
455    }
456
457    /**
458     * Reset all memory caches
459     * @return the list of reseted cache's id
460     */
461    public List<String> resetAllMemoryCaches()
462    {
463        List<String> cleanedCacheIds = new ArrayList<>();
464        for (Cache cache : getAllMemoryCaches())
465        {
466            cache.resetCache();
467            
468            cleanedCacheIds.add(cache.getId());
469        }
470        return cleanedCacheIds;
471    }
472
473    /**
474     * Reset the cache with provided ids
475     * @param ids the list of cache to reset
476     * @return the ids of cache that were actually reseted
477     */
478    public List<String> resetCaches(List<String> ids)
479    {
480        List<String> cleanedCacheIds = new ArrayList<>();
481        for (String id : ids)
482        {
483            try
484            {
485                get(id).resetCache();
486                cleanedCacheIds.add(id);
487            }
488            catch (CacheException e)
489            {
490                getLogger().warn("Failed to clear cache with id '{}'. No cache exists with this id.", id, e);
491            }
492        }
493        return cleanedCacheIds;
494    }
495    
496    /**
497     * Reset all request caches that are marked as non dispatchable
498     */
499    public void resetAllNonDispatchableRequestCaches()
500    {
501        Request request = null;
502        try
503        {
504            request = ContextHelper.getRequest(_context);
505        }
506        catch (CascadingRuntimeException e)
507        {
508            // Nothing... request is null
509            getLogger().debug("No request available when resetting non dispatchable request caches", e);
510            return;
511        }
512        
513        @SuppressWarnings("unchecked")
514        List<String> attrNames = Collections.list(request.getAttributeNames());
515        for (String attrName : attrNames)
516        {
517            if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE))
518            {
519                String id = Strings.CS.replace(attrName, AbstractCacheManager.ROLE + "$", StringUtils.EMPTY);
520                Cache<Object, Object> cache = get(id);
521                if (!cache.isDispatchable())
522                {
523                    refreshStats(request, id);
524                    cache.resetCache();
525                }
526            }
527        }
528    }
529}