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