From de52dd981ccb7eb1897de8df1d85c9c99e3f9ff1 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Thu, 23 Jul 2009 16:18:12 +0000 Subject: [PATCH] Entity cache for ID- and value-based lookups backed by DAO git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@15370 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../org/alfresco/repo/cache/MemoryCache.java | 78 +++++++ .../repo/cache/lookup/EntityLookupCache.java | 184 +++++++++++++++ .../cache/lookup/EntityLookupCacheTest.java | 218 ++++++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 source/java/org/alfresco/repo/cache/MemoryCache.java create mode 100644 source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java create mode 100644 source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java diff --git a/source/java/org/alfresco/repo/cache/MemoryCache.java b/source/java/org/alfresco/repo/cache/MemoryCache.java new file mode 100644 index 0000000000..e8f56b2bd4 --- /dev/null +++ b/source/java/org/alfresco/repo/cache/MemoryCache.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.cache; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * A cache backed by a simple HashMap. + *

+ * Note: This cache is not transaction- or thread-safe. Use it for single-threaded tests only. + * + * @author Derek Hulley + * @since 3.2 + */ +public class MemoryCache implements SimpleCache +{ + private Map map; + + public MemoryCache() + { + map = new HashMap(15); + } + + public boolean contains(K key) + { + return map.containsKey(key); + } + + public Collection getKeys() + { + return map.keySet(); + } + + public V get(K key) + { + return map.get(key); + } + + public void put(K key, V value) + { + map.put(key, value); + } + + public void remove(K key) + { + map.remove(key); + } + + public void clear() + { + map.clear(); + } +} diff --git a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java new file mode 100644 index 0000000000..2bf547ed4a --- /dev/null +++ b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCache.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.cache.lookup; + +import java.io.Serializable; + +import org.alfresco.repo.cache.SimpleCache; +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.3 + */ +public class EntityLookupCache +{ + private static final String NULL_VALUE = "@@NULL_VALUE@@"; + + private final SimpleCache cache; + private final EntityLookup entityLookup; + + @SuppressWarnings("unchecked") + public EntityLookupCache(SimpleCache cache, EntityLookup entityLookup) + { + this.cache = cache; + this.entityLookup = entityLookup; + } + + /** + * Interface to support lookups of the entities using keys and values. + */ + public static interface EntityLookup + { + /** + * Resolve the given value into a unique value key that can be used to find the entity's ID. + *

+ * Implementations will often return the value itself, provided that the value is both + * serializable and has a good equals and hashCode. + * + * @param value the full value being keyed + * @return Returns the business key representing the entity + */ + VK1 getValueKey(V1 value); + + /** + * Find an entity for a given key. + * + * @param key the key (ID) used to identify the entity + * @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. Where the + * behaviour is configurable, + * + * @param value the value (business object) used to identify the entity + * @return Return the entity or null if no entity matches the given value + */ + Pair findByValue(V1 value); + + Pair createValue(V1 value); + } + + @SuppressWarnings("unchecked") + Pair getByKey(K key) + { + // Look in the cache + V value = (V) cache.get(key); + if (value != null && value.equals(NULL_VALUE)) + { + // We checked before + return null; + } + else if (value != null) + { + return new Pair(key, value); + } + // Resolve it + Pair entityPair = entityLookup.findByKey(key); + if (entityPair == null) + { + // Cache nulls + cache.put(key, NULL_VALUE); + } + else + { + // Cache the value + cache.put(key, entityPair.getSecond()); + } + // Done + return entityPair; + } + + @SuppressWarnings("unchecked") + Pair getByValue(V value) + { + // Get the value key + VK valueKey = entityLookup.getValueKey(value); + // Look in the cache + K key = (K) cache.get(valueKey); + // Check if we have looked this up already + if (key != null && key.equals(NULL_VALUE)) + { + // We checked before + return null; + } + else if (key != null) + { + return new Pair(key, value); + } + // Resolve it + Pair entityPair = entityLookup.findByValue(value); + if (entityPair == null) + { + // Cache a null + cache.put(valueKey, NULL_VALUE); + } + else + { + // Cache the key + cache.put(valueKey, key); + } + // Done + return entityPair; + } + + @SuppressWarnings("unchecked") + Pair getOrCreateByValue(V value) + { + // Get the value key + VK valueKey = entityLookup.getValueKey(value); + // Look in the cache + K key = (K) cache.get(valueKey); + // Check if the value is already mapped to a key + if (key != null && !key.equals(NULL_VALUE)) + { + return new Pair(key, value); + } + // Resolve it + Pair entityPair = entityLookup.findByValue(value); + if (entityPair == null) + { + // Create it + entityPair = entityLookup.createValue(value); + } + key = entityPair.getFirst(); + // Cache the key and value + cache.put(valueKey, key); + cache.put(key, value); + // Done + return entityPair; + } +} diff --git a/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java new file mode 100644 index 0000000000..497444dba5 --- /dev/null +++ b/source/java/org/alfresco/repo/cache/lookup/EntityLookupCacheTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.cache.lookup; + +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.EntityLookup; +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.3 + */ +public class EntityLookupCacheTest extends TestCase implements EntityLookup +{ + private EntityLookupCache entityLookupCache; + private TreeMap database; + + @Override + protected void setUp() throws Exception + { + SimpleCache cache = new MemoryCache(); + entityLookupCache = new EntityLookupCache(cache, this); + database = new TreeMap(); + } + + public void testLookupsUsingIncorrectValue() throws Exception + { + try + { + // Keep the "database" empty + entityLookupCache.getByValue(this); + } + catch (AssertionFailedError e) + { + // Expected + } + } + + public void testLookupAgainstEmpty() throws Exception + { + TestValue value = new TestValue("AAA"); + Pair entityPair = entityLookupCache.getByValue(value); + assertNull(entityPair); + assertTrue(database.isEmpty()); + + // Now do lookup or create + entityPair = entityLookupCache.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 = entityLookupCache.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 = entityLookupCache.getByValue(value); + assertNotNull("Lookup after create should work", entityPair); + + // Look it up using the ID + entityPair = entityLookupCache.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 = entityLookupCache.getByValue(new TestValue("AAA")); + assertNotNull("Expected value to be found", entityPair); + assertEquals("ID is incorrect", new Long(1), entityPair.getFirst()); + + // Look up by ID + entityPair = entityLookupCache.getByKey(new Long(2)); + assertNotNull("Expected value to be found", entityPair); + + // Do lookup or create + entityPair = entityLookupCache.getByValue(new TestValue("CCC")); + assertNotNull("Expected value to be found", entityPair); + assertEquals("ID is incorrect", new Long(3), entityPair.getFirst()); + } + + /** + * 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(); + } + + } + + @Override + public String getValueKey(Object value) + { + assertNotNull(value); + assertTrue(value instanceof TestValue); + String dbValue = ((TestValue)value).val; + return dbValue; + } + + /** + * Simulate creation of a new database entry + */ + @Override + public Pair createValue(Object value) + { + assertNotNull(value); + assertTrue(value instanceof TestValue); + String dbValue = ((TestValue)value).val; + + // Get the last key + Long lastKey = database.isEmpty() ? null : database.lastKey(); + Long newKey = null; + if (lastKey == null) + { + newKey = new Long(1); + } + else + { + newKey = new Long(lastKey.longValue() + 1); + } + database.put(newKey, dbValue); + return new Pair(newKey, value); + } + + @Override + 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); + } + + @Override + public Pair findByValue(Object value) + { + assertNotNull(value); + assertTrue(value instanceof TestValue); + String dbValue = ((TestValue)value).val; + + for (Map.Entry entry : database.entrySet()) + { + if (entry.getValue().equals(dbValue)) + { + return new Pair(entry.getKey(), entry.getValue()); + } + } + return null; + } +}