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.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.CascadingRuntimeException;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.commons.lang3.tuple.Pair;
034
035import org.ametys.core.ui.Callable;
036import org.ametys.runtime.i18n.I18nizableText;
037import org.ametys.runtime.plugin.component.AbstractLogEnabled;
038
039/**
040 * Component that handle all the caches
041 */
042public abstract class AbstractCacheManager extends AbstractLogEnabled implements Component, Contextualizable
043{
044
045    /** The type of cache */
046    public enum CacheType
047    {
048        /** A cache used for a request */
049        REQUEST,
050        /** A cache stored in memory */
051        MEMORY
052    }
053
054    /** Role */
055    public static final String ROLE = AbstractCacheManager.class.getPackageName() + ".CacheManager";
056
057    /** Map linking id with persistent caches */
058    protected Map<String, Cache> _memoryCaches = new HashMap<>();
059
060    /** Map linking id with CacheInfo of a request cache */
061    protected Map<String, CacheInfo> _requestsCacheInfos = new HashMap<>();
062
063    /** HashMap linking an id with a List of WeakReference of Caches. this mean values can be destroyed by the garbage collector */
064    protected Map<String, List<WeakReference<Cache>>> _requestCaches = new HashMap<>();
065
066    /** Avalon context */
067    protected Context _context;
068
069    public void contextualize(Context context) throws ContextException
070    {
071        _context = context;
072    }
073
074    /**
075     * create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 
076     * create a CacheInfo to create the cache later otherwise
077     * @param id id of the cache
078     * @param name name of the cache
079     * @param description description
080     * @param cacheType type of the cache (REQUEST or MEMORY)
081     * @param computableSize true if the size of the cache can be computed
082     * @throws CacheException if a cache already exists for the id
083     */
084    public void createCache(String id, I18nizableText name, I18nizableText description, CacheType cacheType, boolean computableSize) throws CacheException
085    {
086        if (this._memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id))
087        {
088            throw new CacheException("The cache '" + id + "' already exists");
089        }
090
091        Cache ametysCache = _createCache(id, name, description, computableSize);
092
093        if (cacheType == CacheType.MEMORY)
094        {
095            _memoryCaches.put(id, ametysCache);
096        }
097        else
098        {
099            _requestsCacheInfos.put(id, new CacheInfo(name, description, computableSize));
100            synchronized (_requestCaches)
101            {
102                _requestCaches.put(id, new ArrayList<>());
103            }
104        }
105    }
106    
107    /**
108     * Remove the cache identified by the given id.
109     * @param id id of the cache
110     * @param cacheType type of the cache
111     * @throws CacheException if the cache does not exist for the id and type
112     */
113    public synchronized void removeCache(String id, CacheType cacheType) throws CacheException
114    {
115        switch (cacheType)
116        {
117            case MEMORY:
118                if (_memoryCaches.containsKey(id))
119                {
120                    _memoryCaches.remove(id);
121                    return;
122                }
123                break;
124            case REQUEST:
125                if (_requestsCacheInfos.containsKey(id))
126                {
127                    _requestsCacheInfos.remove(id);
128                    _requestCaches.remove(id);
129                    return;
130                }
131                break;
132            default:
133                throw new IllegalStateException("Unknown CacheType " + cacheType);
134        }
135        
136        throw new CacheException("The cache '" + id + "' does not exist");
137    }
138
139    /**
140     * Get the cache by id. If it's a request cache, create it and store it in request and in _requestCaches map.
141     * @param <K> the type of the keys in cache
142     * @param <V> the type of the values in cache
143     * @param id id of the cache
144     * @return the cache related to the id
145     * @throws CacheException if no cache exist for the id
146     */
147    @SuppressWarnings("unchecked")
148    public <K, V> Cache<K, V> get(String id) throws CacheException
149    {
150        if (!_memoryCaches.containsKey(id) && !_requestsCacheInfos.containsKey(id))
151        {
152            throw new CacheException("Cache " + id + " does not exist ");
153        }
154
155        if (_memoryCaches.containsKey(id))
156        {
157            return _memoryCaches.get(id);
158        }
159        else
160        {
161            Request request = null;
162            try
163            {
164                request = ContextHelper.getRequest(_context);
165            }
166            catch (CascadingRuntimeException e)
167            {
168                // Nothing... request is null
169                getLogger().debug("No request available when getting cache {}", id, e);
170            }
171
172            Cache<K, V> requestCache = request != null ? (Cache<K, V>) request.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null;
173            if (requestCache == null)
174            {
175                CacheInfo cacheInfo = _requestsCacheInfos.get(id);
176                requestCache = _createCache(id, cacheInfo.getName(), cacheInfo.getDescription(), cacheInfo.isComputableSize());
177                synchronized (_requestCaches)
178                {
179                    _requestCaches.get(id).add(new WeakReference<Cache>(requestCache));
180                }
181                if (request != null)
182                {
183                    request.setAttribute(AbstractCacheManager.ROLE + "$" + id, requestCache);
184                }
185            }
186
187            return requestCache;
188        }
189    }
190
191    /**
192     * Get all caches classified by Identifier and CacheType. All caches includes all running request caches in any existing request.
193     * @return all cache
194     */
195    public Map<Pair<String, CacheType>, List<Cache>> getAllCaches()
196    {
197        Map<Pair<String, CacheType>, List<Cache>> caches = new HashMap<>();
198
199        _memoryCaches.forEach((id, cache) -> caches.put(Pair.of(id, CacheType.MEMORY), Collections.singletonList(cache)));
200
201        // clean weak references
202        synchronized (_requestCaches)
203        {
204            // Clean the list of destroyed request
205            _requestCaches.forEach((id, cacheList) -> _requestCaches.put(id, cacheList.stream().filter(wr -> wr.get() != null).collect(Collectors.toList())));
206            
207            _requestCaches.forEach((id, cacheList) -> caches.put(Pair.of(id, CacheType.REQUEST), cacheList.stream().map(wr -> wr.get()).collect(Collectors.toList())));
208        }
209        
210        return caches;
211    }
212
213    /**
214     * Get list of memory caches in JSON format
215     * @return the memory caches in JSON format
216     */
217    @Callable
218    public List<Map<String, Object>> getCachesAsJSONMap()
219    {
220        List<Map<String, Object>> properties = new ArrayList<>();
221        _memoryCaches.forEach((k, v) ->
222        {
223            properties.add(v.toJSONMap(getLogger()));
224        });
225        return properties;
226    }
227
228    /**
229     * set new max size to the cache related to given id
230     * @param id the id of cache
231     * @param size the size of the cache in bytes
232     * @return true if success
233     * @throws CacheException throw CacheException if the key is null or invalid
234     * @throws UnsupportedOperationException not implemented yet
235     */
236    @Callable
237    public boolean setSize(String id, long size) throws CacheException, UnsupportedOperationException
238    {
239        throw new UnsupportedOperationException("NOT IMPLEMENTED YET");
240    }
241
242    /**
243     * create a new cache
244     * @param <K> Key type of the cache
245     * @param <V> Value type of the cache
246     * @param id the id of the cache
247     * @param name the name of the cache
248     * @param description the description of the cache
249     * @param computableSize true if the size of the cache can be computed
250     * @return new cache
251     */
252    protected abstract <K, V> Cache<K, V> _createCache(String id, I18nizableText name, I18nizableText description, boolean computableSize);
253
254    /**
255     * Encapsulation of name and description of a cache
256     */
257    protected static final class CacheInfo
258    {
259
260        private I18nizableText _name;
261
262        private I18nizableText _description;
263        
264        private boolean _computableSize;
265
266        /**
267         * Create new CacheInfo with name and description
268         * @param name the name of the CacheInfo
269         * @param description the description of the CacheInfo
270         * @param computableSize true if the size of the cache can be computed
271         */
272        public CacheInfo(I18nizableText name, I18nizableText description, boolean computableSize)
273        {
274            _name = name;
275            _description = description;
276            _computableSize = computableSize;
277        }
278
279        /**
280         * get the name of the CacheInfo
281         * @return the name of the CacheInfo
282         */
283        public I18nizableText getName()
284        {
285            return _name;
286        }
287
288        /**
289         * get the description of the CacheInfo
290         * @return the description of the CacheInfo
291         */
292        public I18nizableText getDescription()
293        {
294            return _description;
295        }
296
297        /**
298         * Is the cache size computable
299         * @return true if the cache size is computable
300         */
301        public boolean isComputableSize()
302        {
303            return _computableSize;
304        }
305    }
306}