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.plugins.core.impl.cache; 017 018import java.lang.reflect.Field; 019import java.time.Duration; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.concurrent.Callable; 026import java.util.concurrent.ExecutionException; 027import java.util.function.Function; 028import java.util.stream.Collectors; 029 030import org.ametys.core.cache.Cache; 031import org.ametys.core.cache.CacheException; 032import org.ametys.core.cache.CacheStats; 033import org.ametys.core.util.SizeUtils; 034import org.ametys.runtime.i18n.I18nizableText; 035 036import com.google.common.cache.CacheBuilder; 037import com.google.common.cache.Weigher; 038import com.google.common.util.concurrent.ExecutionError; 039import com.google.common.util.concurrent.UncheckedExecutionException; 040 041/** 042 * Implementation of AmetysCache with Guava library 043 * 044 * @param <K> the key 045 * @param <V> the value 046 */ 047public class GuavaCache<K, V> implements Cache<K, V> 048{ 049 050 /** the cache itself, using Guava implementation */ 051 protected com.google.common.cache.Cache<K, Optional<V>> _cache; 052 053 /** Id of the cache */ 054 protected String _id; 055 056 /** Label of the cache */ 057 protected I18nizableText _label; 058 059 /** Description of the cache */ 060 protected I18nizableText _description; 061 062 /** Maximum size of the cache in bytes */ 063 protected long _size; 064 065 /** True if the size of the cache can be computed, false otherwise (e.g., the cache store a component) */ 066 protected boolean _computableSize; 067 068 /** The length of time after an entry is created that it should be automatically*/ 069 protected Duration _duration; 070 071 /** True if the put or putAll have been called (even without values), false otherwises. become false again when the cache is reseted or if all values have been cleaned. */ 072 protected boolean _isInitialized; 073 074 /** True if the cache can be transmitted in sub-requests of DispatchGenerator, false otherwises. */ 075 protected boolean _isDispatchable; 076 077 078 /** 079 * Implementation of AmetysCache with Guava library 080 * @param id id of the cache 081 * @param label label of the cache 082 * @param description description 083 * @param size maximum size of the cache 084 * @param computableSize true if the size of the cache can be computed 085 * @param duration the length of time after an entry is created that it should be automatically removed 086 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 087 */ 088 public GuavaCache(String id, I18nizableText label, I18nizableText description, long size, boolean computableSize, Duration duration, boolean isDispatchable) 089 { 090 _label = label; 091 _description = description; 092 _id = id; 093 _size = size; 094 _computableSize = computableSize; 095 _duration = duration; 096 _isInitialized = false; 097 _isDispatchable = isDispatchable; 098 resetCache(); 099 } 100 101 public void resetCache() 102 { 103 CacheBuilder cacheBuilder = com.google.common.cache.CacheBuilder.newBuilder(); 104 105 if (_duration != null) 106 { 107 cacheBuilder = cacheBuilder.expireAfterWrite(_duration); 108 } 109 _cache = cacheBuilder.recordStats() 110 .maximumWeight(_size) 111 .weigher(new Weigher<K, Optional<V>>() 112 { 113 public int weigh(K k, Optional<V> v) 114 { 115 if (_computableSize) 116 { 117 return (int) (SizeUtils.sizeOf(k) + SizeUtils.sizeOf(v.orElse(null))); 118 } 119 else 120 { 121 return 0; 122 } 123 } 124 }).build(); 125 _isInitialized = false; 126 } 127 128 public V get(K key, Function<K, V> function) throws CacheException 129 { 130 if (key instanceof AbstractCacheKey) 131 { 132 if (((AbstractCacheKey) key).isPartialKey()) 133 { 134 throw new CacheException("Complex key is not valid because it contains null values"); 135 } 136 } 137 try 138 { 139 return _cache.get(key, new Callable<Optional<V>>() 140 { 141 @Override 142 public Optional<V> call() 143 { 144 return Optional.ofNullable(function.apply(key)); 145 } 146 }).orElse(null); 147 } 148 catch (ExecutionException | ExecutionError | UncheckedExecutionException e) 149 { 150 // assume that the real exception is always e.getCause() as LocalCache wrap the exception into a ExecutionException, ExecutionError or UncheckedExecutionException 151 throw new CacheException("An error occurred while computing the new value for key " + key, e.getCause()); 152 } 153 } 154 155 public V get(K key) 156 { 157 Optional<V> optional = _cache.getIfPresent(key); 158 if (optional != null) 159 { 160 return optional.orElse(null); 161 } 162 else 163 { 164 return null; 165 } 166 } 167 168 public void put(K key, V value) 169 { 170 if (key instanceof AbstractCacheKey) 171 { 172 if (((AbstractCacheKey) key).isPartialKey()) 173 { 174 throw new RuntimeException("complex key is not valid because it contains null values"); 175 } 176 } 177 _cache.put(key, Optional.ofNullable(value)); 178 _isInitialized = true; 179 } 180 181 public void putAll(Map<K, V> map) 182 { 183 map.forEach((k, v) -> 184 { 185 _cache.put(k, Optional.ofNullable(v)); 186 }); 187 _isInitialized = true; 188 } 189 190 public CacheStats getCacheStats() 191 { 192 return new GuavaCacheStats(_cache.stats()); 193 } 194 195 public long getMemorySize() throws CacheException 196 { 197 if (!_computableSize) 198 { 199 return -1; 200 } 201 try 202 { 203 long sum = 0; 204 Field localCacheField = _cache.getClass().getDeclaredField("localCache"); // NoSuchFieldException 205 localCacheField.setAccessible(true); 206 var localCache = localCacheField.get(_cache); // IllegalAccessException 207 208 Field segmentsField = localCache.getClass().getDeclaredField("segments"); // NoSuchFieldException 209 segmentsField.setAccessible(true); 210 final Object[] segments = (Object[]) segmentsField.get(localCache); // IllegalAccessException 211 212 for (int i = 0; i < segments.length; i++) 213 { 214 Field totalWeight = segments[i].getClass().getDeclaredField("totalWeight"); // NoSuchFieldException 215 totalWeight.setAccessible(true); 216 long totalWeightValue = (long) totalWeight.get(segments[i]); // IllegalAccessException 217 sum += totalWeightValue; 218 } 219 220 return sum; 221 222 } 223 catch (Exception e) 224 { 225 throw new CacheException("Cannot compute object size", e); 226 } 227 } 228 229 public long getNumberOfElements() 230 { 231 return _cache.size(); 232 } 233 234 public void invalidate(K key) 235 { 236 List<K> keyToRemove = new ArrayList<>(); 237 238 if (key instanceof AbstractCacheKey) 239 { 240 AbstractCacheKey invalidationKey = (AbstractCacheKey) key; 241 if (invalidationKey.isPartialKey()) 242 { 243 // Remove null element 244 List<Object> keyList = invalidationKey.getFields(); 245 246 keyToRemove = _cache.asMap().keySet().stream().filter(k -> 247 { 248 AbstractCacheKey abstractK = (AbstractCacheKey) k; 249 boolean isEquals = true; 250 251 for (int i = 0; i < keyList.size(); i++) 252 { 253 isEquals = isEquals && (keyList.get(i) == null || keyList.get(i).equals(abstractK.getFields().get(i))); 254 } 255 256 return isEquals; 257 258 }).collect(Collectors.toList()); 259 } 260 else 261 { 262 keyToRemove.add(key); 263 } 264 } 265 else 266 { 267 keyToRemove.add(key); 268 } 269 _cache.invalidateAll(keyToRemove); 270 271 } 272 273 public void invalidateAll() 274 { 275 _cache.invalidateAll(); 276 _isInitialized = false; 277 } 278 279 public I18nizableText getDescription() 280 { 281 return _description; 282 } 283 284 public I18nizableText getLabel() 285 { 286 return _label; 287 } 288 289 public String getId() 290 { 291 return _id; 292 } 293 294 public long getMaxSize() 295 { 296 return _size; 297 } 298 299 public boolean hasKey(K key) 300 { 301 if (key instanceof AbstractCacheKey) 302 { 303 AbstractCacheKey invalidationKey = (AbstractCacheKey) key; 304 if (invalidationKey.isPartialKey()) 305 { 306 // we ignore null element 307 List<Object> keyList = invalidationKey.getFields(); 308 309 return _cache.asMap().keySet().stream() 310 .anyMatch(k -> { 311 boolean hasKey = true; 312 AbstractCacheKey abstractKey = (AbstractCacheKey) k; 313 List<Object> fields = abstractKey.getFields(); 314 int i = 0; 315 while (hasKey && i < keyList.size()) 316 { 317 hasKey = keyList.get(i) == null || keyList.get(i).equals(fields.get(i)); 318 i++; 319 } 320 return hasKey; 321 }); 322 } 323 else 324 { 325 return _cache.asMap().containsKey(key); 326 } 327 } 328 else 329 { 330 return _cache.asMap().containsKey(key); 331 } 332 } 333 334 public Map<K, V> asMap() 335 { 336 Map<K, V> map = new HashMap<>(); 337 338 Map<K, Optional<V>> maps = _cache.asMap(); 339 maps.forEach((k, v) -> 340 { 341 map.put(k, v.orElse(null)); 342 }); 343 return map; 344 } 345 346 public boolean isComputableSize() 347 { 348 return _computableSize; 349 } 350 351 public boolean isInitialized() 352 { 353 return _isInitialized; 354 } 355 356 public boolean isDispatchable() 357 { 358 return _isDispatchable; 359 } 360 361 public Map<K, V> getAll(AbstractCacheKey filterKey) 362 { 363 return this.asMap() 364 .entrySet() 365 .stream() 366 .filter(entry -> _checkKey(entry.getKey(), filterKey.getFields())) 367 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 368 } 369 370 private boolean _checkKey(K key, List<Object> fields) 371 { 372 if (key instanceof AbstractCacheKey) 373 { 374 if (((AbstractCacheKey) key).getFields().size() != fields.size()) 375 { 376 throw new RuntimeException("Complex key lengths does not match"); 377 } 378 } 379 else 380 { 381 throw new RuntimeException("Key is not a complex key"); 382 } 383 384 for (int i = 0; i < fields.size(); i++) 385 { 386 if (fields.get(i) != null && !fields.get(i).equals(((AbstractCacheKey) key).getFields().get(i))) 387 { 388 return false; 389 } 390 } 391 392 return true; 393 } 394}