/**
* Copyright (c) Codice Foundation
* <p/>
* This 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 any later version.
* <p/>
* 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
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.catalog.cache.impl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MapStoreConfig;
import com.hazelcast.config.XmlConfigBuilder;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import ddf.catalog.cache.ResourceCacheInterface;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.impl.MetacardImpl;
import ddf.catalog.resource.Resource;
import ddf.catalog.resource.data.ReliableResource;
public class ResourceCache implements ResourceCacheInterface {
private static final String KARAF_HOME = "karaf.home";
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceCache.class);
private static final String PRODUCT_CACHE_NAME = "Product_Cache";
/**
* Default location for product-cache directory, <INSTALL_DIR>/data/product-cache
*/
public static final String DEFAULT_PRODUCT_CACHE_DIRECTORY =
"data" + File.separator + PRODUCT_CACHE_NAME;
private static final long BYTES_IN_MEGABYTES = FileUtils.ONE_MB;
private static final long DEFAULT_MAX_CACHE_DIR_SIZE_BYTES = 10737418240L; //10 GB
private List<String> pendingCache = new ArrayList<String>();
/**
* Directory for products cached to file system
*/
private String productCacheDirectory;
private HazelcastInstance instance;
private IMap<Object, Object> cache;
private ProductCacheDirListener<Object, Object> cacheListener = new ProductCacheDirListener<Object, Object>(
DEFAULT_MAX_CACHE_DIR_SIZE_BYTES);
private BundleContext context;
private String xmlConfigFilename;
//called after all parameters are set
public void setCache(HazelcastInstance instance) {
LOGGER.debug("In setCache");
this.instance = instance;
if (this.instance == null) {
Config cfg = getHazelcastConfig(context, xmlConfigFilename);
cfg.setClassLoader(getClass().getClassLoader());
this.instance = Hazelcast.newHazelcastInstance(cfg);
}
cache = this.instance.getMap(PRODUCT_CACHE_NAME);
cacheListener.setHazelcastInstance(this.instance);
cache.addEntryListener(cacheListener, true);
}
public void setupCache() {
setCache(null);
}
private Config getHazelcastConfig(BundleContext context, String xmlConfigFilename) {
Config cfg = null;
Bundle bundle = context.getBundle();
URL xmlConfigFileUrl = null;
if (StringUtils.isNotBlank(xmlConfigFilename)) {
xmlConfigFileUrl = bundle.getResource(xmlConfigFilename);
}
XmlConfigBuilder xmlConfigBuilder = null;
if (xmlConfigFileUrl != null) {
try {
xmlConfigBuilder = new XmlConfigBuilder(xmlConfigFileUrl.openStream());
cfg = xmlConfigBuilder.build();
LOGGER.info("Successfully built hazelcast config from XML config file {}",
xmlConfigFilename);
} catch (FileNotFoundException e) {
LOGGER.info("FileNotFoundException trying to build hazelcast config from XML file "
+ xmlConfigFilename, e);
cfg = null;
} catch (IOException e) {
LOGGER.info("IOException trying to build hazelcast config from XML file "
+ xmlConfigFilename, e);
cfg = null;
}
}
if (cfg == null) {
LOGGER.info("Falling back to using generic Config for hazelcast");
cfg = new Config();
} else if (LOGGER.isDebugEnabled()) {
MapConfig mapConfig = cfg.getMapConfig("Product_Cache");
if (mapConfig == null) {
LOGGER.debug("mapConfig is NULL for persistentNotifications - try persistent*");
mapConfig = cfg.getMapConfig("persistent*");
if (mapConfig == null) {
LOGGER.debug("mapConfig is NULL for persistent*");
}
} else {
MapStoreConfig mapStoreConfig = mapConfig.getMapStoreConfig();
if (null != mapStoreConfig) {
LOGGER.debug("mapStoreConfig factoryClassName = {}",
mapStoreConfig.getFactoryClassName());
}
}
}
return cfg;
}
public void teardownCache() {
instance.shutdown();
}
public long getCacheDirMaxSizeMegabytes() {
LOGGER.debug("Getting max size for cache directory.");
return cacheListener.getMaxDirSizeBytes() / BYTES_IN_MEGABYTES;
}
public void setCacheDirMaxSizeMegabytes(long cacheDirMaxSizeMegabytes) {
LOGGER.debug("Setting max size for cache directory: {}", cacheDirMaxSizeMegabytes);
cacheListener.setMaxDirSizeBytes(cacheDirMaxSizeMegabytes * BYTES_IN_MEGABYTES);
}
public String getProductCacheDirectory() {
return productCacheDirectory;
}
public void setProductCacheDirectory(final String productCacheDirectory) {
String newProductCacheDirectoryDir = "";
if (!StringUtils.isEmpty(productCacheDirectory)) {
String path = FilenameUtils.normalize(productCacheDirectory);
File directory = new File(path);
// Create the directory if it doesn't exist
if ((!directory.exists() && directory.mkdirs()) || (directory.isDirectory() && directory
.canRead() && directory.canWrite())) {
LOGGER.debug("Setting product cache directory to: {}", path);
newProductCacheDirectoryDir = path;
}
}
// if productCacheDirectory is invalid or productCacheDirectory is
// an empty string, default to the DEFAULT_PRODUCT_CACHE_DIRECTORY in <karaf.home>
if (newProductCacheDirectoryDir.isEmpty()) {
try {
final File karafHomeDir = new File(System.getProperty(KARAF_HOME));
if (karafHomeDir.isDirectory()) {
final File fspDir = new File(
karafHomeDir + File.separator + DEFAULT_PRODUCT_CACHE_DIRECTORY);
// if directory does not exist, try to create it
if (fspDir.isDirectory() || fspDir.mkdirs()) {
LOGGER.debug("Setting product cache directory to: {}",
fspDir.getAbsolutePath());
newProductCacheDirectoryDir = fspDir.getAbsolutePath();
} else {
LOGGER.warn(
"Unable to create directory: {}. Please check for proper permissions to create this folder. Instead using default folder.",
fspDir.getAbsolutePath());
}
} else {
LOGGER.warn(
"Karaf home folder defined by system property {} is not a directory. Using default folder.",
KARAF_HOME);
}
} catch (NullPointerException npe) {
LOGGER.warn(
"Unable to create FileSystemProvider folder - {} system property not defined. Using default folder.",
KARAF_HOME);
}
}
this.productCacheDirectory = newProductCacheDirectoryDir;
LOGGER.debug("Set product cache directory to: {}", this.productCacheDirectory);
}
public BundleContext getContext() {
return context;
}
public void setContext(BundleContext context) {
LOGGER.debug("Setting context");
this.context = context;
}
public String getXmlConfigFilename() {
return xmlConfigFilename;
}
public void setXmlConfigFilename(String xmlConfigFilename) {
LOGGER.debug("Setting xmlConfigFilename to: {}", xmlConfigFilename);
this.xmlConfigFilename = xmlConfigFilename;
}
/**
* Returns true if resource with specified cache key is already in the process of
* being cached. This check helps clients prevent attempting to cache the same resource
* multiple times.
*
* @param key
* @return
*/
@Override
public boolean isPending(String key) {
return pendingCache.contains(key);
}
/**
* Called by ReliableResourceDownloadManager when resource has completed being
* cached to disk and is ready to be added to the cache map.
*
* @param reliableResource the resource to add to the cache map
*/
@Override
public void put(ReliableResource reliableResource) {
LOGGER.trace("ENTERING: put(ReliableResource)");
reliableResource.setLastTouchedMillis(System.currentTimeMillis());
cache.put(reliableResource.getKey(), reliableResource);
removePendingCacheEntry(reliableResource.getKey());
LOGGER.trace("EXITING: put(ReliableResource)");
}
@Override
public void removePendingCacheEntry(String cacheKey) {
if (!pendingCache.remove(cacheKey)) {
LOGGER.debug("Did not find pending cache entry with key = {}", cacheKey);
} else {
LOGGER.debug("Removed pending cache entry with key = {}", cacheKey);
}
}
@Override
public void addPendingCacheEntry(ReliableResource reliableResource) {
String cacheKey = reliableResource.getKey();
if (isPending(cacheKey)) {
LOGGER.debug("Cache entry with key = {} is already pending", cacheKey);
} else if (containsValid(cacheKey, reliableResource.getMetacard())) {
LOGGER.debug("Cache entry with key = {} is already in cache", cacheKey);
} else {
pendingCache.add(cacheKey);
}
}
/**
* @param key
* @return Resource, {@code null} if not found.
*/
@Override
public Resource getValid(String key, Metacard latestMetacard) {
LOGGER.debug("ENTERING: get()");
if (key == null) {
throw new IllegalArgumentException("Must specify non-null key");
}
if (latestMetacard == null) {
throw new IllegalArgumentException("Must specify non-null metacard");
}
LOGGER.debug("key {}", key);
ReliableResource cachedResource = (ReliableResource) cache.get(key);
// Check that ReliableResource actually maps to a file (product) in the
// product cache directory. This check handles the case if the product
// cache directory has had files deleted from it.
if (cachedResource != null) {
if (!validateCacheEntry(cachedResource, latestMetacard)) {
throw new IllegalArgumentException(
"Entry found in cache was out-of-date or otherwise invalid. Will need to be re-cached. Entry key: "
+ key);
}
if (cachedResource.hasProduct()) {
LOGGER.debug("EXITING: get() for key {}", key);
return cachedResource;
} else {
LOGGER.debug(
"Entry found in the cache, but no product found in cache directory for key = {}",
key);
cache.remove(key);
throw new IllegalArgumentException(
"Entry found in the cache, but no product found in cache directory for key = "
+ key);
}
} else {
LOGGER.debug("No product found in cache for key = {}", key);
throw new IllegalArgumentException("No product found in cache for key = " + key);
}
}
/**
* States whether an item is in the cache or not.
*
* @param key
* @return {@code true} if items exists in cache.
*/
@Override
public boolean containsValid(String key, Metacard latestMetacard) {
if (key == null) {
return false;
}
ReliableResource cachedResource = (ReliableResource) cache.get(key);
boolean result;
try {
result = cachedResource != null ?
(validateCacheEntry(cachedResource, latestMetacard)) :
false;
} catch (IllegalArgumentException e) {
LOGGER.debug(e.getMessage());
return false;
}
return result;
}
/**
* Compares the {@link Metacard} in a {@link ReliableResource} pulled from cache with a Metacard obtained directly
* from the Catalog to ensure they are the same. Typically used to determine if the cache entry is out-of-date based
* on the Catalog having an updated Metacard.
*
* @param cachedResource
* @param latestMetacard
* @return true if the cached ReliableResource still matches the most recent Metacard from the Catalog, false otherwise
* @throws IllegalArgumentException if parameters are null
*/
protected boolean validateCacheEntry(ReliableResource cachedResource, Metacard latestMetacard)
throws IllegalArgumentException {
LOGGER.trace("ENTERING: validateCacheEntry");
if (cachedResource == null || latestMetacard == null) {
throw new IllegalArgumentException(
"Neither the cachedResource nor the metacard retrieved from the catalog can be null.");
}
int cachedResourceHash = cachedResource.getMetacard().hashCode();
MetacardImpl latestMetacardImpl = new MetacardImpl(latestMetacard);
int latestMetacardHash = latestMetacardImpl.hashCode();
// compare hashes of cachedResource.getMetacard() and latestMetcard
if (cachedResourceHash == latestMetacardHash) {
LOGGER.trace("EXITING: validateCacheEntry");
return true;
} else {
File cachedFile = new File(cachedResource.getFilePath());
if (!FileUtils.deleteQuietly(cachedFile)) {
LOGGER.debug("File was not removed from cache directory. File Path: {}",
cachedResource.getFilePath());
}
cache.remove(cachedResource.getKey());
LOGGER.trace("EXITING: validateCacheEntry");
return false;
}
}
}