/**
* 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/>.
*
* @author Gabriel Roldan, Boundless Spatial Inc, Copyright 2015
*/
package org.geowebcache.storage;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.config.BlobStoreConfig;
import org.geowebcache.config.ConfigurationException;
import org.geowebcache.config.FileBlobStoreConfig;
import org.geowebcache.config.XMLConfiguration;
import org.geowebcache.layer.TileLayer;
import org.geowebcache.layer.TileLayerDispatcher;
import org.geowebcache.locks.LockProvider;
import org.geowebcache.storage.blobstore.file.FileBlobStore;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
/**
* A composite {@link BlobStore} that multiplexes tile operations to configured blobstores based on
* {@link BlobStoreConfig#getId() blobstore id} and TileLayers {@link TileLayer#getBlobStoreId()
* BlobStoreId} matches.
* <p>
* Tile operations for {@link TileLayer}s with no configured {@link TileLayer#getBlobStoreId()
* BlobStoreId} (i.e. {@code null}) are redirected to the "default blob store", which is either
* <b>the</b> one configured as the {@link BlobStoreConfig#isDefault() default} one, or a
* {@link FileBlobStore} following the {@link DefaultStorageFinder#getDefaultPath() legacy cache
* directory lookup mechanism}, if no blobstore is set as default.
* <p>
* At construction time, {@link BlobStore} instances will be created for all
* {@link BlobStoreConfig#isEnabled() enabled} configs.
*
* @since 1.8
*/
public class CompositeBlobStore implements BlobStore {
private static Log log = LogFactory.getLog(CompositeBlobStore.class);
public static final String DEFAULT_STORE_DEFAULT_ID = "_DEFAULT_STORE_";
@VisibleForTesting
Map<String, LiveStore> blobStores = new ConcurrentHashMap<>();
private TileLayerDispatcher layers;
private DefaultStorageFinder defaultStorageFinder;
private LockProvider lockProvider;
private final ReadWriteLock configLock = new ReentrantReadWriteLock();
private final BlobStoreListenerList listeners = new BlobStoreListenerList();
@VisibleForTesting
static final class LiveStore {
BlobStoreConfig config;
BlobStore liveInstance;
public LiveStore(BlobStoreConfig config, @Nullable BlobStore store) {
Preconditions.checkArgument(config.isEnabled() == (store != null));
this.config = config;
this.liveInstance = store;
}
}
/**
* Create a composite blob store that multiplexes tile operations to configured blobstores based
* on {@link BlobStoreConfig#getId() blobstore id} and TileLayers
* {@link TileLayer#getBlobStoreId() BlobStoreId} matches.
*
* @param layers used to get the layer's {@link TileLayer#getBlobStoreId() blobstore id}
* @param defaultStorageFinder to resolve the location of the cache directory for the legacy
* blob store when no {@link BlobStoreConfig#isDefault() default blob store} is given
* @param configuration the configuration as read from {@code geowebcache.xml} containing the
* configured {@link XMLConfiguration#getBlobStores() blob stores}
* @throws ConfigurationException if there's a configuration error like a store confing having
* no id, or two store configs having the same id, or more than one store config being
* marked as the default one, or the default store is not
* {@link BlobStoreConfig#isEnabled() enabled}
* @throws StorageException if the live {@code BlobStore} instance can't be
* {@link BlobStoreConfig#createInstance() created} of an enabled
* {@link BlobStoreConfig}
*/
public CompositeBlobStore(TileLayerDispatcher layers,
DefaultStorageFinder defaultStorageFinder, XMLConfiguration configuration)
throws StorageException, ConfigurationException {
this.layers = layers;
this.defaultStorageFinder = defaultStorageFinder;
this.lockProvider = configuration.getLockProvider();
this.blobStores = loadBlobStores(configuration.getBlobStores());
}
@Override
public boolean delete(String layerName) throws StorageException {
return readFunctionUnsafe(()->store(layerName).delete(layerName));
}
@Override
public boolean deleteByGridsetId(String layerName, String gridSetId) throws StorageException {
return readFunctionUnsafe(()->store(layerName).deleteByGridsetId(layerName, gridSetId));
}
@Override
public boolean delete(TileObject obj) throws StorageException {
return readFunctionUnsafe(()->store(obj.getLayerName()).delete(obj));
}
@Override
public boolean delete(TileRange obj) throws StorageException {
return readFunctionUnsafe(()->store(obj.getLayerName()).delete(obj));
}
@Override
public boolean get(TileObject obj) throws StorageException {
return readFunctionUnsafe(()->store(obj.getLayerName()).get(obj));
}
@Override
public void put(TileObject obj) throws StorageException {
readActionUnsafe(()->store(obj.getLayerName()).put(obj));
}
@Deprecated
@Override
public void clear() throws StorageException {
throw new UnsupportedOperationException();
}
@Override
public synchronized void destroy() {
destroy(blobStores);
}
private void destroy(Map<String, LiveStore> blobStores) {
for (LiveStore bs : blobStores.values()) {
try {
if (bs.config.isEnabled()) {
bs.liveInstance.destroy();
}
} catch (Exception e) {
log.error("Error disposing BlobStore " + bs.config.getId(), e);
}
}
blobStores.clear();
}
/**
* Adds the listener to all enabled blob stores
*/
@Override
public void addListener(BlobStoreListener listener) {
readAction(()->{
this.listeners.addListener(listener);// save it for later in case setBlobStores is
// called
for (LiveStore bs : blobStores.values()) {
if (bs.config.isEnabled()) {
bs.liveInstance.addListener(listener);
}
}
});
}
/**
* Removes the listener from all the enabled blob stores
*/
@Override
public boolean removeListener(BlobStoreListener listener) {
return readFunction(()->{
this.listeners.removeListener(listener);
return blobStores.values().stream()
.filter(bs->bs.config.isEnabled())
.map(bs->bs.liveInstance.removeListener(listener))
.collect(Collectors.reducing((x,y)->x||y)) // Don't use anyMatch or findFirst as we don't want it to shortcut
.orElse(false);
});
}
@Override
public boolean rename(String oldLayerName, String newLayerName) throws StorageException {
return readFunctionUnsafe(()->{
for (LiveStore bs : blobStores.values()) {
BlobStoreConfig config = bs.config;
if (config.isEnabled()) {
if (bs.liveInstance.rename(oldLayerName, newLayerName)) {
return true;
}
}
}
return false;
});
}
@Override
public String getLayerMetadata(String layerName, String key) {
return readFunction(()->store(layerName).getLayerMetadata(layerName, key));
}
@Override
public void putLayerMetadata(String layerName, String key, String value) {
readAction(()->{
store(layerName).putLayerMetadata(layerName, key, value);
});
}
@Override
public boolean layerExists(String layerName) {
return readFunction(()->blobStores.values().stream()
.anyMatch(bs->bs.config.isEnabled() && bs.liveInstance.layerExists(layerName)));
}
private BlobStore store(String layerId) throws StorageException {
LiveStore store;
try {
store = forLayer(layerId);
} catch (GeoWebCacheException e) {
throw new StorageException(e.getMessage(), e);
}
if (!store.config.isEnabled()) {
throw new StorageException("Attempted to use a blob store that's disabled: "
+ store.config.getId());
}
return store.liveInstance;
}
/**
* @throws StorageException if the blobstore is not enabled or does not exist
* @throws GeoWebCacheException if the layer is not found
*/
private LiveStore forLayer(String layerName) throws StorageException, GeoWebCacheException {
TileLayer layer;
try {
layer = layers.getTileLayer(layerName);
} catch (GeoWebCacheException e) {
throw e;
}
String storeId = layer.getBlobStoreId();
LiveStore store;
if (null == storeId) {
store = defaultStore();
} else {
store = blobStores.get(storeId);
}
if (store == null) {
throw new StorageException("No BlobStore with id '" + storeId + "' found");
}
return store;
}
private LiveStore defaultStore() throws StorageException {
LiveStore store = blobStores.get(CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID);
if (store == null) {
throw new StorageException("No default BlobStore has been defined");
}
return store;
}
public void setBlobStores(Iterable<? extends BlobStoreConfig> configs) throws StorageException,
ConfigurationException {
configLock.writeLock().lock();
try {
Map<String, LiveStore> newStores = loadBlobStores(configs);
Map<String, LiveStore> oldStores = this.blobStores;
this.blobStores = newStores;
for (LiveStore ls : oldStores.values()) {
if (ls.liveInstance != null) {
ls.liveInstance.destroy();
}
}
} finally {
configLock.writeLock().unlock();
}
}
/**
* Loads the blob stores from the list of configuration objects
*
* @param configs the list of blob store configurations
* @return a mapping of blob store id to {@link LiveStore} containing the configuration itself
* and the live instance if the blob store is enabled
* @throws ConfigurationException if there's a configuration error like a store confing having
* no id, or two store configs having the same id, or more than one store config being
* marked as the default one, or the default store is not
* {@link BlobStoreConfig#isEnabled() enabled}
* @throws StorageException if the live {@code BlobStore} instance can't be
* {@link BlobStoreConfig#createInstance() created} of an enabled
* {@link BlobStoreConfig}
*/
Map<String, LiveStore> loadBlobStores(Iterable<? extends BlobStoreConfig> configs)
throws StorageException, ConfigurationException {
Map<String, LiveStore> stores = new HashMap<>();
BlobStoreConfig defaultStore = null;
try {
for (BlobStoreConfig config : configs) {
final String id = config.getId();
final boolean enabled = config.isEnabled();
if (Strings.isNullOrEmpty(id)) {
throw new ConfigurationException("No id provided for blob store " + config);
}
if (stores.containsKey(id)) {
throw new ConfigurationException("Duplicate blob store id: " + id
+ ". Check your configuration.");
}
if (CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID.equals(id)) {
throw new ConfigurationException(CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID
+ " is a reserved identifier, please don't use it in the configuration");
}
BlobStore store = null;
if (enabled) {
store = config.createInstance(layers, lockProvider);
}
LiveStore liveStore = new LiveStore(config, store);
stores.put(config.getId(), liveStore);
if (config.isDefault()) {
if (defaultStore == null) {
if (!enabled) {
throw new ConfigurationException(
"The default blob store can't be disabled: " + config.getId());
}
defaultStore = config;
stores.put(CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID, liveStore);
} else {
throw new ConfigurationException("Duplicate default blob store: "
+ defaultStore.getId() + " and " + config.getId());
}
}
}
if (!stores.containsKey(CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID)) {
FileBlobStoreConfig config = new FileBlobStoreConfig();
config.setEnabled(true);
config.setDefault(true);
config.setBaseDirectory(defaultStorageFinder.getDefaultPath());
BlobStore store;
store = new FileBlobStore(config.getBaseDirectory());
stores.put(CompositeBlobStore.DEFAULT_STORE_DEFAULT_ID,
new LiveStore(config, store));
}
} catch (ConfigurationException | StorageException e) {
destroy(stores);
throw e;
}
return new ConcurrentHashMap<>(stores);
}
@Override
public boolean deleteByParametersId(String layerName, String parametersId)
throws StorageException {
return readFunctionUnsafe(()->store(layerName).deleteByParametersId(layerName, parametersId));
}
@Override
public Set<Map<String, String>> getParameters(String layerName) {
return readFunction(()-> store(layerName).getParameters(layerName));
}
@Override
public Set<String> getParameterIds(String layerName) {
return readFunction(()->store(layerName).getParameterIds(layerName));
}
@FunctionalInterface
static interface StorageAction {
void run() throws StorageException;
}
@FunctionalInterface
static interface StorageAccessor<T> {
T get() throws StorageException;
}
protected <T> T readFunctionUnsafe(StorageAccessor<T> function) throws StorageException {
configLock.readLock().lock();
try {
return function.get();
} finally {
configLock.readLock().unlock();
}
}
protected <T> T readFunction(StorageAccessor<T> function) {
try {
return readFunctionUnsafe(function);
} catch (StorageException e) {
throw Throwables.propagate(e);
}
}
protected void readActionUnsafe(StorageAction function) throws StorageException {
readFunctionUnsafe((StorageAccessor<Void>)()->{function.run();return null;});
}
protected void readAction(StorageAction function) {
readFunction((StorageAccessor<Void>)()->{function.run();return null;});
}
public Map<String,Optional<Map<String, String>>> getParametersMapping(String layerName) {
return readFunction(()->store(layerName).getParametersMapping(layerName));
}
}