entityLookup)
{
this(cache, CACHE_REGION_DEFAULT, entityLookup);
}
/**
* Construct the lookup cache, using the given cache region.
*
* All keys will be unique to the given cache region, allowing the cache to be shared
* between instances of this class.
*
* @param cache the cache that will back the two-way lookups; null to have no backing
* in a cache.
* @param cacheRegion the region within the cache to use.
* @param entityLookup the instance that is able to find and persist entities
*/
@SuppressWarnings("unchecked")
public EntityLookupCache(SimpleCache cache, String cacheRegion, EntityLookupCallbackDAO entityLookup)
{
ParameterCheck.mandatory("cacheRegion", cacheRegion);
ParameterCheck.mandatory("entityLookup", entityLookup);
this.cache = cache;
this.cacheRegion = cacheRegion;
this.entityLookup = entityLookup;
}
/**
* Find the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#findByKey(Serializable) entity callback} will be used if necessary.
*
* It is up to the client code to decide if a null return value indicates a concurrency violation
* or not; the former would normally result in a concurrency-related exception such as
* {@link ConcurrencyFailureException}.
*
* @param key The entity key, which may be valid or invalid (null not allowed)
* @return Returns the key-value pair or null if the key doesn't reference an entity
*/
@SuppressWarnings("unchecked")
public Pair getByKey(K key)
{
if (key == null)
{
throw new IllegalArgumentException("An entity lookup key may not be null");
}
// Handle missing cache
if (cache == null)
{
return entityLookup.findByKey(key);
}
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
// Look in the cache
V value = (V) cache.get(keyCacheKey);
if (value != null)
{
if (value.equals(VALUE_NOT_FOUND))
{
// We checked before
return null;
}
else if (value.equals(VALUE_NULL))
{
return new Pair(key, null);
}
else
{
return new Pair(key, value);
}
}
// Resolve it
Pair entityPair = entityLookup.findByKey(key);
if (entityPair == null)
{
// Cache "not found"
cache.put(keyCacheKey, VALUE_NOT_FOUND);
}
else
{
value = entityPair.getSecond();
// Get the value key
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey != null)
{
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
// The key is good, so we can cache the value
cache.put(valueCacheKey, key);
}
cache.put(
keyCacheKey,
(value == null ? VALUE_NULL : value));
}
// Done
return entityPair;
}
/**
* Find the entity associated with the given value.
* The {@link EntityLookupCallbackDAO#findByValue(Object) entity callback} will be used if no entry exists in the cache.
*
* It is up to the client code to decide if a null return value indicates a concurrency violation
* or not; the former would normally result in a concurrency-related exception such as
* {@link ConcurrencyFailureException}.
*
* @param value The entity value, which may be valid or invalid (null is allowed)
* @return Returns the key-value pair or null if the value doesn't reference an entity
*/
@SuppressWarnings("unchecked")
public Pair getByValue(V value)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.findByValue(value);
}
// Get the value key.
// The cast to (VK) is counter-intuitive, but works because they're all just Serializable
// It's nasty, but hidden from the cache client code.
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey == null)
{
return entityLookup.findByValue(value);
}
// Look in the cache
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
K key = (K) cache.get(valueCacheKey);
// Check if we have looked this up already
if (key != null)
{
// We checked before and ...
if (key.equals(VALUE_NOT_FOUND))
{
// ... it didn't exist
return null;
}
else
{
// ... it did exist
return getByKey(key);
}
}
// Resolve it
Pair entityPair = entityLookup.findByValue(value);
if (entityPair == null)
{
// Cache "not found"
cache.put(valueCacheKey, VALUE_NOT_FOUND);
}
else
{
key = entityPair.getFirst();
// Cache the key
cache.put(valueCacheKey, key);
cache.put(
new CacheRegionKey(cacheRegion, key),
(entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
}
// Done
return entityPair;
}
/**
* Find the entity associated with the given value and create it if it doesn't exist.
* The {@link EntityLookupCallbackDAO#findByValue(Object)} and {@link EntityLookupCallbackDAO#createValue(Object)}
* will be used if necessary.
*
* @param value The entity value (null is allowed)
* @return Returns the key-value pair (new or existing and never null)
*/
@SuppressWarnings("unchecked")
public Pair getOrCreateByValue(V value)
{
// Handle missing cache
if (cache == null)
{
Pair entityPair = entityLookup.findByValue(value);
if (entityPair == null)
{
entityPair = entityLookup.createValue(value);
}
return entityPair;
}
// Get the value key
// The cast to (VK) is counter-intuitive, but works because they're all just Serializable.
// It's nasty, but hidden from the cache client code.
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey == null)
{
Pair entityPair = entityLookup.findByValue(value);
if (entityPair == null)
{
entityPair = entityLookup.createValue(value);
// Cache the value
cache.put(
new CacheRegionKey(cacheRegion, entityPair.getFirst()),
(entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
}
return entityPair;
}
// Look in the cache
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
K key = (K) cache.get(valueCacheKey);
// Check if the value is already mapped to a key
if (key != null && !key.equals(VALUE_NOT_FOUND))
{
return new Pair(key, value);
}
// Resolve it
Pair entityPair = entityLookup.findByValue(value);
if (entityPair == null)
{
// Create it
entityPair = entityLookup.createValue(value);
}
key = entityPair.getFirst();
// Cache the key and value
cache.put(valueCacheKey, key);
cache.put(
new CacheRegionKey(cacheRegion, key),
(value == null ? VALUE_NULL : value));
// Done
return entityPair;
}
/**
* Update the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#updateValue(Serializable, Object)} callback
* will be used if necessary.
*
* It is up to the client code to decide if a 0 return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key The entity key, which may be valid or invalid (null not allowed)
* @param value The new entity value (may be null)
* @return Returns the row update count.
*/
@SuppressWarnings("unchecked")
public int updateValue(K key, V value)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.updateValue(key, value);
}
// Remove entries for the key (bidirectional removal removes the old value as well)
removeByKey(key);
// Do the update
int updateCount = entityLookup.updateValue(key, value);
if (updateCount == 0)
{
// Nothing was done
return updateCount;
}
// Get the value key.
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey != null)
{
// There is a good value key, cache by value
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
cache.put(valueCacheKey, key);
}
// Cache by key
cache.put(
new CacheRegionKey(cacheRegion, key),
(value == null ? VALUE_NULL : value));
// Done
return updateCount;
}
/**
* Cache-only operation: Get the key for a given value key (note: not 'value' but 'value key').
*
* @param value The entity value key, which must be valid (null not allowed)
* @return The entity key (may be null)
*/
@SuppressWarnings("unchecked")
public K getKey(VK valueKey)
{
// There is a good value key, cache by value
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
K key = (K) cache.get(valueCacheKey);
// Check if we have looked this up already
if (key != null && key.equals(VALUE_NOT_FOUND))
{
key = null;
}
return key;
}
/**
* Cache-only operation: Get the value for a given key
*
* @param key The entity key, which may be valid or invalid (null not allowed)
* @return The entity value (may be null)
*/
@SuppressWarnings("unchecked")
public V getValue(K key)
{
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
// Look in the cache
V value = (V) cache.get(keyCacheKey);
if (value == null)
{
return null;
}
else if (value.equals(VALUE_NOT_FOUND))
{
// We checked before
return null;
}
else if (value.equals(VALUE_NULL))
{
return null;
}
else
{
return value;
}
}
/**
* Cache-only operation: Update the cache's value
*
* @param key The entity key, which may be valid or invalid (null not allowed)
* @param value The new entity value (may be null)
*/
@SuppressWarnings("unchecked")
public void setValue(K key, V value)
{
// Handle missing cache
if (cache == null)
{
return;
}
// Remove entries for the key (bidirectional removal removes the old value as well)
removeByKey(key);
// Get the value key.
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey != null)
{
// There is a good value key, cache by value
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
cache.put(valueCacheKey, key);
}
// Cache by key
cache.put(
new CacheRegionKey(cacheRegion, key),
(value == null ? VALUE_NULL : value));
// Done
}
/**
* Delete the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#deleteByKey(Serializable)} callback will be used if necessary.
*
* It is up to the client code to decide if a 0 return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key the entity key, which may be valid or invalid (null not allowed)
* @return Returns the row deletion count
*/
public int deleteByKey(K key)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.deleteByKey(key);
}
// Remove entries for the key (bidirectional removal removes the old value as well)
removeByKey(key);
// Do the delete
return entityLookup.deleteByKey(key);
}
/**
* Delete the entity having the given value..
* The {@link EntityLookupCallbackDAO#deleteByValue(Object)} callback will be used if necessary.
*
* It is up to the client code to decide if a 0 return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key the entity value, which may be valid or invalid (null allowed)
* @return Returns the row deletion count
*/
public int deleteByValue(V value)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.deleteByValue(value);
}
// Remove entries for the value
removeByValue(value);
// Do the delete
return entityLookup.deleteByValue(value);
}
/**
* Cache-only operation: Remove all cache values associated with the given key.
*/
@SuppressWarnings("unchecked")
public void removeByKey(K key)
{
// Handle missing cache
if (cache == null)
{
return;
}
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
V value = (V) cache.get(keyCacheKey);
if (value != null && !value.equals(VALUE_NOT_FOUND))
{
// Get the value key and remove it
VK valueKey = entityLookup.getValueKey(value);
if (valueKey != null)
{
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
cache.remove(valueCacheKey);
}
}
cache.remove(keyCacheKey);
}
/**
* Cache-only operation: Remove all cache values associated with the given value
*
* @param value The entity value (null is allowed)
*/
@SuppressWarnings("unchecked")
public void removeByValue(V value)
{
// Handle missing cache
if (cache == null)
{
return;
}
// Get the value key
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
if (valueKey == null)
{
// No key generated for the value. There is nothing that can be done.
return;
}
// Look in the cache
CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
K key = (K) cache.get(valueCacheKey);
// Check if the value is already mapped to a key
if (key != null && !key.equals(VALUE_NOT_FOUND))
{
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
cache.remove(keyCacheKey);
}
cache.remove(valueCacheKey);
}
/**
* Cache-only operation: Remove all cache entries
*
* NOTE: This operation removes ALL entries for ALL cache regions.
*/
public void clear()
{
// Handle missing cache
if (cache == null)
{
return;
}
cache.clear();
}
/**
* Key-wrapper used to separate cache regions, allowing a single cache to be used for different
* purposes.
* This class is distinct from the ID key so that ID-based lookups don't class with value-based lookups.
*/
private static class CacheRegionKey implements Serializable
{
private static final long serialVersionUID = -213050301938804468L;
private final String cacheRegion;
private final Serializable cacheKey;
private final int hashCode;
private CacheRegionKey(String cacheRegion, Serializable cacheKey)
{
this.cacheRegion = cacheRegion;
this.cacheKey = cacheKey;
this.hashCode = cacheRegion.hashCode() + cacheKey.hashCode();
}
@Override
public String toString()
{
return cacheRegion + "." + cacheKey.toString();
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
else if (!(obj instanceof CacheRegionKey))
{
return false;
}
CacheRegionKey that = (CacheRegionKey) obj;
return this.cacheRegion.equals(that.cacheRegion) && this.cacheKey.equals(that.cacheKey);
}
@Override
public int hashCode()
{
return hashCode;
}
}
/**
* Value-key-wrapper used to separate cache regions, allowing a single cache to be used for different
* purposes.
* This class is distinct from the region key so that ID-based lookups don't class with value-based lookups.
*/
private static class CacheRegionValueKey implements Serializable
{
private static final long serialVersionUID = 5838308035326617927L;
private final String cacheRegion;
private final Serializable cacheValueKey;
private final int hashCode;
private CacheRegionValueKey(String cacheRegion, Serializable cacheValueKey)
{
this.cacheRegion = cacheRegion;
this.cacheValueKey = cacheValueKey;
this.hashCode = cacheRegion.hashCode() + cacheValueKey.hashCode();
}
@Override
public String toString()
{
return cacheRegion + "." + cacheValueKey.toString();
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
else if (!(obj instanceof CacheRegionValueKey))
{
return false;
}
CacheRegionValueKey that = (CacheRegionValueKey) obj;
return this.cacheRegion.equals(that.cacheRegion) && this.cacheValueKey.equals(that.cacheValueKey);
}
@Override
public int hashCode()
{
return hashCode;
}
}
}