/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.geowebcache.storage.blobstore.memory;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.io.ByteArrayResource;
import org.geowebcache.io.Resource;
import org.geowebcache.storage.BlobStore;
import org.geowebcache.storage.BlobStoreListener;
import org.geowebcache.storage.StorageException;
import org.geowebcache.storage.TileObject;
import org.geowebcache.storage.TileRange;
import org.geowebcache.storage.blobstore.memory.guava.GuavaCacheProvider;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* This class is an implementation of the {@link BlobStore} interface wrapping another {@link BlobStore} implementation and supporting in memory
* caching. Caching is provided by an input {@link CacheProvider} object. It must be pointed out that this Blobstore has an asynchronous relation
* with the underlying wrapped {@link BlobStore}. In fact, each operation on the wrapped {@link BlobStore} is scheduled in a queue and will be done
* by an executor thread. Operations that require a boolean value will have to wait until previous tasks are completed.
*
* @author Nicola Lagomarsini Geosolutions
*/
public class MemoryBlobStore implements BlobStore, ApplicationContextAware {
/** {@link Log} object used for logging exceptions */
private final static Log LOG = LogFactory.getLog(MemoryBlobStore.class);
/** {@link BlobStore} to use when no element is found */
private BlobStore store;
/** {@link CacheProvider} object to use for caching */
private CacheProvider cacheProvider;
/** Executor service used for scheduling cacheProvider store operations like put,delete,... */
private final ExecutorService executorService;
/** Optional name used for searching the bean related to the CacheProvider to set in the ApplicationContext */
private String cacheBeanName;
/** Boolean used for Application Context initialization */
private AtomicBoolean cacheAlreadySet;
/** {@link ReentrantReadWriteLock} used for handling concurrency when accessing the cacheProvider. */
private final ReentrantReadWriteLock lock;
/** {@link WriteLock} used for scheduling the access to the {@link MemoryBlobStore} state */
private final WriteLock blobStoreStateLock;
/**
* {@link ReadLock} used for granting access to operations which does not change the {@link MemoryBlobStore} state, but can change the state of
* its components like {@link CacheProvider} and {@link BlobStore}
*/
private final ReadLock componentsStateLock;
public MemoryBlobStore() {
// Initialization of the various elements
this.executorService = Executors.newFixedThreadPool(1);
lock = new ReentrantReadWriteLock(true);
blobStoreStateLock = lock.writeLock();
componentsStateLock = lock.readLock();
cacheAlreadySet = new AtomicBoolean(false);
// Initialization of the cacheProvider and store. Must be overridden, this uses default and caches in memory
setStore(new NullBlobStore());
GuavaCacheProvider startingCache = new GuavaCacheProvider(new CacheConfiguration());
this.cacheProvider = startingCache;
}
@Override
public boolean layerExists(String layerName) {
componentsStateLock.lock();
try {
return store.layerExists(layerName);
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean delete(String layerName) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing layer: " + layerName + " from cache provider");
}
// Remove from cacheProvider
cacheProvider.removeLayer(layerName);
// Remove the layer. Wait other scheduled tasks
boolean executed = executeBlobStoreTask(BlobStoreAction.DELETE_LAYER, store, layerName);
if (LOG.isDebugEnabled()) {
if (executed) {
LOG.debug("Delete Layer Task executed");
} else {
LOG.debug("Delete LayerTask failed");
}
}
// Returns the result
return executed;
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean deleteByGridsetId(String layerName, String gridSetId) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing Layer: " + layerName);
}
// Remove the layer from the cacheProvider
cacheProvider.removeLayer(layerName);
if (LOG.isDebugEnabled()) {
LOG.debug("Scheduling GridSet: " + gridSetId + " removal for Layer: " + layerName);
}
// Remove selected gridsets
executorService.submit(new BlobStoreTask(store, BlobStoreAction.DELETE_GRIDSET,
layerName, gridSetId));
return true;
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean delete(TileObject obj) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing TileObject: " + obj);
}
// Remove from cacheProvider
cacheProvider.removeTileObj(obj);
// Remove selected TileObject
if (LOG.isDebugEnabled()) {
LOG.debug("Scheduling removal of TileObject: " + obj);
}
executorService.submit(new BlobStoreTask(store, BlobStoreAction.DELETE_SINGLE, obj));
return true;
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean delete(TileRange obj) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing TileObjects for Layer: " + obj.getLayerName()
+ ", min/max levels: " + "[" + obj.getZoomStart() + ", "
+ obj.getZoomStop() + "], Gridset: " + obj.getGridSetId());
}
// Remove layer for the cacheProvider
cacheProvider.removeLayer(obj.getLayerName());
// Remove selected TileObject
if (LOG.isDebugEnabled()) {
LOG.debug("Scheduling removal of TileObjects for Layer: " + obj.getLayerName()
+ ", min/max levels: " + "[" + obj.getZoomStart() + ", "
+ obj.getZoomStop() + "], Gridset: " + obj.getGridSetId());
}
// Remove selected TileRange
executorService.submit(new BlobStoreTask(store, BlobStoreAction.DELETE_RANGE, obj));
return true;
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean get(TileObject obj) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Checking if TileObject:" + obj + " is present");
}
TileObject cached = cacheProvider.getTileObj(obj);
boolean found = false;
if (cached == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("TileObject:" + obj
+ " not found. Try to get it from the wrapped blobstore");
}
// Try if it can be found in the system. Wait other scheduled tasks
found = executeBlobStoreTask(BlobStoreAction.GET, store, obj);
// If the file has been found, it is inserted in cacheProvider
if (found) {
if (LOG.isDebugEnabled()) {
LOG.debug("TileObject:" + obj + " found. Put it in cache");
}
// Get the Cached TileObject
cached = getByteResourceTile(obj);
// Put the file in Cache
cacheProvider.putTileObj(cached);
}
} else {
// Found in cacheProvider
found = true;
}
// If found add its resource to the input TileObject
if (found) {
if (LOG.isDebugEnabled()) {
LOG.debug("TileObject:" + obj + " found, update the input TileObject");
}
Resource resource = cached.getBlob();
obj.setBlob(resource);
obj.setCreated(resource.getLastModified());
obj.setBlobSize((int) resource.getSize());
}
return found;
} finally {
componentsStateLock.unlock();
}
}
@Override
public void put(TileObject obj) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Convert Input resource into a Byte Array");
}
TileObject cached = getByteResourceTile(obj);
if (LOG.isDebugEnabled()) {
LOG.debug("Adding TileObject: " + obj + " to cache");
}
cacheProvider.putTileObj(cached);
// Add selected TileObject. Wait other scheduled tasks
if (LOG.isDebugEnabled()) {
LOG.debug("Adding TileObject: " + obj + " to the wrapped blobstore");
}
// Variable containing the execution result
executeBlobStoreTask(BlobStoreAction.PUT, store, obj);
} finally {
componentsStateLock.unlock();
}
}
@Override
public void clear() throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Flushing cache");
}
// flush the cacheProvider
cacheProvider.clear();
// Remove all the files
executorService.submit(new BlobStoreTask(store, BlobStoreAction.CLEAR, ""));
} finally {
componentsStateLock.unlock();
}
}
@Override
public void destroy() {
blobStoreStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Reset cache");
}
// flush the cacheProvider
cacheProvider.reset();
// Remove all the files
if (LOG.isDebugEnabled()) {
LOG.debug("Destroy wrapped store");
}
executeBlobStoreTask(BlobStoreAction.DESTROY, store, "");
// Stop the pending tasks
executorService.shutdownNow();
} finally {
blobStoreStateLock.unlock();
}
}
@Override
public void addListener(BlobStoreListener listener) {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding a new Listener");
}
// Add a new Listener
store.addListener(listener);
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean removeListener(BlobStoreListener listener) {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing listener");
}
// Remove a listener
return store.removeListener(listener);
} finally {
componentsStateLock.unlock();
}
}
@Override
public boolean rename(String oldLayerName, String newLayerName) throws StorageException {
componentsStateLock.lock();
try {
// flush the cacheProvider
if (LOG.isDebugEnabled()) {
LOG.debug("Flushing cache");
}
cacheProvider.clear();
// Rename the layer. Wait other scheduled tasks
if (LOG.isDebugEnabled()) {
LOG.debug("Executing Layer rename task");
}
// Variable containing the execution result
boolean executed = executeBlobStoreTask(BlobStoreAction.RENAME, store, oldLayerName,
newLayerName);
return executed;
} finally {
componentsStateLock.unlock();
}
}
@Override
public String getLayerMetadata(String layerName, String key) {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Getting metadata for Layer: " + layerName);
}
// Get the Layer metadata
return store.getLayerMetadata(layerName, key);
} finally {
componentsStateLock.unlock();
}
}
@Override
public void putLayerMetadata(String layerName, String key, String value) {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding metadata for Layer: " + layerName);
}
// Add a new Layer Metadata
store.putLayerMetadata(layerName, key, value);
} finally {
componentsStateLock.unlock();
}
}
/**
* @return a {@link CacheStatistics} object containing the {@link CacheProvider} statistics
*/
public CacheStatistics getCacheStatistics() {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Getting Cache Statistics");
}
return cacheProvider.getStatistics();
} finally {
componentsStateLock.unlock();
}
}
/**
* Setter for the store to wrap
*
* @param store
*/
public void setStore(BlobStore store) {
blobStoreStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting the wrapped store");
}
if (store == null) {
throw new NullPointerException("Input BlobStore cannot be null");
}
this.store = store;
} finally {
blobStoreStateLock.unlock();
}
}
/**
* @return The wrapped {@link BlobStore} implementation
*/
public BlobStore getStore() {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Returning the wrapped store");
}
return store;
} finally {
componentsStateLock.unlock();
}
}
/**
* Setter for the cacheProvider to use
*
* @param cacheProvider
*/
public void setCacheProvider(CacheProvider cache) {
blobStoreStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting cache provided");
}
if (cache == null) {
throw new IllegalArgumentException("Input BlobStore cannot be null");
}
this.cacheProvider = cache;
cacheAlreadySet.getAndSet(true);
} finally {
blobStoreStateLock.unlock();
}
}
/***
* This method is used for converting a {@link TileObject} {@link Resource} into a {@link ByteArrayResource}.
*
* @param obj
* @return a TileObject with resource stored in a Byte Array
* @throws StorageException
*/
private TileObject getByteResourceTile(TileObject obj) throws StorageException {
// Get TileObject resource
Resource blob = obj.getBlob();
final Resource finalBlob;
// If it is a ByteArrayResource, the result is simply copied
if (obj.getBlob() instanceof ByteArrayResource) {
if (LOG.isDebugEnabled()) {
LOG.debug("Resource is already a Byte Array, only a copy is needed");
}
ByteArrayResource byteArrayResource = (ByteArrayResource) obj.getBlob();
byte[] contents = byteArrayResource.getContents();
finalBlob = new ByteArrayResource(contents);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Resource is not a Byte Array, data must be transferred");
}
// Else the result is written to a new WritableByteChannel
final ByteArrayOutputStream bOut = new ByteArrayOutputStream();
WritableByteChannel wChannel = Channels.newChannel(bOut);
try {
blob.transferTo(wChannel);
} catch (IOException e) {
throw new StorageException(e.getLocalizedMessage(), e);
}
finalBlob = new ByteArrayResource(bOut.toByteArray());
}
// Creation of a new Resource
TileObject cached = TileObject.createCompleteTileObject(obj.getLayerName(), obj.getXYZ(),
obj.getGridSetId(), obj.getBlobFormat(), obj.getParameters(), finalBlob);
return cached;
}
/**
* Setter for the Cache Provider name, note that this cannot be used in combination with the setCacheProvider method in the application Context
* initialization
*
* @param cacheBeanName
*/
public void setCacheBeanName(String cacheBeanName) {
blobStoreStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting cache providee name");
}
this.cacheBeanName = cacheBeanName;
} finally {
blobStoreStateLock.unlock();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (!cacheAlreadySet.get()) {
// Get all the CacheProvider beans
String[] beans = applicationContext.getBeanNamesForType(CacheProvider.class);
int beanSize = beans.length;
boolean configured = false;
// If at least one bean is present, use it
if (beanSize > 0) {
// If a bean name is defined, get the related bean
if (cacheBeanName != null && !cacheBeanName.isEmpty()) {
for (String beanDef : beans) {
if (cacheBeanName.equalsIgnoreCase(beanDef)) {
CacheProvider bean = applicationContext.getBean(beanDef,
CacheProvider.class);
if (bean.isAvailable()) {
setCacheProvider(bean);
configured = true;
break;
}
}
}
}
// If only one is present it is used
if (!configured && beanSize == 1) {
CacheProvider bean = applicationContext.getBean(beans[0], CacheProvider.class);
if (bean.isAvailable()) {
setCacheProvider(bean);
configured = true;
}
}
// If two are present and at least one of them is not guava, then it is used
if (!configured && beanSize == 2) {
for (String beanDef : beans) {
CacheProvider bean = applicationContext.getBean(beanDef,
CacheProvider.class);
if (!(bean instanceof GuavaCacheProvider) && bean.isAvailable()) {
setCacheProvider(bean);
configured = true;
break;
}
}
// Try again and search if at least a GuavaCacheProvider is present
if (!configured) {
for (String beanDef : beans) {
CacheProvider bean = applicationContext.getBean(beanDef,
CacheProvider.class);
if (bean.isAvailable()) {
setCacheProvider(bean);
configured = true;
break;
}
}
}
}
if (!configured) {
if (LOG.isDebugEnabled()) {
LOG.debug("CacheProvider not configured, use default configuration");
}
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("CacheProvider already configured");
}
}
}
private boolean executeBlobStoreTask(BlobStoreAction action, BlobStore store, Object... objs) {
Future<Boolean> future = executorService.submit(new BlobStoreTask(store, action, objs));
// Variable containing the execution result
boolean executed = false;
if (LOG.isDebugEnabled()) {
LOG.debug("Waiting scheduled Tasks");
}
try {
// Waiting tasks
executed = future.get();
} catch (InterruptedException | ExecutionException e) {
if (LOG.isErrorEnabled()) {
LOG.error(e.getMessage(), e);
}
}
return executed;
}
/**
* {@link Callable} implementation used for creating various tasks to submit to the {@link MemoryBlobStore} executor service.
*
* @author Nicola Lagomarsini GeoSolutions
*/
static class BlobStoreTask implements Callable<Boolean> {
/** Store on which tasks must be executed */
private BlobStore store;
/** Array of objects that must be used for the selected operation */
private Object[] objs;
/** Enum containing the kind of action to execute */
private BlobStoreAction action;
public BlobStoreTask(BlobStore store, BlobStoreAction action, Object... objs) {
this.objs = objs;
this.store = store;
this.action = action;
}
@Override
public Boolean call() throws Exception {
boolean result = false;
try {
// Execution of the requested operation
result = action.executeOperation(store, objs);
} catch (StorageException s) {
if (LOG.isErrorEnabled()) {
LOG.error(s.getMessage(), s);
}
}
return result;
}
}
/**
* Enum containing all the possible operations that can be executed by a {@link BlobStoreTask}. Each operation must implement the
* "executeOperation" method.
*
* @author Nicola Lagomarsini GeoSolutions
*/
public enum BlobStoreAction {
PUT {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 1 || !(objs[0] instanceof TileObject)) {
return false;
}
store.put((TileObject) objs[0]);
return true;
}
},
GET {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 1 || !(objs[0] instanceof TileObject)) {
return false;
}
return store.get((TileObject) objs[0]);
}
},
DELETE_SINGLE {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 1 || !(objs[0] instanceof TileObject)) {
return false;
}
return store.delete((TileObject) objs[0]);
}
},
DELETE_RANGE {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 1 || !(objs[0] instanceof TileRange)) {
return false;
}
return store.delete((TileRange) objs[0]);
}
},
DELETE_GRIDSET {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 2 || !(objs[0] instanceof String)
|| !(objs[1] instanceof String)) {
return false;
}
return store.deleteByGridsetId((String) objs[0], (String) objs[1]);
}
},
DELETE_PARAMS_ID {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 2 || !(objs[0] instanceof String)
|| !(objs[1] instanceof String)) {
return false;
}
return store.deleteByParametersId((String) objs[0], (String) objs[1]);
}
},
DELETE_LAYER {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 2 || !(objs[0] instanceof String)) {
return false;
}
return store.delete((String) objs[0]);
}
},
CLEAR {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
store.clear();
return true;
}
},
DESTROY {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
store.destroy();
return true;
}
},
RENAME {
@Override
public boolean executeOperation(BlobStore store, Object... objs)
throws StorageException {
if (objs == null || objs.length < 2 || !(objs[0] instanceof String)
|| !(objs[1] instanceof String)) {
return false;
}
return store.rename((String) objs[0], (String) objs[1]);
}
};
/**
* Executes an operation defined by the Enum.
*
* @param store
* @param objs
* @return operation result
* @throws StorageException
*/
public abstract boolean executeOperation(BlobStore store, Object... objs)
throws StorageException;
}
@Override
public boolean deleteByParametersId(String layerName, String parametersId)
throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Removing Layer: " + layerName);
}
// Remove the layer from the cacheProvider
cacheProvider.removeLayer(layerName);
if (LOG.isDebugEnabled()) {
LOG.debug("Scheduling Parameters: " + parametersId + " removal for Layer: " + layerName);
}
// Remove selected parameters
executorService.submit(new BlobStoreTask(store, BlobStoreAction.DELETE_PARAMS_ID,
layerName, parametersId));
return true;
} finally {
componentsStateLock.unlock();
}
}
@Override
public Set<Map<String, String>> getParameters(String layerName) throws StorageException {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Getting parameters for Layer: " + layerName);
}
return store.getParameters(layerName);
} finally {
componentsStateLock.unlock();
}
}
public Map<String,Optional<Map<String, String>>> getParametersMapping(String layerName) {
componentsStateLock.lock();
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Getting parameters for Layer: " + layerName);
}
return store.getParametersMapping(layerName);
} finally {
componentsStateLock.unlock();
}
}
}