diff --git a/source/java/org/alfresco/repo/content/caching/CacheMissException.java b/source/java/org/alfresco/repo/content/caching/CacheMissException.java
new file mode 100644
index 0000000000..7a77e4b860
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/CacheMissException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+/**
+ * CacheMissException will be thrown if an attempt is made to read
+ * content from the ContentCache when it is not in the cache.
+ *
+ * @author Matt Ward
+ */
+public class CacheMissException extends RuntimeException
+{
+ private static final long serialVersionUID = -410818899455752655L;
+
+ /**
+ * @param contentUrl URL of content that was attempted to be retrieved.
+ */
+ public CacheMissException(String contentUrl)
+ {
+ super("Content not found in cache [URL=" + contentUrl + "]");
+ }
+}
diff --git a/source/java/org/alfresco/repo/content/caching/CachingContentStore.java b/source/java/org/alfresco/repo/content/caching/CachingContentStore.java
new file mode 100644
index 0000000000..18b83c7cd5
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/CachingContentStore.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+import java.util.Date;
+
+import org.alfresco.repo.content.ContentContext;
+import org.alfresco.repo.content.ContentStore;
+import org.alfresco.service.cmr.repository.ContentIOException;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentStreamListener;
+import org.alfresco.service.cmr.repository.ContentWriter;
+
+/**
+ * Implementation of ContentStore that wraps any other ContentStore (the backing store)
+ * transparently providing caching of content in that backing store.
+ *
+ * CachingContentStore should only be used to wrap content stores that are significantly
+ * slower that FileContentStore - otherwise performance may actually degrade from its use.
+ *
+ * It is important that cacheOnInbound is set to true for exceptionally slow backing stores,
+ * e.g. {@link org.alfresco.enterprise.repo.content.xam.XAMContentStore}
+ *
+ * @author Matt Ward
+ */
+public class CachingContentStore implements ContentStore
+{
+ private final ContentStore backingStore;
+ private final ContentCache cache;
+ private final boolean cacheOnInbound;
+
+
+ public CachingContentStore(ContentStore backingStore, ContentCache cache, boolean cacheOnInbound)
+ {
+ this.backingStore = backingStore;
+ this.cache = cache;
+ this.cacheOnInbound = cacheOnInbound;
+ }
+
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#isContentUrlSupported(java.lang.String)
+ */
+ @Override
+ public boolean isContentUrlSupported(String contentUrl)
+ {
+ return backingStore.isContentUrlSupported(contentUrl);
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#isWriteSupported()
+ */
+ @Override
+ public boolean isWriteSupported()
+ {
+ return backingStore.isWriteSupported();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getTotalSize()
+ */
+ @Override
+ public long getTotalSize()
+ {
+ return backingStore.getTotalSize();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getSpaceUsed()
+ */
+ @Override
+ public long getSpaceUsed()
+ {
+ return backingStore.getSpaceUsed();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getSpaceFree()
+ */
+ @Override
+ public long getSpaceFree()
+ {
+ return backingStore.getSpaceFree();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getSpaceTotal()
+ */
+ @Override
+ public long getSpaceTotal()
+ {
+ return backingStore.getSpaceTotal();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getRootLocation()
+ */
+ @Override
+ public String getRootLocation()
+ {
+ return backingStore.getRootLocation();
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#exists(java.lang.String)
+ */
+ @Override
+ public boolean exists(String contentUrl)
+ {
+ return backingStore.exists(contentUrl);
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getReader(java.lang.String)
+ */
+ @Override
+ public ContentReader getReader(String contentUrl)
+ {
+ if (!cache.contains(contentUrl))
+ {
+ ContentReader bsReader = backingStore.getReader(contentUrl);
+ if (!cache.put(contentUrl, bsReader))
+ {
+ // Content wasn't put into cache successfully.
+ return bsReader.getReader();
+ }
+ }
+
+ // TODO: what if, in the meantime this item has been deleted from the disk cache?
+ return cache.getReader(contentUrl);
+ }
+
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getWriter(org.alfresco.repo.content.ContentContext)
+ */
+ @Override
+ public ContentWriter getWriter(final ContentContext context)
+ {
+ if (cacheOnInbound)
+ {
+ final ContentWriter bsWriter = backingStore.getWriter(context);
+
+ // write to cache
+ final ContentWriter writer = cache.getWriter(bsWriter.getContentUrl());
+
+ writer.addListener(new ContentStreamListener()
+ {
+ @Override
+ public void contentStreamClosed() throws ContentIOException
+ {
+ // copy from the cache to the backing store
+ bsWriter.putContent(writer.getReader());
+ }
+ });
+
+ return writer;
+ }
+ else
+ {
+ // No need to invalidate the cache for this content URL, since a content URL
+ // is only ever written to once.
+ return backingStore.getWriter(context);
+ }
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getWriter(org.alfresco.service.cmr.repository.ContentReader, java.lang.String)
+ */
+ @Override
+ public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl)
+ {
+ return backingStore.getWriter(existingContentReader, newContentUrl);
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getUrls(org.alfresco.repo.content.ContentStore.ContentUrlHandler)
+ */
+ @Override
+ public void getUrls(ContentUrlHandler handler) throws ContentIOException
+ {
+ backingStore.getUrls(handler);
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getUrls(java.util.Date, java.util.Date, org.alfresco.repo.content.ContentStore.ContentUrlHandler)
+ */
+ @Override
+ public void getUrls(Date createdAfter, Date createdBefore, ContentUrlHandler handler)
+ throws ContentIOException
+ {
+ backingStore.getUrls(createdAfter, createdBefore, handler);
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#delete(java.lang.String)
+ */
+ @Override
+ public boolean delete(String contentUrl)
+ {
+ if (cache.contains(contentUrl))
+ cache.remove(contentUrl);
+
+ return backingStore.delete(contentUrl);
+ }
+}
diff --git a/source/java/org/alfresco/repo/content/caching/CachingContentStoreSpringTest.java b/source/java/org/alfresco/repo/content/caching/CachingContentStoreSpringTest.java
new file mode 100644
index 0000000000..38e1ba7fef
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/CachingContentStoreSpringTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+import java.io.File;
+
+import org.alfresco.repo.content.AbstractWritableContentStoreTest;
+import org.alfresco.repo.content.ContentContext;
+import org.alfresco.repo.content.ContentStore;
+import org.alfresco.repo.content.filestore.FileContentStore;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.util.TempFileProvider;
+
+/**
+ * Tests for the CachingContentStore that use a full spring context.
+ *
+ * @author Matt Ward
+ */
+public class CachingContentStoreSpringTest extends AbstractWritableContentStoreTest
+{
+ private CachingContentStore store;
+ private FileContentStore backingStore;
+ private ContentCache cache;
+
+ @Override
+ public void setUp() throws Exception
+ {
+ super.setUp();
+
+ File tempDir = TempFileProvider.getTempDir();
+
+ backingStore = new FileContentStore(ctx,
+ tempDir.getAbsolutePath() +
+ File.separatorChar +
+ getName());
+
+ cache = new ContentCacheImpl();
+ store = new CachingContentStore(backingStore, cache, false);
+ }
+
+
+ public void testStoreWillReadFromCacheWhenAvailable()
+ {
+ final String content = "Content for " + getName() + " test.";
+
+ // Write some content to the backing store.
+ ContentWriter writer = backingStore.getWriter(ContentContext.NULL_CONTEXT);
+ writer.putContent(content);
+ final String contentUrl = writer.getContentUrl();
+
+ // Read content using the CachingContentStore - will cause content to be cached.
+ String retrievedContent = store.getReader(contentUrl).getContentString();
+ assertEquals(content, retrievedContent);
+
+ // Remove the original content from the backing store.
+ backingStore.delete(contentUrl);
+ assertFalse("Original content should have been deleted", backingStore.exists(contentUrl));
+
+ // The cached version is still available.
+ String contentAfterDelete = store.getReader(contentUrl).getContentString();
+ assertEquals(content, contentAfterDelete);
+ }
+
+
+ public void testCacheOnInbound()
+ {
+ store = new CachingContentStore(backingStore, cache, true);
+ final String content = "Content for " + getName() + " test.";
+ final String contentUrl = FileContentStore.createNewFileStoreUrl();
+
+ assertFalse("Content shouldn't be cached yet", cache.contains(contentUrl));
+
+ // Write some content using the caching store
+ ContentWriter writer = store.getWriter(new ContentContext(null, contentUrl));
+ writer.putContent(content);
+
+ assertTrue("Cache should contain content after write", cache.contains(contentUrl));
+ // Check DIRECTLY with the cache, since a getReader() from the CachingContentStore would result
+ // in caching, but we're checking that caching was caused by the write operation.
+ String retrievedContent = cache.getReader(contentUrl).getContentString();
+ assertEquals(content, retrievedContent);
+
+ // The content should have been written through to the backing store.
+ String fromBackingStore = backingStore.getReader(contentUrl).getContentString();
+ assertEquals("Content should be in backing store", content, fromBackingStore);
+
+ // Remove the original content from the backing store.
+ backingStore.delete(contentUrl);
+ assertFalse("Original content should have been deleted", backingStore.exists(contentUrl));
+
+ // The cached version is still available
+ String contentAfterDelete = store.getReader(contentUrl).getContentString();
+ assertEquals(content, contentAfterDelete);
+ }
+
+
+
+ /*
+ * @see org.alfresco.repo.content.AbstractReadOnlyContentStoreTest#getStore()
+ */
+ @Override
+ protected ContentStore getStore()
+ {
+ return store;
+ }
+}
+
diff --git a/source/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java b/source/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java
new file mode 100644
index 0000000000..63cdd594ba
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Date;
+
+import org.alfresco.repo.content.ContentContext;
+import org.alfresco.repo.content.ContentStore;
+import org.alfresco.repo.content.ContentStore.ContentUrlHandler;
+import org.alfresco.service.cmr.repository.ContentIOException;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentStreamListener;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/**
+ * Tests for the CachingContentStore class. Tests use mock backing store and cache.
+ *
+ * @author Matt Ward
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CachingContentStoreTest
+{
+ private CachingContentStore cachingStore;
+
+ @Mock
+ private ContentStore backingStore;
+
+ @Mock
+ private ContentCache cache;
+
+
+ @Before
+ public void setUp() throws Exception
+ {
+ cachingStore = new CachingContentStore(backingStore, cache, false);
+ }
+
+
+ @Test
+ public void getReaderForItemInCache()
+ {
+ ContentReader cachedContentReader = mock(ContentReader.class);
+ when(cache.contains("url")).thenReturn(true);
+ when(cache.getReader("url")).thenReturn(cachedContentReader);
+
+ ContentReader returnedReader = cachingStore.getReader("url");
+
+ assertSame(returnedReader, cachedContentReader);
+ verify(backingStore, never()).getReader(anyString());
+ }
+
+
+ @Test
+ public void getReadForItemMissingFromCache()
+ {
+ ContentReader sourceContent = mock(ContentReader.class);
+ when(cache.contains("url")).thenReturn(false);
+ when(backingStore.getReader("url")).thenReturn(sourceContent);
+
+ cachingStore.getReader("url");
+
+ verify(backingStore).getReader("url");
+ verify(cache).put("url", sourceContent);
+ }
+
+
+ @Test
+ public void getWriterWhenNotCacheOnInbound()
+ {
+ ContentContext ctx = ContentContext.NULL_CONTEXT;
+
+ cachingStore.getWriter(ctx);
+
+ verify(backingStore).getWriter(ctx);
+ }
+
+
+ @Test
+ public void getWriterWhenCacheOnInbound() throws ContentIOException, IOException
+ {
+ cachingStore = new CachingContentStore(backingStore, cache, true);
+ ContentContext ctx = ContentContext.NULL_CONTEXT;
+ ContentWriter bsWriter = mock(ContentWriter.class);
+ when(backingStore.getWriter(ctx)).thenReturn(bsWriter);
+ when(bsWriter.getContentUrl()).thenReturn("url");
+ ContentWriter cacheWriter = mock(ContentWriter.class);
+ when(cache.getWriter("url")).thenReturn(cacheWriter);
+ ContentReader readerFromCacheWriter = mock(ContentReader.class);
+ when(cacheWriter.getReader()).thenReturn(readerFromCacheWriter);
+
+
+ cachingStore.getWriter(ctx);
+
+ // Check that a listener was attached to cacheWriter with the correct behaviour
+ ArgumentCaptor arg = ArgumentCaptor.forClass(ContentStreamListener.class);
+ verify(cacheWriter).addListener(arg.capture());
+ // Simulate a stream close
+ arg.getValue().contentStreamClosed();
+ // Check behaviour of the listener
+ verify(bsWriter).putContent(readerFromCacheWriter);
+
+ verify(backingStore).getWriter(ctx);
+ }
+
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Tests for delegated methods follow...
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ @Test
+ public void delegatedIsContentUrlSupported()
+ {
+ when(backingStore.isContentUrlSupported("url")).thenReturn(true);
+ assertTrue(cachingStore.isContentUrlSupported("url"));
+
+ when(backingStore.isContentUrlSupported("url")).thenReturn(false);
+ assertFalse(cachingStore.isContentUrlSupported("url"));
+ }
+
+
+ @Test
+ public void delegatedIsWriteSupported()
+ {
+ when(backingStore.isWriteSupported()).thenReturn(true);
+ assertTrue(cachingStore.isWriteSupported());
+
+ when(backingStore.isWriteSupported()).thenReturn(false);
+ assertFalse(cachingStore.isWriteSupported());
+ }
+
+
+ @Test
+ public void delegatedGetTotalSize()
+ {
+ when(backingStore.getTotalSize()).thenReturn(234L);
+ assertEquals(234L, cachingStore.getTotalSize());
+ }
+
+
+ @Test
+ public void delegatedGetSpaceUsed()
+ {
+ when(backingStore.getSpaceUsed()).thenReturn(453L);
+ assertEquals(453L, cachingStore.getSpaceUsed());
+ }
+
+
+ @Test
+ public void delegatedGetSpaceFree()
+ {
+ when(backingStore.getSpaceFree()).thenReturn(124L);
+ assertEquals(124L, cachingStore.getSpaceFree());
+ }
+
+
+ @Test
+ public void delegatedGetSpaceTotal()
+ {
+ when(backingStore.getSpaceTotal()).thenReturn(4234L);
+ assertEquals(4234L, cachingStore.getSpaceTotal());
+ }
+
+
+ @Test
+ public void delegatedGetRootLocation()
+ {
+ when(backingStore.getRootLocation()).thenReturn("/random/root/dir");
+ assertEquals("/random/root/dir", cachingStore.getRootLocation());
+ }
+
+
+ @Test
+ public void delegatedExists()
+ {
+ when(backingStore.exists("url")).thenReturn(true);
+ assertTrue(cachingStore.exists("url"));
+
+ when(backingStore.exists("url")).thenReturn(false);
+ assertFalse(cachingStore.exists("url"));
+ }
+
+
+ @Test
+ public void delegatedGetUrls1()
+ {
+ ContentUrlHandler handler = createDummyUrlHandler();
+
+ cachingStore.getUrls(handler);
+
+ verify(backingStore).getUrls(handler);
+ }
+
+
+ @Test
+ public void delegatedGetUrls2()
+ {
+ ContentUrlHandler handler = createDummyUrlHandler();
+ Date after = new Date(123L);
+ Date before = new Date(456L);
+
+ cachingStore.getUrls(after, before, handler);
+
+ verify(backingStore).getUrls(after, before, handler);
+ }
+
+
+ @Test
+ public void delegatedDelete()
+ {
+ when(backingStore.delete("url")).thenReturn(true);
+ assertTrue(cachingStore.delete("url"));
+
+ when(backingStore.delete("url")).thenReturn(false);
+ assertFalse(cachingStore.delete("url"));
+ }
+
+
+ /**
+ * Create a stub handler - just so we can check it has been passed around correctly.
+ *
+ * @return ContentUrlHandler
+ */
+ private ContentUrlHandler createDummyUrlHandler()
+ {
+ ContentUrlHandler handler = new ContentUrlHandler()
+ {
+ @Override
+ public void handle(String contentUrl)
+ {
+ }
+ };
+ return handler;
+ }
+
+}
diff --git a/source/java/org/alfresco/repo/content/caching/ContentCache.java b/source/java/org/alfresco/repo/content/caching/ContentCache.java
new file mode 100644
index 0000000000..3cb9c843f2
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/ContentCache.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentWriter;
+
+/**
+ * A cache designed to operate on content and split between memory and disk.
+ * The binary content data itself is stored on disk but the references to
+ * those files are stored in memory.
+ *
+ * @author Matt Ward
+ */
+public interface ContentCache
+{
+ /**
+ * Check to see if the content - specified by URL - exists in the cache.
+ *
+ * Note that just because the in-memory cache has a record of the content item having been placed
+ * into the cache, it does not mean that the disk item is guaranteed to be there. The temp file
+ * clean-up process, for example, may have removed it.
+ *
+ * @param contentUrl
+ * @return true if the URL exists in the in-memory cache. It may therefore be cached on disk.
+ */
+ boolean contains(String contentUrl);
+
+ /**
+ * Retrieve a ContentReader for the cached content specified by URL.
+ *
+ * @param contentUrl
+ * @return ContentReader
+ * @throws org.alfresco.repo.content.caching.CacheMissException
+ * If the cache does not contain the specified content.
+ */
+ ContentReader getReader(String contentUrl);
+
+ /**
+ * Put an item into cache - this will populate both a disk file (with content) and
+ * the in-memory lookup table (with the URL and cache file location).
+ *
+ * Empty content will NOT be cached - in which case false is returned.
+ *
+ * @param contentUrl
+ * @param reader
+ * @return true if the content was cached, false otherwise.
+ */
+ boolean put(String contentUrl, ContentReader reader);
+
+ /**
+ * Remove a cached item from the in-memory lookup table. Implementation should not remove
+ * the actual cached content (file) - this should be left to the clean-up process.
+ *
+ * @param contentUrl
+ */
+ void remove(String contentUrl);
+
+ /**
+ * Retrieve a ContentWriter to write content to a cache file. Upon closing the stream
+ * a listener will add the new content file to the in-memory lookup table.
+ *
+ * @param context
+ * @return ContentWriter
+ */
+ ContentWriter getWriter(String url);
+}
diff --git a/source/java/org/alfresco/repo/content/caching/ContentCacheImpl.java b/source/java/org/alfresco/repo/content/caching/ContentCacheImpl.java
new file mode 100644
index 0000000000..62677004d7
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/ContentCacheImpl.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.content.caching;
+
+import java.io.File;
+
+import net.sf.ehcache.CacheManager;
+
+import org.alfresco.repo.cache.EhCacheAdapter;
+import org.alfresco.repo.content.filestore.FileContentReader;
+import org.alfresco.repo.content.filestore.FileContentWriter;
+import org.alfresco.service.cmr.repository.ContentIOException;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentStreamListener;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.util.TempFileProvider;
+
+/**
+ * The one and only implementation of the ContentCache class.
+ *
+ * Binary content data itself is stored on disk in temporary files managed
+ * by Alfresco (see {@link org.alfresco.util.TempFileProvider}).
+ *
+ * The in-memory lookup table is provided by Ehcache.
+ *
+ * @author Matt Ward
+ */
+public class ContentCacheImpl implements ContentCache
+{
+ private static final String CACHE_DIR = "caching_cs";
+ private static final String TMP_FILE_EXTENSION = ".tmp";
+ private static final String EHCACHE_NAME = "contentStoreCache";
+ private static final long T24_HOURS = 86400;
+ private final File cacheRoot = TempFileProvider.getLongLifeTempDir(CACHE_DIR);
+ private EhCacheAdapter memoryStore;
+
+ public ContentCacheImpl()
+ {
+ // TODO: Configuration to be moved out into Spring
+ memoryStore = new EhCacheAdapter();
+ configureMemoryStore();
+ }
+
+ private void configureMemoryStore()
+ {
+ CacheManager manager = CacheManager.getInstance();
+
+ // Create the cache if it hasn't already been created.
+ if (!manager.cacheExists(EHCACHE_NAME))
+ {
+ net.sf.ehcache.Cache memoryOnlyCache =
+ new net.sf.ehcache.Cache(EHCACHE_NAME, 10000, false, false, T24_HOURS, T24_HOURS);
+
+ manager.addCache(memoryOnlyCache);
+ }
+
+ memoryStore.setCache(manager.getCache(EHCACHE_NAME));
+ }
+
+
+ @Override
+ public boolean contains(String contentUrl)
+ {
+ return memoryStore.contains(contentUrl);
+ }
+
+
+
+ @Override
+ public ContentReader getReader(String contentUrl)
+ {
+ if (memoryStore.contains(contentUrl))
+ {
+ String path = memoryStore.get(contentUrl);
+ return new FileContentReader(new File(path), contentUrl);
+ }
+
+ throw new CacheMissException(contentUrl);
+ }
+
+
+
+ @Override
+ public boolean put(String contentUrl, ContentReader source)
+ {
+ File cacheFile = createCacheFile(contentUrl);
+
+ // Copy the content from the source into a cache file
+ if (source.getSize() > 0L)
+ {
+ source.getContent(cacheFile);
+ // Add a record of the cached file to the in-memory cache.
+ memoryStore.put(contentUrl, cacheFile.getAbsolutePath());
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Create a File object and makes any intermediate directories in the path.
+ *
+ * @param contentUrl
+ * @return File
+ */
+ private File createCacheFile(String contentUrl)
+ {
+ File path = new File(cacheRoot, pathFromUrl(contentUrl));
+ File parentDir = path.getParentFile();
+
+ parentDir.mkdirs();
+
+ File cacheFile = TempFileProvider.createTempFile(path.getName(), TMP_FILE_EXTENSION, parentDir);
+ return cacheFile;
+ }
+
+
+
+
+ /*
+ * @see org.alfresco.repo.content.caching.ContentCache#remove(java.lang.String)
+ */
+ @Override
+ public void remove(String contentUrl)
+ {
+ // Remove from the in-memory cache, but not from disk. Let the clean-up process do this asynchronously.
+ memoryStore.remove(contentUrl);
+ }
+
+
+ /*
+ * @see org.alfresco.repo.content.caching.ContentCache#getWriter(org.alfresco.repo.content.ContentContext)
+ */
+ @Override
+ public ContentWriter getWriter(final String url)
+ {
+ // Get a writer to a cache file.
+ final File cacheFile = createCacheFile(url);
+ ContentWriter writer = new FileContentWriter(cacheFile);
+
+ // Attach a listener to populate the in-memory store when done writing.
+ writer.addListener(new ContentStreamListener()
+ {
+ @Override
+ public void contentStreamClosed() throws ContentIOException
+ {
+ memoryStore.put(url, cacheFile.getAbsolutePath());
+ }
+ });
+
+ return writer;
+ }
+
+
+ /**
+ * Converts a content URL to a relative path name where the protocol will
+ * be the name of a subdirectory. For example:
+ *
+ * store://2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin
+ *
+ * will become:
+ *
+ * store/2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin
+ *
+ * @param contentUrl
+ * @return String representation of relative path to file.
+ */
+ private String pathFromUrl(String contentUrl)
+ {
+ return contentUrl.replaceFirst("://", "/");
+ }
+}