/*
* =============================================================================
*
* Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* =============================================================================
*/
package org.thymeleaf.cache;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.util.Validate;
/**
*
*
* @author Daniel Fernández
* @author Guven Demir
*
* @since 2.0.0
*
* @param <K> The type of the cache keys
* @param <V> The type of the cache values
*/
public final class StandardCache<K, V> implements ICache<K,V> {
private static final long REPORT_INTERVAL = 300000L; // 5 minutes
private static final String REPORT_FORMAT =
"[THYMELEAF][*][*][*][CACHE_REPORT] %8s elements | %12s puts | %12s gets | %12s hits | %12s misses | %.2f hit ratio | %.2f miss ratio - [%s]";
private volatile long lastExecution = System.currentTimeMillis();
private final String name;
private final boolean useSoftReferences;
private final int maxSize;
private final CacheDataContainer<K,V> dataContainer;
private final ICacheEntryValidityChecker<? super K, ? super V> entryValidityChecker;
private final boolean traceExecution;
private final boolean enableCounters;
private final Logger logger;
private final AtomicLong getCount;
private final AtomicLong putCount;
private final AtomicLong hitCount;
private final AtomicLong missCount;
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final Logger logger) {
this(name, useSoftReferences, initialCapacity, -1, null, logger, false);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final ICacheEntryValidityChecker<? super K, ? super V> entryValidityChecker,
final Logger logger) {
this(name, useSoftReferences, initialCapacity, -1, entryValidityChecker, logger, false);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final int maxSize, final Logger logger) {
this(name, useSoftReferences, initialCapacity, maxSize, null, logger, false);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final int maxSize, final ICacheEntryValidityChecker<? super K, ? super V> entryValidityChecker,
final Logger logger) {
this(name, useSoftReferences, initialCapacity, maxSize, entryValidityChecker, logger, false);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final int maxSize, final ICacheEntryValidityChecker<? super K, ? super V> entryValidityChecker,
final Logger logger, final boolean enableCounters) {
super();
Validate.notEmpty(name, "Name cannot be null or empty");
Validate.isTrue(initialCapacity > 0, "Initial capacity must be > 0");
Validate.isTrue(maxSize != 0, "Cache max size must be either -1 (no limit) or > 0");
this.name = name;
this.useSoftReferences = useSoftReferences;
this.maxSize = maxSize;
this.entryValidityChecker = entryValidityChecker;
this.logger = logger;
this.traceExecution = (logger != null && logger.isTraceEnabled());
this.enableCounters = (this.traceExecution || enableCounters);
this.dataContainer =
new CacheDataContainer<K,V>(this.name, initialCapacity, maxSize, this.traceExecution, this.logger);
this.getCount = new AtomicLong(0);
this.putCount = new AtomicLong(0);
this.hitCount = new AtomicLong(0);
this.missCount = new AtomicLong(0);
if (this.logger != null) {
if (this.maxSize < 0) {
this.logger.trace("[THYMELEAF][CACHE_INITIALIZE] Initializing cache {}. Soft references {}.",
this.name, (this.useSoftReferences? "are used" : "not used"));
} else {
this.logger.trace("[THYMELEAF][CACHE_INITIALIZE] Initializing cache {}. Max size: {}. Soft references {}.",
new Object[] {this.name, Integer.valueOf(this.maxSize), (this.useSoftReferences? "are used" : "not used")});
}
}
}
// -----
public void put(final K key, final V value) {
incrementReportEntity(this.putCount);
final CacheEntry<V> entry = new CacheEntry<V>(value, this.useSoftReferences);
// newSize will be -1 if traceExecution is false
final int newSize = this.dataContainer.put(key, entry);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_ADD][{}] Adding cache entry in cache \"{}\" for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
outputReportIfNeeded();
}
}
public V get(final K key) {
return get(key, this.entryValidityChecker);
}
public V get(final K key, final ICacheEntryValidityChecker<? super K, ? super V> validityChecker) {
incrementReportEntity(this.getCount);
final CacheEntry<V> resultEntry = this.dataContainer.get(key);
if (resultEntry == null) {
incrementReportEntity(this.missCount);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_MISS] Cache miss in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
outputReportIfNeeded();
}
return null;
}
final V resultValue =
resultEntry.getValueIfStillValid(this.name, key, validityChecker, this.traceExecution, this.logger);
if (resultValue == null) {
final int newSize = this.dataContainer.remove(key);
incrementReportEntity(this.missCount);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_REMOVE][{}] Removing cache entry in cache \"{}\" (Entry \"{}\" is not valid anymore). New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_MISS] Cache miss in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
outputReportIfNeeded();
}
return null;
}
incrementReportEntity(this.hitCount);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_HIT] Cache hit in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
outputReportIfNeeded();
}
return resultValue;
}
/**
* <p>
* Returns all the keys contained in this cache. Note this method might return keys for entries
* that are already invalid, so the result of calling {@link #get(Object)} for these keys might
* be <tt>null</tt>.
* </p>
*
* @return the complete set of cache keys. Might include keys for already-invalid (non-cleaned) entries.
* @since 2.1.4
*/
public Set<K> keySet() {
return this.dataContainer.keySet();
}
public void clear() {
this.dataContainer.clear();
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_REMOVE][0] Removing ALL cache entries in cache \"{}\". New size is 0.",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name});
}
}
public void clearKey(final K key) {
final int newSize = this.dataContainer.remove(key);
if (this.traceExecution && newSize != -1) {
this.logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_REMOVE][{}] Removed cache entry in cache \"{}\" for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
}
}
// -----
public String getName() {
return this.name;
}
public boolean hasMaxSize() {
return (this.maxSize > 0);
}
public int getMaxSize() {
return this.maxSize;
}
public boolean getUseSoftReferences() {
return this.useSoftReferences;
}
public int size() {
return this.dataContainer.size();
}
public long getPutCount() {
return this.putCount.get();
}
public long getGetCount() {
return this.getCount.get();
}
public long getHitCount() {
return this.hitCount.get();
}
public long getMissCount(){
return this.missCount.get();
}
public double getHitRatio() {
long hitCount = getHitCount();
long getCount = getGetCount();
if (hitCount == 0 || getCount == 0) {
return 0;
}
return hitCount / getCount;
}
public double getMissRatio() {
return 1 - getHitRatio();
}
// -----
private void incrementReportEntity(final AtomicLong entity) {
if (this.enableCounters) {
entity.incrementAndGet();
}
}
private void outputReportIfNeeded() {
final long currentTime = System.currentTimeMillis();
if ((currentTime - this.lastExecution) >= REPORT_INTERVAL) { // first check without need to sync
synchronized (this) {
if ((currentTime - this.lastExecution) >= REPORT_INTERVAL) {
long hitCount = getHitCount();
long missCount = getMissCount();
long putCount = getPutCount();
long getCount = getGetCount();
double hitRatio = hitCount / getCount;
double missRatio = 1 - hitRatio;
this.logger.trace(
String.format(REPORT_FORMAT,
Integer.valueOf(size()),
putCount,
getCount,
hitCount,
missCount,
hitRatio,
missRatio,
this.name));
this.lastExecution = currentTime;
}
}
}
}
static final class CacheDataContainer<K,V> {
private final String name;
private final boolean sizeLimit;
private final int maxSize;
private final boolean traceExecution;
private final Logger logger;
private final ConcurrentHashMap<K,CacheEntry<V>> container;
private final Object[] fifo;
private int fifoPointer;
CacheDataContainer(final String name, final int initialCapacity,
final int maxSize, final boolean traceExecution, final Logger logger) {
super();
this.name = name;
this.container = new ConcurrentHashMap<K,CacheEntry<V>>(initialCapacity, 0.9f, 2);
this.maxSize = maxSize;
this.sizeLimit = (maxSize >= 0);
if (this.sizeLimit) {
this.fifo = new Object[this.maxSize];
Arrays.fill(this.fifo, null);
} else {
this.fifo = null;
}
this.fifoPointer = 0;
this.traceExecution = traceExecution;
this.logger = logger;
}
public CacheEntry<V> get(final Object key) {
// FIFO is not used for this --> better performance, but no LRU (only insertion order will apply)
return this.container.get(key);
}
public Set<K> keySet() {
// This 'strange' cast is needed in order to keep compatibility with Java 6 and 7, when compiling with
// Java 8. The reason is, the return type of Java 8's ConcurrentHashMap#keySet() changed to a class
// called KeySetView, implementing java.util.Set but new in Java 8. This made this code throw a
// java.lang.NoSuchMethodError when executed in Java 6 or 7.
// By adding the cast, we are binding bytecode not to the specific keySet() method of ConcurrentHashMap,
// but to the one defined at the java.util.Map interface, which simply returns java.util.Set.
return ((Map<K,CacheEntry<V>>)this.container).keySet();
}
public int put(final K key, final CacheEntry<V> value) {
if (this.traceExecution) {
return putWithTracing(key, value);
}
return putWithoutTracing(key, value);
}
private int putWithoutTracing(final K key, final CacheEntry<V> value) {
// If we are not tracing, it's better to avoid the size() operation which has
// some performance implications in ConcurrentHashMap (iteration and counting these maps
// is slow if they are big)
final CacheEntry<V> existing = this.container.putIfAbsent(key, value);
if (existing != null) {
// When not in 'trace' mode, will always return -1
return -1;
}
if (this.sizeLimit) {
synchronized (this.fifo) {
final Object removedKey = this.fifo[this.fifoPointer];
if (removedKey != null) {
this.container.remove(removedKey);
}
this.fifo[this.fifoPointer] = key;
this.fifoPointer = (this.fifoPointer + 1) % this.maxSize;
}
}
return -1;
}
private synchronized int putWithTracing(final K key, final CacheEntry<V> value) {
final CacheEntry<V> existing = this.container.putIfAbsent(key, value);
if (existing == null) {
if (this.sizeLimit) {
final Object removedKey = this.fifo[this.fifoPointer];
if (removedKey != null) {
final CacheEntry<V> removed = this.container.remove(removedKey);
if (removed != null) {
final Integer newSize = Integer.valueOf(this.container.size());
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_REMOVE][{}] Max size exceeded for cache \"{}\". Removing entry for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, newSize, this.name, removedKey, newSize});
}
}
this.fifo[this.fifoPointer] = key;
this.fifoPointer = (this.fifoPointer + 1) % this.maxSize;
}
}
return this.container.size();
}
public int remove(final K key) {
if (this.traceExecution) {
return removeWithTracing(key);
}
return removeWithoutTracing(key);
}
private int removeWithoutTracing(final K key) {
// FIFO is also updated to avoid 'removed' keys remaining at FIFO (which could end up reducing cache size to 1)
final CacheEntry<V> removed = this.container.remove(key);
if (removed != null) {
if (this.sizeLimit && key != null) {
for (int i = 0; i < this.maxSize; i++) {
if (key.equals(this.fifo[i])) {
this.fifo[i] = null;
break;
}
}
}
}
return -1;
}
private synchronized int removeWithTracing(final K key) {
// FIFO is also updated to avoid 'removed' keys remaining at FIFO (which could end up reducing cache size to 1)
final CacheEntry<V> removed = this.container.remove(key);
if (removed == null) {
// When tracing is active, this means nothing was removed
return -1;
}
if (this.sizeLimit && key != null) {
for (int i = 0; i < this.maxSize; i++) {
if (key.equals(this.fifo[i])) {
this.fifo[i] = null;
break;
}
}
}
return this.container.size();
}
public void clear() {
this.container.clear();
}
public int size() {
return this.container.size();
}
}
static final class CacheEntry<V> {
private final SoftReference<V> cachedValueReference;
private final long creationTimeInMillis;
// Although we will use the reference for normal operation for cleaner code, this
// variable will act as an "anchor" to avoid the value to be cleaned if we don't
// want the reference type to be "soft"
@SuppressWarnings("unused")
private final V cachedValueAnchor;
CacheEntry(final V cachedValue, final boolean useSoftReferences) {
super();
this.cachedValueReference = new SoftReference<V>(cachedValue);
this.cachedValueAnchor = (!useSoftReferences? cachedValue : null);
this.creationTimeInMillis = System.currentTimeMillis();
}
public <K> V getValueIfStillValid(final String cacheMapName,
final K key, final ICacheEntryValidityChecker<? super K, ? super V> checker,
final boolean traceExecution, final Logger logger) {
final V cachedValue = this.cachedValueReference.get();
if (cachedValue == null) {
// The soft reference has been cleared by GC -> Memory could be running low
if (traceExecution) {
logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_DELETED_REFERENCES] Some entries at cache \"{}\" " +
"seem to have been sacrificed by the Garbage Collector (soft references).",
new Object[] {TemplateEngine.threadIndex(), cacheMapName, cacheMapName});
}
return null;
}
if (checker == null || checker.checkIsValueStillValid(key, cachedValue, this.creationTimeInMillis)) {
return cachedValue;
}
return null;
}
public long getCreationTimeInMillis() {
return this.creationTimeInMillis;
}
}
}