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({ "rawtypes", "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;
- }
-
- /**
- * Attempt to create the entity and, failing that, look it up.
- * This method takes the opposite approach to {@link #getOrCreateByValue(Object)}, which assumes the entity's
- * existence: in this case the entity is assumed to NOT exist.
- * The {@link EntityLookupCallbackDAO#createValue(Object)} and {@link EntityLookupCallbackDAO#findByValue(Object)}
- * will be used if necessary.
- *
- * Use this method when the data involved is seldom reused.
- *
- * @param value The entity value (null is allowed)
- * @param controlDAO an essential DAO required in order to ensure a transactionally-safe attempt at data creation
- * @return Returns the key-value pair (new or existing and never null)
- */
- public Pair createOrGetByValue(V value, ControlDAO controlDAO)
- {
- if (controlDAO == null)
- {
- throw new IllegalArgumentException("The ControlDAO is required in order to perform a safe attempted insert.");
- }
- Savepoint savepoint = controlDAO.createSavepoint("EntityLookupCache.createOrGetByValue");
- try
- {
- Pair entityPair = entityLookup.createValue(value);
- controlDAO.releaseSavepoint(savepoint);
- // Cache it
- if (cache != null)
- {
- cache.put(
- new CacheRegionKey(cacheRegion, entityPair.getFirst()),
- (entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
- }
- // It's been created and cached
- return entityPair;
- }
- catch (Exception e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- // Fall through to the usual way, which should find it if the failure cause was a duplicate key
- return getOrCreateByValue(value);
- }
- }
-
- /**
- * 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 getByKey(key);
- }
- // 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)
- // but leave the key as it will get updated
- removeByKey(key, false);
-
- // 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;
- }
-
+package org.alfresco.repo.cache.lookup;
+
+import java.io.Serializable;
+import java.sql.Savepoint;
+
+import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.extensions.surf.util.ParameterCheck;
+
+import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.repo.domain.control.ControlDAO;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.util.Pair;
+
+/**
+ * A cache for two-way lookups of database entities. These are characterized by having a unique key (perhaps a database ID) and a separate unique key that identifies the object. If no cache is given, then all calls are passed through to the backing DAO.
+ *
+ * The keys must have good equals
and hashCode
implementations and must respect the case-sensitivity of the use-case.
+ *
+ * All keys will be unique to the given cache region, allowing the cache to be shared between instances of this class.
+ *
+ * Generics:
+ *
+ * - K: The database unique identifier.
+ * - V: The value stored against K.
+ * - VK: The a value-derived key that will be used as a cache key when caching K for lookups by V. This can be the value itself if it is itself a good key.
+ *
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public class EntityLookupCache
+{
+ /**
+ * Interface to support lookups of the entities using keys and values.
+ */
+ public static interface EntityLookupCallbackDAO
+ {
+ /**
+ * Resolve the given value into a unique value key that can be used to find the entity's ID. A return value should be small and efficient; don't return a value if this is not possible.
+ *
+ * Implementations will often return the value itself, provided that the value is both serializable and has a good equals
and hashCode
.
+ *
+ * Were no adequate key can be generated for the value, then null can be returned. In this case, the {@link #findByValue(Object) findByValue} method might not even do a search and just return null itself i.e. if it is difficult to look the value up in storage then it is probably difficult to generate a cache key from it, too.. In this scenario, the cache will be purely for key-based lookups
+ *
+ * @param value
+ * the full value being keyed (never null)
+ * @return Returns the business key representing the entity, or null if an economical key cannot be generated.
+ */
+ VK1 getValueKey(V1 value);
+
+ /**
+ * Find an entity for a given key.
+ *
+ * @param key
+ * the key (ID) used to identify the entity (never null)
+ * @return Return the entity or null if no entity is exists for the ID
+ */
+ Pair findByKey(K1 key);
+
+ /**
+ * Find and entity using the given value key. The equals
and hashCode
methods of the value object should respect case-sensitivity in the same way that this lookup treats case-sensitivity i.e. if the equals
method is case-sensitive then this method should look the entity up using a case-sensitive search.
+ *
+ * Since this is a cache backed by some sort of database, null values are allowed by the cache. The implementation of this method can throw an exception if null is not appropriate for the use-case.
+ *
+ * If the search is impossible or expensive, this method should just return null. This would usually be the case if the {@link #getValueKey(Object) getValueKey} method also returned null i.e. if it is difficult to look the value up in storage then it is probably difficult to generate a cache key from it, too.
+ *
+ * @param value
+ * the value (business object) used to identify the entity (null allowed).
+ * @return Return the entity or null if no entity matches the given value
+ */
+ Pair findByValue(V1 value);
+
+ /**
+ * Create an entity using the given values. It is valid to assume that the entity does not exist within the current transaction at least.
+ *
+ * Since persistence mechanisms often allow null values, these can be expected here. The implementation must throw an exception if null is not allowed for the specific use-case.
+ *
+ * @param value
+ * the value (business object) used to identify the entity (null allowed).
+ * @return Return the newly-created entity ID-value pair
+ */
+ Pair createValue(V1 value);
+
+ /**
+ * Update the entity identified by the given key.
+ *
+ * It is up to the client code to decide if a 0 return value indicates a concurrency violation or not.
+ *
+ * @param key
+ * the existing key (ID) used to identify the entity (never null)
+ * @param value
+ * the new value
+ * @return Returns the row update count.
+ * @throws UnsupportedOperationException
+ * if entity updates are not supported
+ */
+ int updateValue(K1 key, V1 value);
+
+ /**
+ * Delete an entity for the given key.
+ *
+ * It is up to the client code to decide if a 0 return value indicates a concurrency violation or not.
+ *
+ * @param key
+ * the key (ID) used to identify the entity (never null)
+ * @return Returns the row deletion count.
+ * @throws UnsupportedOperationException
+ * if entity deletion is not supported
+ */
+ int deleteByKey(K1 key);
+
+ /**
+ * Delete an entity for the given value.
+ *
+ * It is up to the client code to decide if a 0 return value indicates a concurrency violation or not.
+ *
+ * @param value
+ * the value (business object) used to identify the enitity (null allowed)
+ * @return Returns the row deletion count.
+ * @throws UnsupportedOperationException
+ * if entity deletion is not supported
+ */
+ int deleteByValue(V1 value);
+ }
+
+ /**
+ * Adaptor for implementations that support immutable entities. The update and delete operations throw {@link UnsupportedOperationException}.
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+ public static abstract class EntityLookupCallbackDAOAdaptor
+ implements EntityLookupCallbackDAO
+ {
+ /**
+ * This implementation never finds a value and is backed by {@link #getValueKey(Object)} returning nothing.
+ *
+ * @return Returns null always
+ */
+ public Pair findByValue(V2 value)
+ {
+ return null;
+ }
+
+ /**
+ * This implementation does not find by value and is backed by {@link #findByValue(Object)} returning nothing.
+ *
+ * @return Returns null always
+ */
+ public VK2 getValueKey(V2 value)
+ {
+ return null;
+ }
+
+ /**
+ * Disallows the operation.
+ *
+ * @throws UnsupportedOperationException
+ * always
+ */
+ public int updateValue(K2 key, V2 value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Disallows the operation.
+ *
+ * @throws UnsupportedOperationException
+ * always
+ */
+ public int deleteByKey(K2 key)
+ {
+ throw new UnsupportedOperationException("Entity deletion by key is not supported");
+ }
+
+ /**
+ * Disallows the operation.
+ *
+ * @throws UnsupportedOperationException
+ * always
+ */
+ public int deleteByValue(V2 value)
+ {
+ throw new UnsupportedOperationException("Entity deletion by value is not supported");
+ }
+ }
+
+ /**
+ * A valid null
value i.e. a value that has been persisted as null.
+ */
+ private static final Serializable VALUE_NULL = "@@VALUE_NULL@@";
+ /**
+ * A value that was not found or persisted.
+ */
+ private static final Serializable VALUE_NOT_FOUND = "@@VALUE_NOT_FOUND@@";
+ /**
+ * The cache region that will be used (see {@link CacheRegionKey}) in all the cache keys
+ */
+ private static final String CACHE_REGION_DEFAULT = "DEFAULT";
+
+ private final SimpleCache cache;
+ private final EntityLookupCallbackDAO entityLookup;
+ private final String cacheRegion;
+
+ /**
+ * Construct the lookup cache without any cache. All calls are passed directly to the underlying DAO entity lookup.
+ *
+ * @param entityLookup
+ * the instance that is able to find and persist entities
+ */
+ public EntityLookupCache(EntityLookupCallbackDAO entityLookup)
+ {
+ this(null, CACHE_REGION_DEFAULT, entityLookup);
+ }
+
+ /**
+ * Construct the lookup cache, using the {@link #CACHE_REGION_DEFAULT default cache region}.
+ *
+ * @param cache
+ * the cache that will back the two-way lookups
+ * @param entityLookup
+ * the instance that is able to find and persist entities
+ */
+ @SuppressWarnings("rawtypes")
+ public EntityLookupCache(SimpleCache cache, EntityLookupCallbackDAO 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({"rawtypes", "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;
+ }
+
+ /**
+ * Attempt to create the entity and, failing that, look it up.
+ * This method takes the opposite approach to {@link #getOrCreateByValue(Object)}, which assumes the entity's existence: in this case the entity is assumed to NOT exist. The {@link EntityLookupCallbackDAO#createValue(Object)} and {@link EntityLookupCallbackDAO#findByValue(Object)} will be used if necessary.
+ *
+ * Use this method when the data involved is seldom reused.
+ *
+ * @param value
+ * The entity value (null is allowed)
+ * @param controlDAO
+ * an essential DAO required in order to ensure a transactionally-safe attempt at data creation
+ * @return Returns the key-value pair (new or existing and never null)
+ */
+ public Pair createOrGetByValue(V value, ControlDAO controlDAO)
+ {
+ if (controlDAO == null)
+ {
+ throw new IllegalArgumentException("The ControlDAO is required in order to perform a safe attempted insert.");
+ }
+ Savepoint savepoint = controlDAO.createSavepoint("EntityLookupCache.createOrGetByValue");
+ try
+ {
+ Pair entityPair = entityLookup.createValue(value);
+ controlDAO.releaseSavepoint(savepoint);
+ // Cache it
+ if (cache != null)
+ {
+ VK valueKey = (value == null) ? (VK) VALUE_NULL : entityLookup.getValueKey(value);
+ cache.put(new CacheRegionValueKey(cacheRegion, valueKey), entityPair.getFirst());
+ cache.put(
+ new CacheRegionKey(cacheRegion, entityPair.getFirst()),
+ (entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
+ }
+ // It's been created and cached
+ return entityPair;
+ }
+ catch (Exception e)
+ {
+ controlDAO.rollbackToSavepoint(savepoint);
+ // Fall through to the usual way, which should find it if the failure cause was a duplicate key
+ return getOrCreateByValue(value);
+ }
+ }
+
+ /**
+ * 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 getByKey(key);
+ }
+ // 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)
+ // but leave the key as it will get updated
+ removeByKey(key, false);
+
+ // 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;
+ }
+
+ /**
+ * Find the entity associated with the given value if its cached
+ *
+ * @param value
+ * The entity value (null is not allowed)
+ * @return Returns the key-value pair (existing or null)
+ */
+ @SuppressWarnings("unchecked")
+ public Pair getCachedEntityByValue(V value)
+ {
+ if (cache == null || value == null)
+ {
+ return null;
+ }
+
+ VK valueKey = entityLookup.getValueKey(value);
+ if (valueKey == null)
+ {
+ return null;
+ }
+
+ // Retrieve the cached value
+ CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
+ K key = (K) cache.get(valueCacheKey);
+
+ if (key != null && !key.equals(VALUE_NOT_FOUND))
+ {
+ return getByKey(key);
+ }
+
+ return null;
+ }
+
/**
* Cache-only operation: Get the key for a given value key (note: not 'value' but 'value key').
*
- * @param valueKey The entity value key, which must be valid (null not allowed)
- * @return The entity key (may be null)
+ * @param valueKey
+ * 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)
- // but leave the key as it will get updated
- removeByKey(key, false);
-
- // 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}.
+ 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 value the entity value, which may be valid or invalid (null allowed)
- * @return Returns the row deletion count
+ * @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)
+ // but leave the key as it will get updated
+ removeByKey(key, false);
+
+ // 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 value
+ * 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.
- */
- public void removeByKey(K key)
- {
- // Handle missing cache
- if (cache == null)
- {
- return;
- }
-
- removeByKey(key, true);
- }
-
- /**
- * Cache-only operation: Remove all cache values associated with the given key.
- *
- * @param removeKey true to remove the given key's entry
- */
- @SuppressWarnings("unchecked")
- private void removeByKey(K key, boolean removeKey)
- {
- 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);
- }
- }
- if (removeKey)
- {
- 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();
- }
-}
+ {
+ // 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.
+ */
+ public void removeByKey(K key)
+ {
+ // Handle missing cache
+ if (cache == null)
+ {
+ return;
+ }
+
+ removeByKey(key, true);
+ }
+
+ /**
+ * Cache-only operation: Remove all cache values associated with the given key.
+ *
+ * @param removeKey
+ * true to remove the given key's entry
+ */
+ @SuppressWarnings("unchecked")
+ private void removeByKey(K key, boolean removeKey)
+ {
+ 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);
+ }
+ }
+ if (removeKey)
+ {
+ 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();
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java b/repository/src/main/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
index a7457d8eef..048f54a15d 100644
--- a/repository/src/main/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
+++ b/repository/src/main/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
@@ -1,769 +1,785 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2016 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco 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.
- *
- * Alfresco 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.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.domain.contentdata;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-
-import org.alfresco.repo.cache.SimpleCache;
-import org.alfresco.repo.cache.lookup.EntityLookupCache;
-import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
-import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner;
-import org.alfresco.repo.domain.control.ControlDAO;
-import org.alfresco.repo.domain.encoding.EncodingDAO;
-import org.alfresco.repo.domain.locale.LocaleDAO;
-import org.alfresco.repo.domain.mimetype.MimetypeDAO;
-import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
-import org.alfresco.repo.transaction.TransactionalResourceHelper;
-import org.alfresco.service.cmr.repository.ContentData;
-import org.alfresco.util.EqualsHelper;
-import org.alfresco.util.Pair;
-import org.alfresco.util.transaction.TransactionListenerAdapter;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.dao.ConcurrencyFailureException;
-import org.springframework.dao.DataIntegrityViolationException;
-
-/**
- * Abstract implementation for ContentData DAO.
- *
- * This provides basic services such as caching, but defers to the underlying implementation
- * for CRUD operations.
- *
- * The DAO deals in {@link ContentData} instances. The cache is primarily present to decode
- * IDs into ContentData
instances.
- *
- * @author Derek Hulley
- * @author sglover
- * @since 3.2
- */
-public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
-{
- private static final String CACHE_REGION_CONTENT_DATA = "ContentData";
- private static final String CACHE_REGION_CONTENT_URL = "ContentUrl";
-
- /**
- * Content URL IDs to delete before final commit.
- */
- private static final String KEY_PRE_COMMIT_CONTENT_URL_DELETIONS = "AbstractContentDataDAOImpl.PreCommitContentUrlDeletions";
-
- private static Log logger = LogFactory.getLog(AbstractContentDataDAOImpl.class);
-
- private final ContentDataCallbackDAO contentDataCallbackDAO;
- private final ContentUrlCallbackDAO contentUrlCallbackDAO;
- protected ControlDAO controlDAO;
- protected MimetypeDAO mimetypeDAO;
- protected EncodingDAO encodingDAO;
- protected LocaleDAO localeDAO;
- private EagerContentStoreCleaner contentStoreCleaner;
-
- /**
- * Cache for the ContentData class:
- * KEY: ID
- * VALUE: ContentData object
- * VALUE KEY: NONE
- */
- private EntityLookupCache contentDataCache;
-
- private EntityLookupCache contentUrlCache;
-
- /**
- * Default constructor
- */
- public AbstractContentDataDAOImpl()
- {
- this.contentDataCallbackDAO = new ContentDataCallbackDAO();
- this.contentUrlCallbackDAO = new ContentUrlCallbackDAO();
- this.contentDataCache = new EntityLookupCache(contentDataCallbackDAO);
- this.contentUrlCache = new EntityLookupCache(contentUrlCallbackDAO);
- }
-
- public void setControlDAO(ControlDAO controlDAO)
- {
- this.controlDAO = controlDAO;
- }
-
- public void setMimetypeDAO(MimetypeDAO mimetypeDAO)
- {
- this.mimetypeDAO = mimetypeDAO;
- }
-
- public void setEncodingDAO(EncodingDAO encodingDAO)
- {
- this.encodingDAO = encodingDAO;
- }
-
- public void setLocaleDAO(LocaleDAO localeDAO)
- {
- this.localeDAO = localeDAO;
- }
-
- /**
- * Set this property to enable eager cleanup of orphaned content.
- *
- * @param contentStoreCleaner an eager cleaner (may be null)
- */
- public void setContentStoreCleaner(EagerContentStoreCleaner contentStoreCleaner)
- {
- this.contentStoreCleaner = contentStoreCleaner;
- }
-
- /**
- * @param contentDataCache the cache of IDs to ContentData and vice versa
- */
- public void setContentDataCache(SimpleCache contentDataCache)
- {
- this.contentDataCache = new EntityLookupCache(
- contentDataCache,
- CACHE_REGION_CONTENT_DATA,
- contentDataCallbackDAO);
- }
-
- public void setContentUrlCache(SimpleCache contentUrlCache)
- {
- this.contentUrlCache = new EntityLookupCache(
- contentUrlCache,
- CACHE_REGION_CONTENT_URL,
- contentUrlCallbackDAO);
- }
-
- /**
- * A content_url entity was dereferenced. This makes no assumptions about the
- * current references - dereference deletion is handled in the commit phase.
- */
- protected void registerDereferencedContentUrl(String contentUrl)
- {
- Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
- if (contentUrls.size() == 0)
- {
- ContentUrlDeleteTransactionListener listener = new ContentUrlDeleteTransactionListener();
- AlfrescoTransactionSupport.bindListener(listener);
- }
- contentUrls.add(contentUrl);
- }
-
- @Override
- public Pair createContentData(ContentData contentData)
- {
- if (contentData == null)
- {
- throw new IllegalArgumentException("ContentData values cannot be null");
- }
- Pair entityPair = contentDataCache.getOrCreateByValue(contentData);
- return entityPair;
- }
-
- @Override
- public Pair getContentData(Long id)
- {
- if (id == null)
- {
- throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
- }
- Pair entityPair = contentDataCache.getByKey(id);
- if (entityPair == null)
- {
- throw new DataIntegrityViolationException("No ContentData value exists for ID " + id);
- }
- return entityPair;
- }
-
- /**
- * Internally update a URL or create a new one if it does not exist
- */
- private boolean updateContentUrl(ContentUrlEntity contentUrl)
- {
- int result = 0;
- if (contentUrl == null)
- {
- throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
- }
- Pair pair = contentUrlCache.getByValue(contentUrl);
- if(pair != null)
- {
- result = contentUrlCache.updateValue(pair.getFirst(), contentUrl);
- }
- else
- {
- pair = contentUrlCache.getOrCreateByValue(contentUrl);
- result = contentUrlCache.updateValue(pair.getFirst(), contentUrl);
- }
- return result == 1 ? true : false;
- }
-
- @Override
- public ContentUrlEntity getContentUrl(String contentUrl)
- {
- if (contentUrl == null)
- {
- throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
- }
- ContentUrlEntity entity = new ContentUrlEntity();
- entity.setContentUrl(contentUrl);
- Pair pair = contentUrlCache.getByValue(entity);
- return (pair == null ? null : pair.getSecond());
- }
-
- @Override
- public ContentUrlEntity getContentUrl(Long contentUrlId)
- {
- if (contentUrlId == null)
- {
- throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
- }
- Pair pair = contentUrlCache.getByKey(contentUrlId);
- return (pair == null ? null : pair.getSecond());
- }
-
- public void cacheContentDataForNodes(Set nodeIds)
- {
- for (ContentDataEntity entity : getContentDataEntitiesForNodes(nodeIds))
- {
- contentDataCache.setValue(entity.getId(), makeContentData(entity));
- }
- }
-
- @Override
- public void updateContentData(Long id, ContentData contentData)
- {
- if (id == null)
- {
- throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
- }
- if (contentData == null)
- {
- throw new IllegalArgumentException("Cannot update ContentData with a null.");
- }
- contentData = sanitizeMimetype(contentData);
- int updated = contentDataCache.updateValue(id, contentData);
- if (updated < 1)
- {
- throw new ConcurrencyFailureException("ContentData with ID " + id + " not updated");
- }
- }
-
- private ContentData sanitizeMimetype(ContentData contentData)
- {
- String mimetype = contentData.getMimetype();
- if (mimetype != null)
- {
- mimetype = mimetype.toLowerCase();
- contentData = ContentData.setMimetype(contentData, mimetype);
- }
- return contentData;
- }
-
- @Override
- public void deleteContentData(Long id)
- {
- if (id == null)
- {
- throw new IllegalArgumentException("Cannot delete ContentData by null ID.");
- }
- int deleted = contentDataCache.deleteByKey(id);
- if (deleted < 1)
- {
- throw new ConcurrencyFailureException("ContentData with ID " + id + " no longer exists");
- }
- return;
- }
-
- /**
- * Callback for alf_content_data DAO.
- */
- private class ContentDataCallbackDAO extends EntityLookupCallbackDAOAdaptor
- {
- public Pair createValue(ContentData value)
- {
- value = sanitizeMimetype(value);
- ContentDataEntity contentDataEntity = createContentDataEntity(value);
- // Done
- return new Pair(contentDataEntity.getId(), value);
- }
-
- public Pair findByKey(Long key)
- {
- ContentDataEntity contentDataEntity = getContentDataEntity(key);
- if (contentDataEntity == null)
- {
- return null;
- }
- ContentData contentData = makeContentData(contentDataEntity);
- // Done
- return new Pair(key, contentData);
- }
-
- @Override
- public int updateValue(Long key, ContentData value)
- {
- ContentDataEntity contentDataEntity = getContentDataEntity(key);
- if (contentDataEntity == null)
- {
- return 0; // The client (outer-level code) will decide if this is an error
- }
- return updateContentDataEntity(contentDataEntity, value);
- }
-
- @Override
- public int deleteByKey(Long key)
- {
- return deleteContentDataEntity(key);
- }
- }
-
- /**
- * Callback for alf_content_url DAO.
- */
- private class ContentUrlCallbackDAO extends EntityLookupCallbackDAOAdaptor
- {
- /**
- * @return Returns the Node's NodeRef
- */
- @Override
- public String getValueKey(ContentUrlEntity value)
- {
- return value.getContentUrl();
- }
-
- /**
- * Looks the entity up based on the ContentURL of the given node
- */
- @Override
- public Pair findByValue(ContentUrlEntity entity)
- {
- String contentUrl = entity.getContentUrl();
- ContentUrlEntity ret = getContentUrlEntity(contentUrl);
- // Validate if this entity has exactly the value we are looking for or if it is a CRC collision
- if (ret != null && !entity.getContentUrl().equals(ret.getContentUrl()))
- {
- throw new IllegalArgumentException("Collision detected for this contentURL. '" + entity.getContentUrl()
- + "' collides with existing contentURL '" + ret.getContentUrl() + "'. (ContentUrlShort;ContentUrlCrc) pair collision: ('"
- + entity.getContentUrlShort() + "';'" + entity.getContentUrlCrc() + "')");
- }
- return (ret != null ? new Pair(ret.getId(), ret) : null);
- }
-
- public Pair createValue(ContentUrlEntity value)
- {
- ContentUrlEntity contentUrlEntity = createContentUrlEntity(value.getContentUrl(), value.getSize(), value.getContentUrlKey());
- // Done
- return new Pair(contentUrlEntity.getId(), contentUrlEntity);
- }
-
- public Pair findByKey(Long id)
- {
- ContentUrlEntity contentUrlEntity = getContentUrlEntity(id);
- if (contentUrlEntity == null)
- {
- return null;
- }
- // Done
- return new Pair(contentUrlEntity.getId(), contentUrlEntity);
- }
-
- @Override
- public int updateValue(Long id, ContentUrlEntity value)
- {
- ContentUrlEntity contentUrlEntity = getContentUrlEntity(id);
- if (contentUrlEntity == null)
- {
- return 0; // The client (outer-level code) will decide if this is an error
- }
- return updateContentUrlEntity(contentUrlEntity, value);
- }
-
- @Override
- public int deleteByKey(Long id)
- {
- return deleteContentUrlEntity(id);
- }
- }
-
- /**
- * Translates this instance into an externally-usable ContentData
instance.
- */
- private ContentData makeContentData(ContentDataEntity contentDataEntity)
- {
- // Decode content URL
- Long contentUrlId = contentDataEntity.getContentUrlId();
- String contentUrl = null;
- if(contentUrlId != null)
- {
- Pair entityPair = contentUrlCache.getByKey(contentUrlId);
- if (entityPair == null)
- {
- throw new DataIntegrityViolationException("No ContentUrl value exists for ID " + contentUrlId);
- }
- ContentUrlEntity contentUrlEntity = entityPair.getSecond();
- contentUrl = contentUrlEntity.getContentUrl();
- }
-
- long size = contentDataEntity.getSize() == null ? 0L : contentDataEntity.getSize().longValue();
-
- // Decode mimetype
- Long mimetypeId = contentDataEntity.getMimetypeId();
- String mimetype = null;
- if (mimetypeId != null)
- {
- mimetype = mimetypeDAO.getMimetype(mimetypeId).getSecond();
- }
-
- // Decode encoding
- Long encodingId = contentDataEntity.getEncodingId();
- String encoding = null;
- if (encodingId != null)
- {
- encoding = encodingDAO.getEncoding(encodingId).getSecond();
- }
-
- // Decode locale
- Long localeId = contentDataEntity.getLocaleId();
- Locale locale = null;
- if (localeId != null)
- {
- locale = localeDAO.getLocalePair(localeId).getSecond();
- }
-
- // Build the ContentData
- ContentData contentData = new ContentData(contentUrl, mimetype, size, encoding, locale);
- // Done
- return contentData;
- }
-
- /**
- * Translates the {@link ContentData} into persistable values using the helper DAOs
- */
- protected ContentDataEntity createContentDataEntity(ContentData contentData)
- {
- // Resolve the content URL
- Long contentUrlId = null;
- String contentUrl = contentData.getContentUrl();
- long size = contentData.getSize();
- if (contentUrl != null)
- {
- ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
- contentUrlEntity.setContentUrl(contentUrl);
- contentUrlEntity.setSize(size);
- Pair pair = contentUrlCache.createOrGetByValue(contentUrlEntity, controlDAO);
- contentUrlId = pair.getFirst();
- }
-
- // Resolve the mimetype
- Long mimetypeId = null;
- String mimetype = contentData.getMimetype();
- if (mimetype != null)
- {
- mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
- }
- // Resolve the encoding
- Long encodingId = null;
- String encoding = contentData.getEncoding();
- if (encoding != null)
- {
- encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
- }
- // Resolve the locale
- Long localeId = null;
- Locale locale = contentData.getLocale();
- if (locale != null)
- {
- localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
- }
-
- // Create ContentDataEntity
- ContentDataEntity contentDataEntity = createContentDataEntity(contentUrlId, mimetypeId, encodingId, localeId);
- // Done
- return contentDataEntity;
- }
-
- /**
- * Translates the {@link ContentData} into persistable values using the helper DAOs
- */
- protected int updateContentDataEntity(ContentDataEntity contentDataEntity, ContentData contentData)
- {
- // Resolve the content URL
- Long oldContentUrlId = contentDataEntity.getContentUrlId();
- ContentUrlEntity contentUrlEntity = null;
- if(oldContentUrlId != null)
- {
- Pair entityPair = contentUrlCache.getByKey(oldContentUrlId);
- if (entityPair == null)
- {
- throw new DataIntegrityViolationException("No ContentUrl value exists for ID " + oldContentUrlId);
- }
- contentUrlEntity = entityPair.getSecond();
- }
-
- String oldContentUrl = (contentUrlEntity != null ? contentUrlEntity.getContentUrl() : null);
- String newContentUrl = contentData.getContentUrl();
- if (!EqualsHelper.nullSafeEquals(oldContentUrl, newContentUrl))
- {
- if (oldContentUrl != null)
- {
- // We have a changed value. The old content URL has been dereferenced.
- registerDereferencedContentUrl(oldContentUrl);
- }
- if (newContentUrl != null)
- {
- if(contentUrlEntity == null)
- {
- contentUrlEntity = new ContentUrlEntity();
- contentUrlEntity.setContentUrl(newContentUrl);
- }
- Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
- Long newContentUrlId = pair.getFirst();
- contentUrlEntity.setId(newContentUrlId);
- contentDataEntity.setContentUrlId(newContentUrlId);
- }
- else
- {
- contentDataEntity.setId(null);
- contentDataEntity.setContentUrlId(null);
- }
- }
-
- // Resolve the mimetype
- Long mimetypeId = null;
- String mimetype = contentData.getMimetype();
- if (mimetype != null)
- {
- mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
- }
- // Resolve the encoding
- Long encodingId = null;
- String encoding = contentData.getEncoding();
- if (encoding != null)
- {
- encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
- }
- // Resolve the locale
- Long localeId = null;
- Locale locale = contentData.getLocale();
- if (locale != null)
- {
- localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
- }
-
- contentDataEntity.setMimetypeId(mimetypeId);
- contentDataEntity.setEncodingId(encodingId);
- contentDataEntity.setLocaleId(localeId);
-
- return updateContentDataEntity(contentDataEntity);
- }
-
- @Override
- public boolean updateContentUrlKey(String contentUrl, ContentUrlKeyEntity contentUrlKey)
- {
- ContentUrlEntity existing = getContentUrl(contentUrl);
- if (existing == null)
- {
- existing = getOrCreateContentUrl(contentUrl, contentUrlKey.getUnencryptedFileSize());
- }
- ContentUrlEntity entity = ContentUrlEntity.setContentUrlKey(existing, contentUrlKey);
- return updateContentUrl(entity);
- }
-
- @Override
- public boolean updateContentUrlKey(long contentUrlId, ContentUrlKeyEntity contentUrlKey)
- {
- boolean success = true;
-
- ContentUrlEntity existing = getContentUrl(contentUrlId);
- if(existing != null)
- {
- ContentUrlEntity entity = ContentUrlEntity.setContentUrlKey(existing, contentUrlKey);
- updateContentUrl(entity);
- }
- else
- {
- if (logger.isDebugEnabled())
- {
- logger.debug("No content url, not updating symmetric key");
- }
- success = false;
- }
-
- return success;
- }
-
- @Override
- public ContentUrlEntity getOrCreateContentUrl(String contentUrl)
- {
- ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
- contentUrlEntity.setContentUrl(contentUrl);
- Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
- Long newContentUrlId = pair.getFirst();
- contentUrlEntity.setId(newContentUrlId);
- // Done
- return contentUrlEntity;
- }
-
- @Override
- public ContentUrlEntity getOrCreateContentUrl(String contentUrl, long size)
- {
- ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
- contentUrlEntity.setContentUrl(contentUrl);
- contentUrlEntity.setSize(size);
- Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
- Long newContentUrlId = pair.getFirst();
- contentUrlEntity.setId(newContentUrlId);
- // Done
- return contentUrlEntity;
- }
-
- /**
- * @param contentUrl the content URL to create or search for
- */
- protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size, ContentUrlKeyEntity contentUrlKey);
-
- /**
- * @param id the ID of the content url entity
- * @return Return the entity or null if it doesn't exist
- */
- protected abstract ContentUrlEntity getContentUrlEntity(Long id);
-
- protected abstract ContentUrlEntity getContentUrlEntity(String contentUrl);
-
-
- /**
- * @param contentUrl the URL of the content url entity
- * @return Return the entity or null if it doesn't exist or is still
- * referenced by a content_data entity
- */
- protected abstract ContentUrlEntity getContentUrlEntityUnreferenced(String contentUrl);
-
- /**
- * Update a content URL with the given orphan time
- *
- * @param id the unique ID of the entity
- * @param orphanTime the time (ms since epoch) that the entity was orphaned
- * @param oldOrphanTime the orphan time we expect to update for optimistic locking (may be null)
- * @return Returns the number of rows updated
- */
- protected abstract int updateContentUrlOrphanTime(Long id, Long orphanTime, Long oldOrphanTime);
-
- /**
- * Create the row for the alf_content_data
- */
- protected abstract ContentDataEntity createContentDataEntity(
- Long contentUrlId,
- Long mimetypeId,
- Long encodingId,
- Long localeId);
-
- /**
- * @param id the entity ID
- * @return Returns the entity or null if it doesn't exist
- */
- protected abstract ContentDataEntity getContentDataEntity(Long id);
-
- /**
- * @param nodeIds the node ID
- * @return Returns the associated entities or null if none exist
- */
- protected abstract List getContentDataEntitiesForNodes(Set nodeIds);
-
- /**
- * Update an existing alf_content_data entity
- *
- * @param entity the existing entity that will be updated
- * @return Returns the number of rows updated (should be 1)
- */
- protected abstract int updateContentDataEntity(ContentDataEntity entity);
-
- /**
- * Delete the entity with the given ID
- *
- * @return Returns the number of rows deleted
- */
- protected abstract int deleteContentDataEntity(Long id);
-
- protected abstract int deleteContentUrlEntity(long id);
- protected abstract int updateContentUrlEntity(ContentUrlEntity existing, ContentUrlEntity entity);
-
- /**
- * Transactional listener that deletes unreferenced content_url entities.
- *
- * @author Derek Hulley
- */
- public class ContentUrlDeleteTransactionListener extends TransactionListenerAdapter
- {
- @Override
- public void beforeCommit(boolean readOnly)
- {
- // Ignore read-only
- if (readOnly)
- {
- return;
- }
- Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
- long orphanTime = System.currentTimeMillis();
- for (String contentUrl : contentUrls)
- {
- ContentUrlEntity contentUrlEntity = getContentUrlEntityUnreferenced(contentUrl);
- if (contentUrlEntity == null)
- {
- // It is still referenced, so ignore it
- continue;
- }
- // Pop this in the queue for deletion from the content store
- boolean isEagerCleanup = contentStoreCleaner.registerOrphanedContentUrl(contentUrl);
- if (!isEagerCleanup)
- {
- // We mark the URL as orphaned.
- // The content binary is not scheduled for immediate removal so just mark the
- // row's orphan time. Concurrently, it is possible for multiple references
- // to be made WHILE the orphan time is set, but we handle that separately.
- Long contentUrlId = contentUrlEntity.getId();
- Long oldOrphanTime = contentUrlEntity.getOrphanTime();
- int updated = updateContentUrlOrphanTime(contentUrlId, orphanTime, oldOrphanTime);
- if (updated != 1)
- {
- throw new ConcurrencyFailureException(
- "Failed to update content URL orphan time: " + contentUrlEntity);
- }
- }
- else
- {
- // ALERT!!!
- // The content is scheduled for deletion once this transaction commits.
- // We need to make sure that the URL is not re-referenced by another transaction.
- List contentUrlId = Collections.singletonList(contentUrlEntity.getId());
- int deleted = deleteContentUrls(contentUrlId);
- if (deleted != 1)
- {
- throw new ConcurrencyFailureException(
- "Failed to delete eagerly-reaped content URL: " + contentUrlEntity);
- }
- }
- }
- contentUrls.clear();
- }
- }
-}
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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.
+ *
+ * Alfresco 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.domain.contentdata;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+
+import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.repo.cache.lookup.EntityLookupCache;
+import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
+import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner;
+import org.alfresco.repo.domain.control.ControlDAO;
+import org.alfresco.repo.domain.encoding.EncodingDAO;
+import org.alfresco.repo.domain.locale.LocaleDAO;
+import org.alfresco.repo.domain.mimetype.MimetypeDAO;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.TransactionalResourceHelper;
+import org.alfresco.service.cmr.repository.ContentData;
+import org.alfresco.util.EqualsHelper;
+import org.alfresco.util.Pair;
+import org.alfresco.util.transaction.TransactionListenerAdapter;
+
+/**
+ * Abstract implementation for ContentData DAO.
+ *
+ * This provides basic services such as caching, but defers to the underlying implementation for CRUD operations.
+ *
+ * The DAO deals in {@link ContentData} instances. The cache is primarily present to decode IDs into ContentData
instances.
+ *
+ * @author Derek Hulley
+ * @author sglover
+ * @since 3.2
+ */
+public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
+{
+ private static final String CACHE_REGION_CONTENT_DATA = "ContentData";
+ private static final String CACHE_REGION_CONTENT_URL = "ContentUrl";
+
+ /**
+ * Content URL IDs to delete before final commit.
+ */
+ private static final String KEY_PRE_COMMIT_CONTENT_URL_DELETIONS = "AbstractContentDataDAOImpl.PreCommitContentUrlDeletions";
+
+ private static Log logger = LogFactory.getLog(AbstractContentDataDAOImpl.class);
+
+ private final ContentDataCallbackDAO contentDataCallbackDAO;
+ private final ContentUrlCallbackDAO contentUrlCallbackDAO;
+ protected ControlDAO controlDAO;
+ protected MimetypeDAO mimetypeDAO;
+ protected EncodingDAO encodingDAO;
+ protected LocaleDAO localeDAO;
+ private EagerContentStoreCleaner contentStoreCleaner;
+
+ /**
+ * Cache for the ContentData class:
+ * KEY: ID
+ * VALUE: ContentData object
+ * VALUE KEY: NONE
+ */
+ private EntityLookupCache contentDataCache;
+
+ private EntityLookupCache contentUrlCache;
+
+ /**
+ * Default constructor
+ */
+ public AbstractContentDataDAOImpl()
+ {
+ this.contentDataCallbackDAO = new ContentDataCallbackDAO();
+ this.contentUrlCallbackDAO = new ContentUrlCallbackDAO();
+ this.contentDataCache = new EntityLookupCache(contentDataCallbackDAO);
+ this.contentUrlCache = new EntityLookupCache(contentUrlCallbackDAO);
+ }
+
+ public void setControlDAO(ControlDAO controlDAO)
+ {
+ this.controlDAO = controlDAO;
+ }
+
+ public void setMimetypeDAO(MimetypeDAO mimetypeDAO)
+ {
+ this.mimetypeDAO = mimetypeDAO;
+ }
+
+ public void setEncodingDAO(EncodingDAO encodingDAO)
+ {
+ this.encodingDAO = encodingDAO;
+ }
+
+ public void setLocaleDAO(LocaleDAO localeDAO)
+ {
+ this.localeDAO = localeDAO;
+ }
+
+ /**
+ * Set this property to enable eager cleanup of orphaned content.
+ *
+ * @param contentStoreCleaner
+ * an eager cleaner (may be null)
+ */
+ public void setContentStoreCleaner(EagerContentStoreCleaner contentStoreCleaner)
+ {
+ this.contentStoreCleaner = contentStoreCleaner;
+ }
+
+ /**
+ * @param contentDataCache
+ * the cache of IDs to ContentData and vice versa
+ */
+ public void setContentDataCache(SimpleCache contentDataCache)
+ {
+ this.contentDataCache = new EntityLookupCache(
+ contentDataCache,
+ CACHE_REGION_CONTENT_DATA,
+ contentDataCallbackDAO);
+ }
+
+ public void setContentUrlCache(SimpleCache contentUrlCache)
+ {
+ this.contentUrlCache = new EntityLookupCache(
+ contentUrlCache,
+ CACHE_REGION_CONTENT_URL,
+ contentUrlCallbackDAO);
+ }
+
+ /**
+ * A content_url entity was dereferenced. This makes no assumptions about the current references - dereference deletion is handled in the commit phase.
+ */
+ protected void registerDereferencedContentUrl(String contentUrl)
+ {
+ Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
+ if (contentUrls.size() == 0)
+ {
+ ContentUrlDeleteTransactionListener listener = new ContentUrlDeleteTransactionListener();
+ AlfrescoTransactionSupport.bindListener(listener);
+ }
+ contentUrls.add(contentUrl);
+ }
+
+ @Override
+ public Pair createContentData(ContentData contentData)
+ {
+ if (contentData == null)
+ {
+ throw new IllegalArgumentException("ContentData values cannot be null");
+ }
+ Pair entityPair = contentDataCache.getOrCreateByValue(contentData);
+ return entityPair;
+ }
+
+ @Override
+ public Pair getContentData(Long id)
+ {
+ if (id == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ Pair entityPair = contentDataCache.getByKey(id);
+ if (entityPair == null)
+ {
+ throw new DataIntegrityViolationException("No ContentData value exists for ID " + id);
+ }
+ return entityPair;
+ }
+
+ /**
+ * Internally update a URL or create a new one if it does not exist
+ */
+ private boolean updateContentUrl(ContentUrlEntity contentUrl)
+ {
+ int result = 0;
+ if (contentUrl == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ Pair pair = contentUrlCache.getByValue(contentUrl);
+ if (pair != null)
+ {
+ result = contentUrlCache.updateValue(pair.getFirst(), contentUrl);
+ }
+ else
+ {
+ pair = contentUrlCache.getOrCreateByValue(contentUrl);
+ result = contentUrlCache.updateValue(pair.getFirst(), contentUrl);
+ }
+ return result == 1 ? true : false;
+ }
+
+ @Override
+ public ContentUrlEntity getContentUrl(String contentUrl)
+ {
+ if (contentUrl == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ ContentUrlEntity entity = new ContentUrlEntity();
+ entity.setContentUrl(contentUrl);
+ Pair pair = contentUrlCache.getByValue(entity);
+ return (pair == null ? null : pair.getSecond());
+ }
+
+ @Override
+ public ContentUrlEntity getContentUrl(Long contentUrlId)
+ {
+ if (contentUrlId == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ Pair pair = contentUrlCache.getByKey(contentUrlId);
+ return (pair == null ? null : pair.getSecond());
+ }
+
+ public void cacheContentDataForNodes(Set nodeIds)
+ {
+ for (ContentDataEntity entity : getContentDataEntitiesForNodes(nodeIds))
+ {
+ contentDataCache.setValue(entity.getId(), makeContentData(entity));
+ }
+ }
+
+ @Override
+ public void updateContentData(Long id, ContentData contentData)
+ {
+ if (id == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ if (contentData == null)
+ {
+ throw new IllegalArgumentException("Cannot update ContentData with a null.");
+ }
+ contentData = sanitizeMimetype(contentData);
+ int updated = contentDataCache.updateValue(id, contentData);
+ if (updated < 1)
+ {
+ throw new ConcurrencyFailureException("ContentData with ID " + id + " not updated");
+ }
+ }
+
+ private ContentData sanitizeMimetype(ContentData contentData)
+ {
+ String mimetype = contentData.getMimetype();
+ if (mimetype != null)
+ {
+ mimetype = mimetype.toLowerCase();
+ contentData = ContentData.setMimetype(contentData, mimetype);
+ }
+ return contentData;
+ }
+
+ @Override
+ public void deleteContentData(Long id)
+ {
+ if (id == null)
+ {
+ throw new IllegalArgumentException("Cannot delete ContentData by null ID.");
+ }
+ int deleted = contentDataCache.deleteByKey(id);
+ if (deleted < 1)
+ {
+ throw new ConcurrencyFailureException("ContentData with ID " + id + " no longer exists");
+ }
+ return;
+ }
+
+ /**
+ * Callback for alf_content_data DAO.
+ */
+ private class ContentDataCallbackDAO extends EntityLookupCallbackDAOAdaptor
+ {
+ public Pair createValue(ContentData value)
+ {
+ value = sanitizeMimetype(value);
+ ContentDataEntity contentDataEntity = createContentDataEntity(value);
+ // Done
+ return new Pair(contentDataEntity.getId(), value);
+ }
+
+ public Pair findByKey(Long key)
+ {
+ ContentDataEntity contentDataEntity = getContentDataEntity(key);
+ if (contentDataEntity == null)
+ {
+ return null;
+ }
+ ContentData contentData = makeContentData(contentDataEntity);
+ // Done
+ return new Pair(key, contentData);
+ }
+
+ @Override
+ public int updateValue(Long key, ContentData value)
+ {
+ ContentDataEntity contentDataEntity = getContentDataEntity(key);
+ if (contentDataEntity == null)
+ {
+ return 0; // The client (outer-level code) will decide if this is an error
+ }
+ return updateContentDataEntity(contentDataEntity, value);
+ }
+
+ @Override
+ public int deleteByKey(Long key)
+ {
+ return deleteContentDataEntity(key);
+ }
+ }
+
+ /**
+ * Callback for alf_content_url DAO.
+ */
+ private class ContentUrlCallbackDAO extends EntityLookupCallbackDAOAdaptor
+ {
+ /**
+ * @return Returns the Node's NodeRef
+ */
+ @Override
+ public String getValueKey(ContentUrlEntity value)
+ {
+ return value.getContentUrl();
+ }
+
+ /**
+ * Looks the entity up based on the ContentURL of the given node
+ */
+ @Override
+ public Pair findByValue(ContentUrlEntity entity)
+ {
+ String contentUrl = entity.getContentUrl();
+ ContentUrlEntity ret = getContentUrlEntity(contentUrl);
+ // Validate if this entity has exactly the value we are looking for or if it is a CRC collision
+ if (ret != null && !entity.getContentUrl().equals(ret.getContentUrl()))
+ {
+ throw new IllegalArgumentException("Collision detected for this contentURL. '" + entity.getContentUrl()
+ + "' collides with existing contentURL '" + ret.getContentUrl() + "'. (ContentUrlShort;ContentUrlCrc) pair collision: ('"
+ + entity.getContentUrlShort() + "';'" + entity.getContentUrlCrc() + "')");
+ }
+ return (ret != null ? new Pair(ret.getId(), ret) : null);
+ }
+
+ public Pair createValue(ContentUrlEntity value)
+ {
+ ContentUrlEntity contentUrlEntity = createContentUrlEntity(value.getContentUrl(), value.getSize(), value.getContentUrlKey());
+ // Done
+ return new Pair(contentUrlEntity.getId(), contentUrlEntity);
+ }
+
+ public Pair findByKey(Long id)
+ {
+ ContentUrlEntity contentUrlEntity = getContentUrlEntity(id);
+ if (contentUrlEntity == null)
+ {
+ return null;
+ }
+ // Done
+ return new Pair(contentUrlEntity.getId(), contentUrlEntity);
+ }
+
+ @Override
+ public int updateValue(Long id, ContentUrlEntity value)
+ {
+ ContentUrlEntity contentUrlEntity = getContentUrlEntity(id);
+ if (contentUrlEntity == null)
+ {
+ return 0; // The client (outer-level code) will decide if this is an error
+ }
+ return updateContentUrlEntity(contentUrlEntity, value);
+ }
+
+ @Override
+ public int deleteByKey(Long id)
+ {
+ return deleteContentUrlEntity(id);
+ }
+ }
+
+ /**
+ * Translates this instance into an externally-usable ContentData
instance.
+ */
+ private ContentData makeContentData(ContentDataEntity contentDataEntity)
+ {
+ // Decode content URL
+ Long contentUrlId = contentDataEntity.getContentUrlId();
+ String contentUrl = null;
+ if (contentUrlId != null)
+ {
+ Pair entityPair = contentUrlCache.getByKey(contentUrlId);
+ if (entityPair == null)
+ {
+ throw new DataIntegrityViolationException("No ContentUrl value exists for ID " + contentUrlId);
+ }
+ ContentUrlEntity contentUrlEntity = entityPair.getSecond();
+ contentUrl = contentUrlEntity.getContentUrl();
+ }
+
+ long size = contentDataEntity.getSize() == null ? 0L : contentDataEntity.getSize().longValue();
+
+ // Decode mimetype
+ Long mimetypeId = contentDataEntity.getMimetypeId();
+ String mimetype = null;
+ if (mimetypeId != null)
+ {
+ mimetype = mimetypeDAO.getMimetype(mimetypeId).getSecond();
+ }
+
+ // Decode encoding
+ Long encodingId = contentDataEntity.getEncodingId();
+ String encoding = null;
+ if (encodingId != null)
+ {
+ encoding = encodingDAO.getEncoding(encodingId).getSecond();
+ }
+
+ // Decode locale
+ Long localeId = contentDataEntity.getLocaleId();
+ Locale locale = null;
+ if (localeId != null)
+ {
+ locale = localeDAO.getLocalePair(localeId).getSecond();
+ }
+
+ // Build the ContentData
+ ContentData contentData = new ContentData(contentUrl, mimetype, size, encoding, locale);
+ // Done
+ return contentData;
+ }
+
+ /**
+ * Translates the {@link ContentData} into persistable values using the helper DAOs
+ */
+ protected ContentDataEntity createContentDataEntity(ContentData contentData)
+ {
+ // Resolve the content URL
+ Long contentUrlId = null;
+ String contentUrl = contentData.getContentUrl();
+ long size = contentData.getSize();
+ if (contentUrl != null)
+ {
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setContentUrl(contentUrl);
+ contentUrlEntity.setSize(size);
+
+ // Attempt to get the data from cache
+ Pair pair = contentUrlCache.getCachedEntityByValue(contentUrlEntity);
+
+ if (pair == null)
+ {
+ pair = contentUrlCache.createOrGetByValue(contentUrlEntity, controlDAO);
+ }
+
+ contentUrlId = pair.getFirst();
+ }
+
+ // Resolve the mimetype
+ Long mimetypeId = null;
+ String mimetype = contentData.getMimetype();
+ if (mimetype != null)
+ {
+ mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
+ }
+ // Resolve the encoding
+ Long encodingId = null;
+ String encoding = contentData.getEncoding();
+ if (encoding != null)
+ {
+ encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
+ }
+ // Resolve the locale
+ Long localeId = null;
+ Locale locale = contentData.getLocale();
+ if (locale != null)
+ {
+ localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
+ }
+
+ // Create ContentDataEntity
+ ContentDataEntity contentDataEntity = createContentDataEntity(contentUrlId, mimetypeId, encodingId, localeId);
+ // Done
+ return contentDataEntity;
+ }
+
+ /**
+ * Translates the {@link ContentData} into persistable values using the helper DAOs
+ */
+ protected int updateContentDataEntity(ContentDataEntity contentDataEntity, ContentData contentData)
+ {
+ // Resolve the content URL
+ Long oldContentUrlId = contentDataEntity.getContentUrlId();
+ ContentUrlEntity contentUrlEntity = null;
+ if (oldContentUrlId != null)
+ {
+ Pair entityPair = contentUrlCache.getByKey(oldContentUrlId);
+ if (entityPair == null)
+ {
+ throw new DataIntegrityViolationException("No ContentUrl value exists for ID " + oldContentUrlId);
+ }
+ contentUrlEntity = entityPair.getSecond();
+ }
+
+ String oldContentUrl = (contentUrlEntity != null ? contentUrlEntity.getContentUrl() : null);
+ String newContentUrl = contentData.getContentUrl();
+ if (!EqualsHelper.nullSafeEquals(oldContentUrl, newContentUrl))
+ {
+ if (oldContentUrl != null)
+ {
+ // We have a changed value. The old content URL has been dereferenced.
+ registerDereferencedContentUrl(oldContentUrl);
+ }
+ if (newContentUrl != null)
+ {
+ if (contentUrlEntity == null)
+ {
+ contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setContentUrl(newContentUrl);
+ }
+ Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
+ Long newContentUrlId = pair.getFirst();
+ contentUrlEntity.setId(newContentUrlId);
+ contentDataEntity.setContentUrlId(newContentUrlId);
+ }
+ else
+ {
+ contentDataEntity.setId(null);
+ contentDataEntity.setContentUrlId(null);
+ }
+ }
+
+ // Resolve the mimetype
+ Long mimetypeId = null;
+ String mimetype = contentData.getMimetype();
+ if (mimetype != null)
+ {
+ mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
+ }
+ // Resolve the encoding
+ Long encodingId = null;
+ String encoding = contentData.getEncoding();
+ if (encoding != null)
+ {
+ encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
+ }
+ // Resolve the locale
+ Long localeId = null;
+ Locale locale = contentData.getLocale();
+ if (locale != null)
+ {
+ localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
+ }
+
+ contentDataEntity.setMimetypeId(mimetypeId);
+ contentDataEntity.setEncodingId(encodingId);
+ contentDataEntity.setLocaleId(localeId);
+
+ return updateContentDataEntity(contentDataEntity);
+ }
+
+ @Override
+ public boolean updateContentUrlKey(String contentUrl, ContentUrlKeyEntity contentUrlKey)
+ {
+ ContentUrlEntity existing = getContentUrl(contentUrl);
+ if (existing == null)
+ {
+ existing = getOrCreateContentUrl(contentUrl, contentUrlKey.getUnencryptedFileSize());
+ }
+ ContentUrlEntity entity = ContentUrlEntity.setContentUrlKey(existing, contentUrlKey);
+ return updateContentUrl(entity);
+ }
+
+ @Override
+ public boolean updateContentUrlKey(long contentUrlId, ContentUrlKeyEntity contentUrlKey)
+ {
+ boolean success = true;
+
+ ContentUrlEntity existing = getContentUrl(contentUrlId);
+ if (existing != null)
+ {
+ ContentUrlEntity entity = ContentUrlEntity.setContentUrlKey(existing, contentUrlKey);
+ updateContentUrl(entity);
+ }
+ else
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("No content url, not updating symmetric key");
+ }
+ success = false;
+ }
+
+ return success;
+ }
+
+ @Override
+ public ContentUrlEntity getOrCreateContentUrl(String contentUrl)
+ {
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setContentUrl(contentUrl);
+ Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
+ Long newContentUrlId = pair.getFirst();
+ contentUrlEntity.setId(newContentUrlId);
+ // Done
+ return contentUrlEntity;
+ }
+
+ @Override
+ public ContentUrlEntity getOrCreateContentUrl(String contentUrl, long size)
+ {
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setContentUrl(contentUrl);
+ contentUrlEntity.setSize(size);
+ Pair pair = contentUrlCache.getOrCreateByValue(contentUrlEntity);
+ Long newContentUrlId = pair.getFirst();
+ contentUrlEntity.setId(newContentUrlId);
+ // Done
+ return contentUrlEntity;
+ }
+
+ /**
+ * @param contentUrl
+ * the content URL to create or search for
+ */
+ protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size, ContentUrlKeyEntity contentUrlKey);
+
+ /**
+ * @param id
+ * the ID of the content url entity
+ * @return Return the entity or null if it doesn't exist
+ */
+ protected abstract ContentUrlEntity getContentUrlEntity(Long id);
+
+ protected abstract ContentUrlEntity getContentUrlEntity(String contentUrl);
+
+ /**
+ * @param contentUrl
+ * the URL of the content url entity
+ * @return Return the entity or null if it doesn't exist or is still referenced by a content_data entity
+ */
+ protected abstract ContentUrlEntity getContentUrlEntityUnreferenced(String contentUrl);
+
+ /**
+ * Update a content URL with the given orphan time
+ *
+ * @param id
+ * the unique ID of the entity
+ * @param orphanTime
+ * the time (ms since epoch) that the entity was orphaned
+ * @param oldOrphanTime
+ * the orphan time we expect to update for optimistic locking (may be null)
+ * @return Returns the number of rows updated
+ */
+ protected abstract int updateContentUrlOrphanTime(Long id, Long orphanTime, Long oldOrphanTime);
+
+ /**
+ * Create the row for the alf_content_data
+ */
+ protected abstract ContentDataEntity createContentDataEntity(
+ Long contentUrlId,
+ Long mimetypeId,
+ Long encodingId,
+ Long localeId);
+
+ /**
+ * @param id
+ * the entity ID
+ * @return Returns the entity or null if it doesn't exist
+ */
+ protected abstract ContentDataEntity getContentDataEntity(Long id);
+
+ /**
+ * @param nodeIds
+ * the node ID
+ * @return Returns the associated entities or null if none exist
+ */
+ protected abstract List getContentDataEntitiesForNodes(Set nodeIds);
+
+ /**
+ * Update an existing alf_content_data entity
+ *
+ * @param entity
+ * the existing entity that will be updated
+ * @return Returns the number of rows updated (should be 1)
+ */
+ protected abstract int updateContentDataEntity(ContentDataEntity entity);
+
+ /**
+ * Delete the entity with the given ID
+ *
+ * @return Returns the number of rows deleted
+ */
+ protected abstract int deleteContentDataEntity(Long id);
+
+ protected abstract int deleteContentUrlEntity(long id);
+
+ protected abstract int updateContentUrlEntity(ContentUrlEntity existing, ContentUrlEntity entity);
+
+ /**
+ * Transactional listener that deletes unreferenced content_url entities.
+ *
+ * @author Derek Hulley
+ */
+ public class ContentUrlDeleteTransactionListener extends TransactionListenerAdapter
+ {
+ @Override
+ public void beforeCommit(boolean readOnly)
+ {
+ // Ignore read-only
+ if (readOnly)
+ {
+ return;
+ }
+ Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
+ long orphanTime = System.currentTimeMillis();
+ for (String contentUrl : contentUrls)
+ {
+ ContentUrlEntity contentUrlEntity = getContentUrlEntityUnreferenced(contentUrl);
+ if (contentUrlEntity == null)
+ {
+ // It is still referenced, so ignore it
+ continue;
+ }
+ // Pop this in the queue for deletion from the content store
+ boolean isEagerCleanup = contentStoreCleaner.registerOrphanedContentUrl(contentUrl);
+ if (!isEagerCleanup)
+ {
+ // We mark the URL as orphaned.
+ // The content binary is not scheduled for immediate removal so just mark the
+ // row's orphan time. Concurrently, it is possible for multiple references
+ // to be made WHILE the orphan time is set, but we handle that separately.
+ Long contentUrlId = contentUrlEntity.getId();
+ Long oldOrphanTime = contentUrlEntity.getOrphanTime();
+ int updated = updateContentUrlOrphanTime(contentUrlId, orphanTime, oldOrphanTime);
+ if (updated != 1)
+ {
+ throw new ConcurrencyFailureException(
+ "Failed to update content URL orphan time: " + contentUrlEntity);
+ }
+ }
+ else
+ {
+ // ALERT!!!
+ // The content is scheduled for deletion once this transaction commits.
+ // We need to make sure that the URL is not re-referenced by another transaction.
+ List contentUrlId = Collections.singletonList(contentUrlEntity.getId());
+ int deleted = deleteContentUrls(contentUrlId);
+ if (deleted != 1)
+ {
+ throw new ConcurrencyFailureException(
+ "Failed to delete eagerly-reaped content URL: " + contentUrlEntity);
+ }
+ }
+ }
+ contentUrls.clear();
+ }
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java b/repository/src/test/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java
index 11d68b82e6..dadd1d87b3 100644
--- a/repository/src/test/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java
+++ b/repository/src/test/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java
@@ -21,391 +21,431 @@
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
- * #L%
+ * #L%
*/
-package org.alfresco.repo.cache.lookup;
-
-import java.sql.Savepoint;
-import java.util.Map;
-import java.util.TreeMap;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.TestCase;
-
-import org.alfresco.repo.cache.MemoryCache;
-import org.alfresco.repo.cache.SimpleCache;
-import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAO;
-import org.alfresco.repo.domain.control.ControlDAO;
-import org.alfresco.util.EqualsHelper;
-import org.alfresco.util.Pair;
-import org.mockito.Mockito;
-import org.springframework.dao.DuplicateKeyException;
-
-/**
- * A cache for two-way lookups of database entities. These are characterized by having a unique
- * key (perhaps a database ID) and a separate unique key that identifies the object.
- *
- * The keys must have good equals
and hashCode implementations and
- * must respect the case-sensitivity of the use-case.
- *
- * @author Derek Hulley
- * @since 3.2
- */
-public class EntityLookupCacheTest extends TestCase implements EntityLookupCallbackDAO
-{
- SimpleCache cache;
- private EntityLookupCache entityLookupCacheA;
- private EntityLookupCache entityLookupCacheB;
- private TreeMap database;
- private ControlDAO controlDAO;
-
- @Override
- protected void setUp() throws Exception
- {
- cache = new MemoryCache();
- entityLookupCacheA = new EntityLookupCache(cache, "A", this);
- entityLookupCacheB = new EntityLookupCache(cache, "B", this);
- database = new TreeMap();
-
- controlDAO = Mockito.mock(ControlDAO.class);
- Mockito.when(controlDAO.createSavepoint(Mockito.anyString())).thenReturn(Mockito.mock(Savepoint.class));
- }
-
- public void testLookupsUsingIncorrectValue() throws Exception
- {
- try
- {
- // Keep the "database" empty
- entityLookupCacheA.getByValue(this);
- }
- catch (AssertionFailedError e)
- {
- // Expected
- }
- }
-
- public void testLookupAgainstEmpty() throws Exception
- {
- TestValue value = new TestValue("AAA");
- Pair entityPair = entityLookupCacheA.getByValue(value);
- assertNull(entityPair);
- assertTrue(database.isEmpty());
-
- // Now do lookup or create
- entityPair = entityLookupCacheA.getOrCreateByValue(value);
- assertNotNull("Expected a value to be found", entityPair);
- Long entityId = entityPair.getFirst();
- assertTrue("Database ID should have been created", database.containsKey(entityId));
- assertEquals("Database value incorrect", value.val, database.get(entityId));
-
- // Do lookup or create again
- entityPair = entityLookupCacheA.getOrCreateByValue(value);
- assertNotNull("Expected a value to be found", entityPair);
- assertEquals("Expected same entity ID", entityId, entityPair.getFirst());
-
- // Look it up using the value
- entityPair = entityLookupCacheA.getByValue(value);
- assertNotNull("Lookup after create should work", entityPair);
-
- // Look it up using the ID
- entityPair = entityLookupCacheA.getByKey(entityId);
- assertNotNull("Lookup by key should work after create", entityPair);
- assertTrue("Looked-up type incorrect", entityPair.getSecond() instanceof TestValue);
- assertEquals("Looked-up type value incorrect", value, entityPair.getSecond());
- }
-
- public void testLookupAgainstExisting() throws Exception
- {
- // Put some values in the "database"
- createValue(new TestValue("AAA"));
- createValue(new TestValue("BBB"));
- createValue(new TestValue("CCC"));
-
- // Look up by value
- Pair entityPair = entityLookupCacheA.getByValue(new TestValue("AAA"));
- assertNotNull("Expected value to be found", entityPair);
- assertEquals("ID is incorrect", Long.valueOf(1), entityPair.getFirst());
-
- // Look up by ID
- entityPair = entityLookupCacheA.getByKey(Long.valueOf(2));
- assertNotNull("Expected value to be found", entityPair);
-
- // Do lookup or create
- entityPair = entityLookupCacheA.getByValue(new TestValue("CCC"));
- assertNotNull("Expected value to be found", entityPair);
- assertEquals("ID is incorrect", Long.valueOf(3), entityPair.getFirst());
- }
-
- public void testRegions() throws Exception
- {
- TestValue valueAAA = new TestValue("AAA");
- Pair entityPairAAA = entityLookupCacheA.getOrCreateByValue(valueAAA);
- assertNotNull(entityPairAAA);
- assertEquals("AAA", database.get(entityPairAAA.getFirst()));
- assertEquals(2, cache.getKeys().size());
-
- TestValue valueBBB = new TestValue("BBB");
- Pair entityPairBBB = entityLookupCacheB.getOrCreateByValue(valueBBB);
- assertNotNull(entityPairBBB);
- assertEquals("BBB", database.get(entityPairBBB.getFirst()));
- assertEquals(4, cache.getKeys().size());
-
- // Now cross-check against the caches and make sure that the cache
- entityPairBBB = entityLookupCacheA.getByValue(valueBBB);
- assertEquals(6, cache.getKeys().size());
- entityPairBBB = entityLookupCacheB.getByValue(valueAAA);
- assertEquals(8, cache.getKeys().size());
- }
-
- public void testNullLookups() throws Exception
- {
- TestValue valueNull = null;
- Pair entityPairNull = entityLookupCacheA.getOrCreateByValue(valueNull);
- assertNotNull(entityPairNull);
- assertTrue(database.containsKey(entityPairNull.getFirst()));
- assertNull(database.get(entityPairNull.getFirst()));
- assertEquals(2, cache.getKeys().size());
-
- // Look it up again
- Pair entityPairCheck = entityLookupCacheA.getOrCreateByValue(valueNull);
- assertNotNull(entityPairNull);
- assertTrue(database.containsKey(entityPairNull.getFirst()));
- assertNull(database.get(entityPairNull.getFirst()));
- assertEquals(entityPairNull, entityPairCheck);
- }
-
- public void testGetOrCreate() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
-
- Pair entityPairOneCheck = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOneCheck);
- assertEquals(id, entityPairOneCheck.getFirst());
- }
-
- public void testCreateOrGet() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- Pair entityPairOne = entityLookupCacheA.createOrGetByValue(valueOne, controlDAO);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(1, cache.getKeys().size());
-
- Pair entityPairOneCheck = entityLookupCacheA.createOrGetByValue(valueOne, controlDAO);
- assertNotNull(entityPairOneCheck);
- assertEquals(id, entityPairOneCheck.getFirst());
- }
-
- public void testUpdate() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- TestValue valueTwo = new TestValue(getName() + "-TWO");
- Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
-
- // Update
- int updateCount = entityLookupCacheA.updateValue(id, valueTwo);
- assertEquals("Update count was incorrect.", 1, updateCount);
- assertEquals(valueTwo.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
- }
-
- public void testDeleteByKey() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
-
- // Delete
- int deleteCount = entityLookupCacheA.deleteByKey(id);
- assertEquals("Delete count was incorrect.", 1, deleteCount);
- assertNull(database.get(id));
- assertEquals(0, cache.getKeys().size());
- }
-
- public void testDeleteByValue() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
-
- // Delete
- int deleteCount = entityLookupCacheA.deleteByValue(valueOne);
- assertEquals("Delete count was incorrect.", 1, deleteCount);
- assertNull(database.get(id));
- assertEquals(0, cache.getKeys().size());
- }
-
- public void testClear() throws Exception
- {
- TestValue valueOne = new TestValue(getName() + "-ONE");
- Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
- assertNotNull(entityPairOne);
- Long id = entityPairOne.getFirst();
- assertEquals(valueOne.val, database.get(id));
- assertEquals(2, cache.getKeys().size());
-
- // Clear it
- entityLookupCacheA.clear();
- assertEquals(valueOne.val, database.get(id)); // Must still be in database
- assertEquals(0, cache.getKeys().size()); // ... but cache must be empty
- }
-
- /**
- * Helper class to represent business object
- */
- private static class TestValue
- {
- private final String val;
- private TestValue(String val)
- {
- this.val = val;
- }
- @Override
- public boolean equals(Object obj)
- {
- if (obj == null || !(obj instanceof TestValue))
- {
- return false;
- }
- return val.equals( ((TestValue)obj).val );
- }
- @Override
- public int hashCode()
- {
- return val.hashCode();
- }
-
- }
-
- public String getValueKey(Object value)
- {
- assertNotNull(value);
- assertTrue(value instanceof TestValue);
- String dbValue = ((TestValue)value).val;
- return dbValue;
- }
-
- public Pair findByKey(Long key)
- {
- assertNotNull(key);
-
- String dbValue = database.get(key);
- if (dbValue == null)
- {
- return null;
- }
- // Make a value object
- TestValue value = new TestValue(dbValue);
- return new Pair(key, value);
- }
-
- public Pair findByValue(Object value)
- {
- assertTrue(value == null || value instanceof TestValue);
- String dbValue = (value == null) ? null : ((TestValue)value).val;
-
- for (Map.Entry entry : database.entrySet())
- {
- if (EqualsHelper.nullSafeEquals(entry.getValue(), dbValue))
- {
- return new Pair(entry.getKey(), entry.getValue());
- }
- }
- return null;
- }
-
- /**
- * Simulate creation of a new database entry
- */
- public Pair createValue(Object value)
- {
- assertTrue(value == null || value instanceof TestValue);
- String dbValue = (value == null) ? null : ((TestValue)value).val;
-
- // Kick out any duplicate values
- if (database.containsValue(dbValue))
- {
- throw new DuplicateKeyException("Value is duplicated: " + value);
- }
-
- // Get the last key
- Long lastKey = database.isEmpty() ? null : database.lastKey();
- Long newKey = null;
- if (lastKey == null)
- {
- newKey = Long.valueOf(1);
- }
- else
- {
- newKey = Long.valueOf(lastKey.longValue() + 1);
- }
- database.put(newKey, dbValue);
- return new Pair(newKey, value);
- }
-
- public int updateValue(Long key, Object value)
- {
- assertNotNull(key);
- assertTrue(value == null || value instanceof TestValue);
-
- // Find it
- Pair entityPair = findByKey(key);
- if (entityPair == null)
- {
- return 0;
- }
- else
- {
- database.put(key, ((TestValue)value).val);
- return 1;
- }
- }
-
- public int deleteByKey(Long key)
- {
- assertNotNull(key);
-
- if (database.containsKey(key))
- {
- database.remove(key);
- return 1;
- }
- else
- {
- return 0;
- }
- }
-
- public int deleteByValue(Object value)
- {
- assertTrue(value == null || value instanceof TestValue);
-
- // Find it
- Pair entityPair = findByValue(value);
- if (entityPair == null)
- {
- return 0;
- }
- else
- {
- database.remove(entityPair.getFirst());
- return 1;
- }
- }
-}
+package org.alfresco.repo.cache.lookup;
+
+import static org.junit.Assert.*;
+
+import java.sql.Savepoint;
+import java.util.Map;
+import java.util.TreeMap;
+
+import junit.framework.AssertionFailedError;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.dao.DuplicateKeyException;
+
+import org.alfresco.repo.cache.MemoryCache;
+import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAO;
+import org.alfresco.repo.domain.control.ControlDAO;
+import org.alfresco.util.EqualsHelper;
+import org.alfresco.util.Pair;
+
+/**
+ * A cache for two-way lookups of database entities. These are characterized by having a unique key (perhaps a database ID) and a separate unique key that identifies the object.
+ *
+ * The keys must have good equals
and hashCode implementations and must respect the case-sensitivity of the use-case.
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public class EntityLookupCacheTest implements EntityLookupCallbackDAO
+{
+ SimpleCache cache;
+ private EntityLookupCache entityLookupCacheA;
+ private EntityLookupCache entityLookupCacheB;
+ private TreeMap database;
+ private ControlDAO controlDAO;
+
+ @Before
+ protected void setUp() throws Exception
+ {
+ cache = new MemoryCache();
+ entityLookupCacheA = new EntityLookupCache(cache, "A", this);
+ entityLookupCacheB = new EntityLookupCache(cache, "B", this);
+ database = new TreeMap();
+
+ controlDAO = Mockito.mock(ControlDAO.class);
+ Mockito.when(controlDAO.createSavepoint(Mockito.anyString())).thenReturn(Mockito.mock(Savepoint.class));
+ }
+
+ @Test
+ public void testLookupsUsingIncorrectValue() throws Exception
+ {
+ try
+ {
+ // Keep the "database" empty
+ entityLookupCacheA.getByValue(this);
+ }
+ catch (AssertionFailedError e)
+ {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testLookupAgainstEmpty() throws Exception
+ {
+ TestValue value = new TestValue("AAA");
+ Pair entityPair = entityLookupCacheA.getByValue(value);
+ assertNull(entityPair);
+ assertTrue(database.isEmpty());
+
+ // Now do lookup or create
+ entityPair = entityLookupCacheA.getOrCreateByValue(value);
+ assertNotNull("Expected a value to be found", entityPair);
+ Long entityId = entityPair.getFirst();
+ assertTrue("Database ID should have been created", database.containsKey(entityId));
+ assertEquals("Database value incorrect", value.val, database.get(entityId));
+
+ // Do lookup or create again
+ entityPair = entityLookupCacheA.getOrCreateByValue(value);
+ assertNotNull("Expected a value to be found", entityPair);
+ assertEquals("Expected same entity ID", entityId, entityPair.getFirst());
+
+ // Look it up using the value
+ entityPair = entityLookupCacheA.getByValue(value);
+ assertNotNull("Lookup after create should work", entityPair);
+
+ // Look it up using the ID
+ entityPair = entityLookupCacheA.getByKey(entityId);
+ assertNotNull("Lookup by key should work after create", entityPair);
+ assertTrue("Looked-up type incorrect", entityPair.getSecond() instanceof TestValue);
+ assertEquals("Looked-up type value incorrect", value, entityPair.getSecond());
+ }
+
+ @Test
+ public void testLookupAgainstExisting() throws Exception
+ {
+ // Put some values in the "database"
+ createValue(new TestValue("AAA"));
+ createValue(new TestValue("BBB"));
+ createValue(new TestValue("CCC"));
+
+ // Look up by value
+ Pair entityPair = entityLookupCacheA.getByValue(new TestValue("AAA"));
+ assertNotNull("Expected value to be found", entityPair);
+ assertEquals("ID is incorrect", Long.valueOf(1), entityPair.getFirst());
+
+ // Look up by ID
+ entityPair = entityLookupCacheA.getByKey(Long.valueOf(2));
+ assertNotNull("Expected value to be found", entityPair);
+
+ // Do lookup or create
+ entityPair = entityLookupCacheA.getByValue(new TestValue("CCC"));
+ assertNotNull("Expected value to be found", entityPair);
+ assertEquals("ID is incorrect", Long.valueOf(3), entityPair.getFirst());
+ }
+
+ @Test
+ public void testRegions() throws Exception
+ {
+ TestValue valueAAA = new TestValue("AAA");
+ Pair entityPairAAA = entityLookupCacheA.getOrCreateByValue(valueAAA);
+ assertNotNull(entityPairAAA);
+ assertEquals("AAA", database.get(entityPairAAA.getFirst()));
+ assertEquals(2, cache.getKeys().size());
+
+ TestValue valueBBB = new TestValue("BBB");
+ Pair entityPairBBB = entityLookupCacheB.getOrCreateByValue(valueBBB);
+ assertNotNull(entityPairBBB);
+ assertEquals("BBB", database.get(entityPairBBB.getFirst()));
+ assertEquals(4, cache.getKeys().size());
+
+ // Now cross-check against the caches and make sure that the cache
+ entityPairBBB = entityLookupCacheA.getByValue(valueBBB);
+ assertEquals(6, cache.getKeys().size());
+ entityPairBBB = entityLookupCacheB.getByValue(valueAAA);
+ assertEquals(8, cache.getKeys().size());
+ }
+
+ @Test
+ public void testNullLookups() throws Exception
+ {
+ TestValue valueNull = null;
+ Pair entityPairNull = entityLookupCacheA.getOrCreateByValue(valueNull);
+ assertNotNull(entityPairNull);
+ assertTrue(database.containsKey(entityPairNull.getFirst()));
+ assertNull(database.get(entityPairNull.getFirst()));
+ assertEquals(2, cache.getKeys().size());
+
+ // Look it up again
+ Pair entityPairCheck = entityLookupCacheA.getOrCreateByValue(valueNull);
+ assertNotNull(entityPairNull);
+ assertTrue(database.containsKey(entityPairNull.getFirst()));
+ assertNull(database.get(entityPairNull.getFirst()));
+ assertEquals(entityPairNull, entityPairCheck);
+ }
+
+ @Test
+ public void testGetOrCreate() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+
+ Pair entityPairOneCheck = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOneCheck);
+ assertEquals(id, entityPairOneCheck.getFirst());
+ }
+
+ @Test
+ public void testCreateOrGet() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ Pair entityPairOne = entityLookupCacheA.createOrGetByValue(valueOne, controlDAO);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ // We cache both by value and by key, so we should have 2 entries
+ assertEquals(2, cache.getKeys().size());
+
+ Pair entityPairOneCheck = entityLookupCacheA.createOrGetByValue(valueOne, controlDAO);
+ assertNotNull(entityPairOneCheck);
+ assertEquals(id, entityPairOneCheck.getFirst());
+ }
+
+ @Test
+ public void testUpdate() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ TestValue valueTwo = new TestValue(getClass().getName() + "-TWO");
+ Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+
+ // Update
+ int updateCount = entityLookupCacheA.updateValue(id, valueTwo);
+ assertEquals("Update count was incorrect.", 1, updateCount);
+ assertEquals(valueTwo.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+ }
+
+ @Test
+ public void testDeleteByKey() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+
+ // Delete
+ int deleteCount = entityLookupCacheA.deleteByKey(id);
+ assertEquals("Delete count was incorrect.", 1, deleteCount);
+ assertNull(database.get(id));
+ assertEquals(0, cache.getKeys().size());
+ }
+
+ @Test
+ public void testDeleteByValue() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+
+ // Delete
+ int deleteCount = entityLookupCacheA.deleteByValue(valueOne);
+ assertEquals("Delete count was incorrect.", 1, deleteCount);
+ assertNull(database.get(id));
+ assertEquals(0, cache.getKeys().size());
+ }
+
+ @Test
+ public void testClear() throws Exception
+ {
+ TestValue valueOne = new TestValue(getClass().getName() + "-ONE");
+ Pair entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ assertEquals(valueOne.val, database.get(id));
+ assertEquals(2, cache.getKeys().size());
+
+ // Clear it
+ entityLookupCacheA.clear();
+ assertEquals(valueOne.val, database.get(id)); // Must still be in database
+ assertEquals(0, cache.getKeys().size()); // ... but cache must be empty
+ }
+
+ @Test
+ public void testGetCachedValue() throws Exception
+ {
+ // Create a new value
+ TestValue valueCached = new TestValue(getClass().getName() + "-CACHED");
+ Pair entityPairOne = entityLookupCacheA.createOrGetByValue(valueCached, controlDAO);
+ assertNotNull(entityPairOne);
+ Long id = entityPairOne.getFirst();
+ // We cache both by value and by key, so we should have 2 entries
+ assertEquals(2, cache.getKeys().size());
+
+ // Check the cache for the previously created value
+ Pair entityPairCacheCheck = entityLookupCacheA.getCachedEntityByValue(valueCached);
+ assertNotNull(entityPairCacheCheck);
+ assertEquals(id, entityPairCacheCheck.getFirst());
+
+ // Clear the cache and attempt to retrieve it again
+ entityLookupCacheA.clear();
+ entityPairCacheCheck = entityLookupCacheA.getCachedEntityByValue(valueCached);
+
+ // Since we are only retrieving from cache, the value should not be found
+ assertNull(entityPairCacheCheck);
+ }
+
+ /**
+ * Helper class to represent business object
+ */
+ private static class TestValue
+ {
+ private final String val;
+
+ private TestValue(String val)
+ {
+ this.val = val;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null || !(obj instanceof TestValue))
+ {
+ return false;
+ }
+ return val.equals(((TestValue) obj).val);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return val.hashCode();
+ }
+
+ }
+
+ public String getValueKey(Object value)
+ {
+ assertNotNull(value);
+ assertTrue(value instanceof TestValue);
+ String dbValue = ((TestValue) value).val;
+ return dbValue;
+ }
+
+ public Pair findByKey(Long key)
+ {
+ assertNotNull(key);
+
+ String dbValue = database.get(key);
+ if (dbValue == null)
+ {
+ return null;
+ }
+ // Make a value object
+ TestValue value = new TestValue(dbValue);
+ return new Pair(key, value);
+ }
+
+ public Pair findByValue(Object value)
+ {
+ assertTrue(value == null || value instanceof TestValue);
+ String dbValue = (value == null) ? null : ((TestValue) value).val;
+
+ for (Map.Entry entry : database.entrySet())
+ {
+ if (EqualsHelper.nullSafeEquals(entry.getValue(), dbValue))
+ {
+ return new Pair(entry.getKey(), entry.getValue());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Simulate creation of a new database entry
+ */
+ public Pair createValue(Object value)
+ {
+ assertTrue(value == null || value instanceof TestValue);
+ String dbValue = (value == null) ? null : ((TestValue) value).val;
+
+ // Kick out any duplicate values
+ if (database.containsValue(dbValue))
+ {
+ throw new DuplicateKeyException("Value is duplicated: " + value);
+ }
+
+ // Get the last key
+ Long lastKey = database.isEmpty() ? null : database.lastKey();
+ Long newKey = null;
+ if (lastKey == null)
+ {
+ newKey = Long.valueOf(1);
+ }
+ else
+ {
+ newKey = Long.valueOf(lastKey.longValue() + 1);
+ }
+ database.put(newKey, dbValue);
+ return new Pair(newKey, value);
+ }
+
+ public int updateValue(Long key, Object value)
+ {
+ assertNotNull(key);
+ assertTrue(value == null || value instanceof TestValue);
+
+ // Find it
+ Pair entityPair = findByKey(key);
+ if (entityPair == null)
+ {
+ return 0;
+ }
+ else
+ {
+ database.put(key, ((TestValue) value).val);
+ return 1;
+ }
+ }
+
+ public int deleteByKey(Long key)
+ {
+ assertNotNull(key);
+
+ if (database.containsKey(key))
+ {
+ database.remove(key);
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ public int deleteByValue(Object value)
+ {
+ assertTrue(value == null || value instanceof TestValue);
+
+ // Find it
+ Pair entityPair = findByValue(value);
+ if (entityPair == null)
+ {
+ return 0;
+ }
+ else
+ {
+ database.remove(entityPair.getFirst());
+ return 1;
+ }
+ }
+}