it.geosolutions.concurrent.ConcurrentTileCacheMultiMap.java Source code

Java tutorial

Introduction

Here is the source code for it.geosolutions.concurrent.ConcurrentTileCacheMultiMap.java

Source

/* JAI-Ext - OpenSource Java Advanced Image Extensions Library
*    http://www.geo-solutions.it/
*    Copyright 2014 GeoSolutions
    
    
* 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 it.geosolutions.concurrent;

import it.geosolutions.concurrent.ConcurrentTileCache.Actions;

import java.awt.Point;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.jai.TileCache;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.cache.Weigher;
import com.sun.media.jai.util.CacheDiagnostics;

/**
 * This implementation of the TileCache class uses a Guava Cache and a multimap in order to provide a better concurrency handling. The first object
 * contains all the cached tiles while the second one contains the mapping of the tile keys for each image. This class implements
 * {@link CacheDiagnostics} in order to get the statistics associated to the {@link TileCache}. The user can define the cache memory capacity, the
 * concurrency level (which indicates in how many segments the cache must be divided), the threshold of the total memory to use and a boolean
 * indicating if the diagnostic must be enabled.
 * 
 * @author Nicola Lagomarsini GeoSolutions S.A.S.
 * 
 */
public class ConcurrentTileCacheMultiMap extends Observable implements TileCache, CacheDiagnostics {

    /** The default memory threshold of the cache. */
    public static final float DEFAULT_MEMORY_THRESHOLD = 0.75F;

    /** The default memory capacity of the cache (16 MB). */
    public static final long DEFAULT_MEMORY_CACHE = 16L * 1024L * 1024L;

    /** The default diagnostic settings */
    public static final boolean DEFAULT_DIAGNOSTIC = false;

    /** The default concurrency settings */
    public static final int DEFAULT_CONCURRENCY_LEVEL = 4;

    /**
     * The tile cache. A Guava Cache is used to cache the tiles. The "key" is a <code>Object</code>. The "value" is a CachedTileImpl.
     */
    private Cache<Object, CachedTileImpl> cacheObject;

    /**
     * A concurrent multimap used for mapping the tile keys for each image
     */
    private Map<Object, Set<Object>> multimap;

    /** The memory capacity of the cache. */
    private long memoryCacheCapacity;

    /** The current memory capacity of the cache. */
    private AtomicLong currentCacheCapacity;

    /** The concurrency level of the cache. */
    private int concurrencyLevel;

    /** The amount of memory to keep after memory control */
    private float memoryCacheThreshold = DEFAULT_MEMORY_THRESHOLD;

    /** diagnosticEnabled enable/disable */
    private volatile boolean diagnosticEnabled = DEFAULT_DIAGNOSTIC;

    /**
     * Logger to use for reporting the informations about the TileCache operations.
     */
    private final static Logger LOGGER = Logger.getLogger(ConcurrentTileCacheMultiMap.class.toString());

    public ConcurrentTileCacheMultiMap() {
        this(DEFAULT_MEMORY_CACHE, DEFAULT_DIAGNOSTIC, DEFAULT_MEMORY_THRESHOLD, DEFAULT_CONCURRENCY_LEVEL);
    }

    public ConcurrentTileCacheMultiMap(long memoryCacheCapacity, boolean diagnostic, float mem_threshold,
            int concurrencyLevel) {
        if (memoryCacheCapacity < 0) {
            throw new IllegalArgumentException("Memory capacity too small");
        }
        this.memoryCacheThreshold = mem_threshold;
        this.diagnosticEnabled = diagnostic;
        this.memoryCacheCapacity = memoryCacheCapacity;
        this.concurrencyLevel = concurrencyLevel;

        // cache creation
        cacheObject = buildCache();

        // multimap creation
        multimap = new ConcurrentHashMap<Object, Set<Object>>();
    }

    /** Add a new tile to the cache */
    public void add(RenderedImage owner, int tileX, int tileY, Raster data) {
        add(owner, tileX, tileY, data, null);
    }

