diff --git a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java index 06a46f4ae2..aca4d51520 100644 --- a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java +++ b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java @@ -27,8 +27,10 @@ package org.alfresco.repo.cache.lookup; import java.io.Serializable; import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.util.Pair; import org.alfresco.util.ParameterCheck; +import org.springframework.dao.ConcurrencyFailureException; /** * A cache for two-way lookups of database entities. These are characterized by having a unique @@ -117,6 +119,84 @@ public class EntityLookupCache 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.3 + */ + public static abstract class EntityLookupCallbackDAOAdaptor + implements EntityLookupCallbackDAO + { + /** + * 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"); + } } /** @@ -180,6 +260,17 @@ public class EntityLookupCache + * 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) { @@ -193,9 +284,9 @@ public class EntityLookupCache + * 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) { @@ -288,6 +398,14 @@ public class EntityLookupCachenull is allowed) + * @return Returns the key-value pair (new or existing and never null) + */ @SuppressWarnings("unchecked") public Pair getOrCreateByValue(V value) { @@ -342,9 +460,122 @@ public class EntityLookupCache + * 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 null) + * @return Returns the row update count. + */ @SuppressWarnings("unchecked") - public void remove(K key) + public int updateValue(K key, V value) { + // Handle missing cache + if (cache == null) + { + return entityLookup.updateValue(key, value); + } + + // Remove entries for the key (bidirectional removal removes the old value as well) + removeByKey(key); + + // Do the update + int updateCount = entityLookup.updateValue(key, value); + if (updateCount == 0) + { + // Nothing was done + return updateCount; + } + + // Get the value key. + VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value); + // Check if the value has a good key + if (valueKey == null) + { + // No good key, so no caching + return updateCount; + } + + // Cache the key and value + CacheRegionKey valueCacheKey = new CacheRegionKey(cacheRegion, valueKey); + cache.put(valueCacheKey, key); + cache.put( + new CacheRegionKey(cacheRegion, key), + (value == null ? VALUE_NULL : value)); + // Done + return updateCount; + } + + /** + * Delete the entity associated with the given key. + * The {@link EntityLookupCallbackDAO#deleteByKey(Serializable)} callback will be used if necessary. + *

+ * It is up to the client code to decide if a 0 return value indicates a concurrency violation + * or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised + * by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}. + * + * @param key the entity key, which may be valid or invalid (null not allowed) + * @return Returns the row deletion count + */ + public int deleteByKey(K key) + { + // Handle missing cache + if (cache == null) + { + return entityLookup.deleteByKey(key); + } + + // Remove entries for the key (bidirectional removal removes the old value as well) + removeByKey(key); + + // Do the delete + return entityLookup.deleteByKey(key); + } + + /** + * Delete the entity having the given value.. + * The {@link EntityLookupCallbackDAO#deleteByValue(Object)} callback will be used if necessary. + *

+ * It is up to the client code to decide if a 0 return value indicates a concurrency violation + * or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised + * by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}. + * + * @param key the entity value, which may be valid or invalid (null allowed) + * @return Returns the row deletion count + */ + public int deleteByValue(V value) + { + // Handle missing cache + if (cache == null) + { + return entityLookup.deleteByValue(value); + } + + // Remove entries for the value + removeByValue(value); + + // Do the delete + return entityLookup.deleteByValue(value); + } + + /** + * Cache-only operation: Remove all cache values associated with the given key. + */ + @SuppressWarnings("unchecked") + public void removeByKey(K key) + { + // Handle missing cache + if (cache == null) + { + return; + } + CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key); V value = (V) cache.get(keyCacheKey); if (value != null && !value.equals(VALUE_NOT_FOUND)) @@ -357,6 +588,39 @@ public class EntityLookupCachenull 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 + CacheRegionKey valueCacheKey = new CacheRegionKey(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); + } + /** * Key-wrapper used to separate cache regions, allowing a single cache to be used for different * purposes. diff --git a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java index 9d1bcd2c0b..8cfe9f67fd 100644 --- a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java +++ b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java @@ -163,7 +163,56 @@ public class EntityLookupCacheTest extends TestCase implements EntityLookupCallb assertTrue(database.containsKey(entityPairNull.getFirst())); assertNull(database.get(entityPairNull.getFirst())); assertEquals(entityPairNull, entityPairCheck); - } + } + + 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()); + } /** * Helper class to represent business object @@ -251,4 +300,54 @@ public class EntityLookupCacheTest extends TestCase implements EntityLookupCallb 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; + } + } }