All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.thymeleaf.cache.StandardCache Maven / Gradle / Ivy

The newest version!
/*
 * =============================================================================
 *
 *   Copyright (c) 2011-2013, 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.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  The type of the cache keys
 * @param  The type of the cache values
 */
public final class StandardCache implements ICache {

    
    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 - [%s]";
    private volatile long lastExecution = System.currentTimeMillis();
    
    private final String name;
    private final boolean useSoftReferences;
    private final int maxSize;
    private final CacheDataContainer dataContainer;
    private final ICacheEntryValidityChecker entryValidityChecker;

    private final boolean traceExecution;
    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);
    }

    public StandardCache(final String name, final boolean useSoftReferences, 
            final int initialCapacity, final ICacheEntryValidityChecker entryValidityChecker, 
            final Logger logger) {
        this(name, useSoftReferences, initialCapacity, -1, entryValidityChecker, logger);
    }

    public StandardCache(final String name, final boolean useSoftReferences, 
            final int initialCapacity, final int maxSize, final Logger logger) {
        this(name, useSoftReferences, initialCapacity, maxSize, null, logger);
    }

    public StandardCache(final String name, final boolean useSoftReferences, 
            final int initialCapacity, final int maxSize, final ICacheEntryValidityChecker entryValidityChecker, 
            final Logger logger) {
        
        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.dataContainer = 
                new CacheDataContainer(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.debug("[THYMELEAF][CACHE_INITIALIZE] Initializing cache {}. Soft references {}.", 
                        this.name, (this.useSoftReferences? "are used" : "not used"));
            } else {
                this.logger.debug("[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 entry = new CacheEntry(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 validityChecker) {
        
        incrementReportEntity(this.getCount);
        
        final CacheEntry 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);
            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});
            }
            incrementReportEntity(this.missCount);
            outputReportIfNeeded();
            return null;
        }
        
        if (this.traceExecution) {
            this.logger.trace(
                    "[THYMELEAF][{}][{}][CACHE_HIT] Cache hit in cache \"{}\" for key \"{}\".", 
                    new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
        }
        
        incrementReportEntity(this.hitCount);
        outputReportIfNeeded();
        return resultValue;
        
    }

    

    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();
    }

    
    
    // -----

    
    private void incrementReportEntity(final AtomicLong entity) {
        if (this.traceExecution) {
            entity.incrementAndGet();
        }
    }

    
    private void outputReportIfNeeded() {
        
        if (this.traceExecution) { // fail fast
            
            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) {
                        this.logger.trace(
                                String.format(REPORT_FORMAT,
                                        Integer.valueOf(size()),
                                        Long.valueOf(this.putCount.get()),
                                        Long.valueOf(this.getCount.get()),
                                        Long.valueOf(this.hitCount.get()),
                                        Long.valueOf(this.missCount.get()),
                                        this.name));
                        this.lastExecution = currentTime;
                    }
                }
            }
            
        }
        
    }
    
    




    static final class CacheDataContainer {
        
        private final String name;
        private final boolean sizeLimit;
        private final int maxSize;
        private final boolean traceExecution;
        private final Logger logger;
        
        private final ConcurrentHashMap> 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>(initialCapacity);
            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 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 int put(final K key, final CacheEntry value) {
            if (this.traceExecution) {
                return putWithTracing(key, value); 
            }
            return putWithoutTracing(key, value);
        }

        
        private int putWithoutTracing(final K key, final CacheEntry 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 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 value) {

            final CacheEntry existing = this.container.putIfAbsent(key, value);
            if (existing == null) {
                if (this.sizeLimit) {
                    final Object removedKey = this.fifo[this.fifoPointer]; 
                    if (removedKey != null) {
                        final CacheEntry 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 not updated, not a real benefit in doing it.
            this.container.remove(key);
            return -1;
        }

        
        private synchronized int removeWithTracing(final K key) {
            // FIFO is not updated, not a real benefit in doing it.
            final CacheEntry removed = this.container.remove(key);
            if (removed == null) {
                // When tracing is active, this means nothing was removed
                return -1;
            }
            return this.container.size();
        }


        public void clear() {
            this.container.clear();
        }
        
        
        public int size() {
            return this.container.size();
        }
        
    }




    static final class CacheEntry {

        private final SoftReference 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(cachedValue);
            this.cachedValueAnchor = (!useSoftReferences? cachedValue : null);
            this.creationTimeInMillis = System.currentTimeMillis();

        }

        public  V getValueIfStillValid(final String cacheMapName, 
                final K key, final ICacheEntryValidityChecker 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;
        }

    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy