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;
+ }
+}