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}