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