    /** Add a new tile to the cache */
    public void add(RenderedImage owner, int tileX, int tileY, Raster data, Object tileCacheMetric) {
        // This tile is not in the cache; create a new CachedTileImpl.
        // else just update.

        // Key associated to the image
        Object imageKey = CachedTileImpl.hashKey(owner);

        // old tile
        CachedTileImpl cti;
        // create a new tile
        CachedTileImpl cti_new = new CachedTileImpl(owner, tileX, tileY, data, tileCacheMetric);

        if (diagnosticEnabled) {
            // if the tile is already cached
            cti = (CachedTileImpl) cacheObject.asMap().putIfAbsent(cti_new.key, cti_new);
            synchronized (cacheObject) {
                if (cti != null) {
                    cti.updateTileTimeStamp();
                    cti.setAction(Actions.SUBSTITUTION_FROM_ADD);
                    setChanged();
                    notifyObservers(cti);
                }
                // Update Cache Memory Size
                currentCacheCapacity.addAndGet(cti_new.getTileSize());

                // Update the tile action in order to notify it to the observers
                cti_new.setAction(Actions.ADDITION);
                setChanged();
                notifyObservers(cti_new);
                updateMultiMap(cti_new.key, imageKey);
            }
        } else {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Added new Tile Image key " + imageKey);
            }
            // new tile insertion
            cacheObject.asMap().putIfAbsent(cti_new.key, cti_new);
            // Atomically adds a new Map if needed and then adds a new tile inside the MultiMap.
            updateMultiMap(cti_new.key, imageKey);
        }
    }

    /** Removes the selected tile from the cache */
    public void remove(RenderedImage owner, int tileX, int tileY) {
        // Calculation of the tile key
        Object key = CachedTileImpl.hashKey(owner, tileX, tileY);
        // remove operation
        removeTileByKey(key);
    }

    /** Retrieves the selected tile from the cache */
    public Raster getTile(RenderedImage owner, int tileX, int tileY) {
        // Calculation of the tile key
        Object key = CachedTileImpl.hashKey(owner, tileX, tileY);
        // Get operation
        return getTileFromKey(key);
    }

    /**
     * Retrieves an array of all tiles in the cache which are owned by the image. May be <code>null</code> if there were no tiles in the cache. The
     * array contains no null entries.
     */
    public Raster[] getTiles(RenderedImage owner) {
        // instantiation of the result array
        Raster[] tilesData = null;

        // Calculation of the key associated to the image
        Object imageKey = CachedTileImpl.hashKey(owner);

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Getting image Tiles Image key " + imageKey);
        }
        // Selection of the tile keys for the image
        Set<Object> keys = multimap.get(imageKey);

        // If no key is found then a null object is returned
        if (keys == null || keys.isEmpty()) {
            return tilesData;
        }

        // Else it is created an iterator on the tile keys
        Iterator<Object> it = keys.iterator();
        // Another check on the iterator
        if (it.hasNext()) {
            // arbitrarily set a temporary vector size
            Vector<Raster> tempData = new Vector<Raster>(10, 20);
            // cycle through all the tile keys present in the multimap and check if they are in the
            // cache...
            while (it.hasNext()) {
                Object key = it.next();
                // get the tile from the key
                Raster rasterTile = getTileFromKey(key);

                // ...then add to the vector if present
                if (rasterTile != null) {
                    tempData.add(rasterTile);
                }
            }
            // Vector size
            int tmpsize = tempData.size();
            if (tmpsize > 0) {
                tilesData = (Raster[]) tempData.toArray(new Raster[tmpsize]);
            }
        }
        return tilesData;
    }

    /**
     * Removes all tiles in the cache which are owned by the image.
     */
    public void removeTiles(RenderedImage owner) {

        // Calculation of the key associated to the image
        Object imageKey = CachedTileImpl.hashKey(owner);

        if (diagnosticEnabled) {
            synchronized (cacheObject) {
                // Selection of the keys associated to the image and removal of each of them
                Set<Object> keys = multimap.get(imageKey);
                if (keys != null) {
                    Iterator<Object> it = keys.iterator();
                    while (it.hasNext()) {
                        Object key = it.next();
                        removeTileByKey(key);
                    }
                }
            }
        } else {
            // Get the keys associated to the image and remove them
            Set<Object> keys = multimap.get(imageKey);
            if (keys != null) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Removing image Tiles Image key " + imageKey);
                }
                cacheObject.invalidateAll(keys);
            }
        }
    }

    /**
     * Adds all tiles in the Point array which are owned by the image.
     */
    public void addTiles(RenderedImage owner, Point[] tileIndices, Raster[] tiles, Object tileCacheMetric) {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Addeding Tiles");
        }
        // cycle through the array for adding tiles
        for (int i = 0; i < tileIndices.length; i++) {
            int tileX = tileIndices[i].x;
            int tileY = tileIndices[i].y;
            Raster tile = tiles[i];
            add(owner, tileX, tileY, tile, tileCacheMetric);
        }
    }

    /**
     * Retrieves an array of tiles in the cache which are specified by the Point array and owned by the image. May be <code>null</code> if there were
     * not in the cache. The array contains null entries.
     */
    public Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
        // instantiation of the array
        Raster[] tilesData = new Raster[tileIndices.length];

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Getting Tiles at the selected positions");
        }
        // cycle through the array for getting tiles
        for (int i = 0; i < tilesData.length; i++) {
            int tileX = tileIndices[i].x;
            int tileY = tileIndices[i].y;

            Raster rasterData = getTile(owner, tileX, tileY);
            // even if the tile is not present it is inserted in the array
            if (rasterData == null) {
                tilesData[i] = null;

            } else {
                // found tile in cache
                tilesData[i] = rasterData;
            }
        }

        return tilesData;
    }

    /** Removes all tiles present in the cache without checking for the image owner */
    public void flush() {
        synchronized (cacheObject) {
            // It is necessary to clear all the elements
            // from the old cache.
            if (diagnosticEnabled) {
                // Creation of an iterator for accessing to every tile in the cache
                Iterator<Object> keys = cacheObject.asMap().keySet().iterator();
                // cycle across the cache for removing and updating every tile
                while (keys.hasNext()) {
                    Object key = keys.next();
                    CachedTileImpl cti = (CachedTileImpl) cacheObject.asMap().remove(key);

                    // diagnosticEnabled

                    cti.setAction(Actions.REMOVAL_FROM_FLUSH);
                    setChanged();
                    notifyObservers(cti);
                }
            } else {
                // Invalidation of all the keys of the cache
                cacheObject.invalidateAll();
            }

            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Flushing cache");
            }

            // Cache creation
            cacheObject = buildCache();
            // multimap creation
            multimap = new ConcurrentHashMap<Object, Set<Object>>();
        }
    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public void memoryControl() {
        throw new UnsupportedOperationException("Memory Control not supported");
    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public void setTileCapacity(int tileCapacity) {
        throw new UnsupportedOperationException("Deprecated Operation");
    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public int getTileCapacity() {
        throw new UnsupportedOperationException("Deprecated Operation");
    }

    /** Sets the cache memory capacity and then flush and rebuild the cache */
    public void setMemoryCapacity(long memoryCacheCapacity) {
        synchronized (cacheObject) {
            if (memoryCacheCapacity < 0) {
                throw new IllegalArgumentException("Memory capacity too small");
            } else {
                this.memoryCacheCapacity = memoryCacheCapacity;
                // The flush is done in order to rebuild the cache with the new settings
                flush();
            }
        }
    }

    /** Retrieve the cache memory capacity */
    public long getMemoryCapacity() {
        return memoryCacheCapacity;
    }

    /** Sets the cache memory threshold and then flush and rebuild the cache */
    public void setMemoryThreshold(float mt) {
        synchronized (cacheObject) {
            if (mt < 0.0F || mt > 1.0F) {
                throw new IllegalArgumentException("Memory threshold should be between 0 and 1");
            } else {
                memoryCacheThreshold = mt;
                // The flush is done in order to rebuild the cache with the new settings
                flush();

            }
        }
    }

    /** Retrieve the cache memory threshold */
    public float getMemoryThreshold() {
        return memoryCacheThreshold;
    }

    /** Sets the cache ConcurrencyLevel and then flush and rebuild the cache */
    public void setConcurrencyLevel(int concurrency) {
        synchronized (cacheObject) {
            if (concurrency < 1) {
                throw new IllegalArgumentException("ConcurrencyLevel must be at least 1");
            } else {
                concurrencyLevel = concurrency;
                // The flush is done in order to rebuild the cache with the new settings
                flush();

            }
        }
    }

    /** Retrieve the cache concurrency level */
    public int getConcurrencyLevel() {
        return concurrencyLevel;
    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public void setTileComparator(Comparator comparator) {
        throw new UnsupportedOperationException("Comparator not supported");

    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public Comparator getTileComparator() {
        throw new UnsupportedOperationException("Comparator not supported");
    }

    /** Disables diagnosticEnabled for the observers */
    public void disableDiagnostics() {
        synchronized (cacheObject) {
            diagnosticEnabled = false;
            // The flush is done in order to rebuild the cache with the new settings
            flush();
        }
    }

    /** Enables diagnosticEnabled for the observers */
    public void enableDiagnostics() {
        synchronized (cacheObject) {
            diagnosticEnabled = true;
            // The flush is done in order to rebuild the cache with the new settings
            flush();
        }
    }

    /** Retrieves the hit count from the cache statistics */
    public long getCacheHitCount() {
        if (diagnosticEnabled) {
            return cacheObject.stats().hitCount();
        }
        return 0;
    }

    /** Retrieves the current memory size of the cache */
    public long getCacheMemoryUsed() {
        return currentCacheCapacity.get();
    }

    /** Retrieves the miss count from the cache statistics */
    public long getCacheMissCount() {
        if (diagnosticEnabled) {
            return cacheObject.stats().missCount();
        }
        return 0;
    }

    /** Retrieves the number of tiles in the cache */
    public long getCacheTileCount() {
        return cacheObject.size();
    }

    /**
     * Not Supported
     * 
     * @throws UnsupportedOperationException
     */
    public void resetCounts() {
        throw new UnsupportedOperationException("Operation not supported");
    }

    /**
     * Creation of a listener to use for handling the removed tiles
     * 
     * @param diagnostic
     * @return
     */
    private RemovalListener<Object, CachedTileImpl> createListener(final boolean diagnostic) {
        return new RemovalListener<Object, CachedTileImpl>() {
            public void onRemoval(RemovalNotification<Object, CachedTileImpl> n) {
                // if a tile is manually removed, the diagnosticEnabled already consider
                // it in
                // the remove() method

                if (diagnostic) {
                    synchronized (cacheObject) {
                        CachedTileImpl cti = n.getValue();
                        // Update of the tile action
                        if (n.wasEvicted()) {
                            cti.setAction(Actions.REMOVAL_FROM_EVICTION);
                        } else {
                            cti.setAction(Actions.MANUAL_REMOVAL);
                        }
                        // Update Cache Memory Size
                        currentCacheCapacity.addAndGet(-cti.getTileSize());
                        // Removal from the multimap
                        removeTileFromMultiMap(cti);
                        setChanged();
                        notifyObservers(cti);
                    }
                } else {
                    CachedTileImpl cti = n.getValue();
                    if (n.getCause() == RemovalCause.SIZE) {
                        // Logging if the tile is removed because the size is exceeded
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.fine("Removing from MultiMap for size");
                        }
                    }
                    removeTileFromMultiMap(cti);
                }
            }
        };
    }

    /**
     * Method for removing the tile keys from the multimap. If the KeySet associated to the image is empty, it is removed from the multimap.
     * 
     * @param cti
     */
    private void removeTileFromMultiMap(CachedTileImpl cti) {
        // Tile key
        Object key = cti.getKey();
        // Image key
        Object imageKey = cti.getImageKey();
        // KeySet associated to the image
        Set<Object> tileKeys = multimap.get(imageKey);

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Removing tile from MultiMap Image key " + imageKey);
        }

        if (tileKeys != null) {
            // Removal of the keys
            tileKeys.remove(key);
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Removed Tile Image key " + imageKey);
            }
            // If the KeySet is empty then it is removed from the multimap
            if (tileKeys.isEmpty()) {
                multimap.remove(imageKey);
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Removed image SET Image key " + imageKey);
                }
            }
        }
    }

    /** Private cache creation method */
    private Cache<Object, CachedTileImpl> buildCache() {
        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
        builder.maximumWeight((long) (memoryCacheCapacity * memoryCacheThreshold))
                .concurrencyLevel(concurrencyLevel).weigher(new Weigher<Object, CachedTileImpl>() {
                    public int weigh(Object o, CachedTileImpl cti) {
                        return (int) cti.getTileSize();
                    }
                });
        // Setting of the listener
        builder.removalListener(createListener(diagnosticEnabled));
        // Enable statistics only when the diagnostic flag is set to true;
        if (diagnosticEnabled) {
            builder.recordStats();
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Building Cache");
        }
        // Update of the Memory cache size
        currentCacheCapacity = new AtomicLong(0);

        return builder.build();
    }

    /**
     * Update of the multimap when a tile is added.
     * 
     * @param key
     * @param imageKey
     */
    private void updateMultiMap(Object key, Object imageKey) {
        Set<Object> tileKeys = null;
        synchronized (cacheObject) {
            // Check if the multimap contains the keys for the image
            tileKeys = multimap.get(imageKey);
            if (tileKeys == null) {
                // If no key is present then a new KeySet is created and then added to the multimap
                tileKeys = new ConcurrentSkipListSet<Object>();
                multimap.put(imageKey, tileKeys);
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Created new Set for the image Image key " + imageKey);
                }
            }
        }
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Added Tile to the set Image key " + imageKey);
        }
        // Finally the tile key is added.
        tileKeys.add(key);
    }

    /**
     * Removes the tile associated to the key.
     * 
     * @param key
     */
    private void removeTileByKey(Object key) {
        // check if the tile is still in cache
        CachedTileImpl cti = (CachedTileImpl) cacheObject.getIfPresent(key);
        // if so the tile is deleted (even if another thread write on it)
        if (cti != null) {
            if (diagnosticEnabled) {
                synchronized (cacheObject) {
                    // Upgrade the tile action
                    cti.setAction(Actions.ABOUT_TO_REMOVAL);
                    setChanged();
                    notifyObservers(cti);
                    // Removal of the tile
                    cti = (CachedTileImpl) cacheObject.asMap().remove(key);
                    if (cti != null) {
                        // Upgrade the tile action
                        cti.setAction(Actions.MANUAL_REMOVAL);
                        setChanged();
                        notifyObservers(cti);
                    }
                }
            } else {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Removed Tile Image key " + cti.getImageKey());
                }
                // Discard the tile from the cache
                cacheObject.invalidate(key);
            }
        }
    }

    /**
     * Gets the tile associated to the key.
     * 
     * @param key
     * @return
     */
    private Raster getTileFromKey(Object key) {
        Raster tileData = null;
        // check if the tile is present
        CachedTileImpl cti = (CachedTileImpl) cacheObject.asMap().get(key);
        // If not tile is found, null is returned
        if (cti == null) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Null Tile returned");
            }
            return null;
        }
        if (diagnosticEnabled) {
            synchronized (cacheObject) {

                // Update last-access time for diagnosticEnabled
                cti.updateTileTimeStamp();
                cti.setAction(Actions.UPDATING_TILE_FROM_GETTILE);
                setChanged();
                notifyObservers(cti);
            }
        }
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Get the selected tile Image key " + cti.getImageKey());
        }
        // return the selected tile
        tileData = cti.getTile();
        return tileData;
    }
}