diff --git a/source/java/org/alfresco/repo/content/caching/test/SlowContentStore.java b/source/java/org/alfresco/repo/content/caching/test/SlowContentStore.java
new file mode 100644
index 0000000000..b1a4752b05
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/test/SlowContentStore.java
@@ -0,0 +1,229 @@
+/*
+ * 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.test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+
+import org.alfresco.repo.content.AbstractContentReader;
+import org.alfresco.repo.content.AbstractContentStore;
+import org.alfresco.repo.content.AbstractContentWriter;
+import org.alfresco.repo.content.filestore.FileContentStore;
+import org.alfresco.service.cmr.repository.ContentIOException;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentWriter;
+
+/**
+ * Package-private class - only for testing the CachingContentStore.
+ *
+ * @author Matt Ward
+ */
+class SlowContentStore extends AbstractContentStore
+{
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#isWriteSupported()
+ */
+ @Override
+ public boolean isWriteSupported()
+ {
+ return true;
+ }
+
+ /*
+ * @see org.alfresco.repo.content.ContentStore#getReader(java.lang.String)
+ */
+ @Override
+ public ContentReader getReader(String contentUrl)
+ {
+ return new SlowReader(contentUrl);
+ }
+
+ @Override
+ protected ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl)
+ {
+ if (newContentUrl == null)
+ newContentUrl = FileContentStore.createNewFileStoreUrl() + ".slow";
+
+ return new SlowWriter(newContentUrl, existingContentReader);
+ }
+
+
+ @Override
+ public boolean exists(String contentUrl)
+ {
+ return false;
+ }
+
+ private class SlowWriter extends AbstractContentWriter
+ {
+ protected SlowWriter(String contentUrl, ContentReader existingContentReader)
+ {
+ super(contentUrl, existingContentReader);
+ }
+
+ @Override
+ public long getSize()
+ {
+ return 20;
+ }
+
+ @Override
+ protected ContentReader createReader() throws ContentIOException
+ {
+ return new SlowReader(getContentUrl());
+ }
+
+ @Override
+ protected WritableByteChannel getDirectWritableChannel() throws ContentIOException
+ {
+ return new WritableByteChannel()
+ {
+ private boolean closed = false;
+ private int left = 200;
+
+ @Override
+ public boolean isOpen()
+ {
+ return !closed;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ closed = true;
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException
+ {
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException error)
+ {
+ throw new RuntimeException(error);
+ }
+
+ if (left > 0)
+ {
+ src.get();
+ left--;
+ return 1;
+ }
+ return 0;
+ }
+ };
+ }
+
+ }
+
+
+ private class SlowReader extends AbstractContentReader
+ {
+ protected SlowReader(String contentUrl)
+ {
+ super(contentUrl);
+ }
+
+ @Override
+ public boolean exists()
+ {
+ return true;
+ }
+
+ @Override
+ public long getLastModified()
+ {
+ return 0L;
+ }
+
+ @Override
+ public long getSize()
+ {
+ return 20;
+ }
+
+ @Override
+ protected ContentReader createReader() throws ContentIOException
+ {
+ return new SlowReader(getContentUrl());
+ }
+
+ @Override
+ protected ReadableByteChannel getDirectReadableChannel() throws ContentIOException
+ {
+ return new ReadableByteChannel()
+ {
+ private final byte[] content = "This is the content for my slow ReadableByteChannel".getBytes();
+ private int index = 0;
+ private boolean closed = false;
+
+ @Override
+ public boolean isOpen()
+ {
+ return !closed;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ closed = true;
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException
+ {
+ if (index < content.length)
+ {
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException error)
+ {
+ throw new RuntimeException(error);
+ }
+ dst.put(content[index++]);
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ };
+ }
+
+ }
+
+
+ public static void main(String[] args)
+ {
+ SlowContentStore scs = new SlowContentStore();
+
+ ContentReader reader = scs.getReader("store://something/bin");
+ String content = reader.getContentString();
+ System.out.println("Content: " + content);
+ }
+}
diff --git a/source/java/org/alfresco/repo/content/caching/test/SlowContentStoreTest.java b/source/java/org/alfresco/repo/content/caching/test/SlowContentStoreTest.java
new file mode 100644
index 0000000000..bcc3fae181
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/caching/test/SlowContentStoreTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.alfresco.repo.content.ContentContext;
+import org.alfresco.repo.content.caching.CachingContentStore;
+import org.alfresco.util.ApplicationContextHelper;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Test;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+/**
+ * Tests that exercise the CachingContentStore in conjunction with a backing store
+ * that runs deliberately slowly.
+ *
+ * @author Matt Ward
+ */
+public class SlowContentStoreTest
+{
+ private ClassPathXmlApplicationContext ctx;
+ private CachingContentStore cachingStore;
+ private static final Log logger = LogFactory.getLog(SlowContentStoreTest.class);
+
+
+ public SlowContentStoreTest()
+ {
+ String conf = "classpath:cachingstore/test-context.xml";
+ String slowconf = "classpath:cachingstore/test-slow-context.xml";
+ ctx = (ClassPathXmlApplicationContext) ApplicationContextHelper.getApplicationContext(new String[] { conf, slowconf });
+ cachingStore = (CachingContentStore) ctx.getBean("cachingContentStore");
+ cachingStore.setCacheOnInbound(false);
+ }
+
+
+ @Test
+ public void readsAreFasterFromCache()
+ {
+ // First read will hit the SLOW backing store
+ TimedStoreReader storeReader = new TimedStoreReader();
+ storeReader.execute();
+ assertTrue("First read should take a while", storeReader.timeTakenMillis() > 1000);
+ logger.info(String.format("First read took %ds", storeReader.timeTakenMillis()));
+ // The content came from the slow backing store...
+ assertEquals("This is the content for my slow ReadableByteChannel", storeReader.content);
+
+ // Subsequent reads will hit the cache
+ for (int i = 0; i < 5; i++)
+ {
+ storeReader = new TimedStoreReader();
+ storeReader.execute();
+ assertTrue("Subsequent reads should be fast", storeReader.timeTakenMillis() < 100);
+ logger.info(String.format("Cache read took %ds", storeReader.timeTakenMillis()));
+ // The content came from the slow backing store, but was cached...
+ assertEquals("This is the content for my slow ReadableByteChannel", storeReader.content);
+ }
+ }
+
+
+ @Test
+ public void writeThroughCacheResultsInFastReadFirstTime()
+ {
+ cachingStore.setCacheOnInbound(true);
+
+ // This content will be cached on the way in
+ cachingStore.getWriter(new ContentContext(null, "any-url")).
+ putContent("Content written from " + getClass().getSimpleName());
+
+ // First read will hit cache
+ TimedStoreReader storeReader = new TimedStoreReader();
+ storeReader.execute();
+ assertTrue("First read should be fast", storeReader.timeTakenMillis() < 100);
+ logger.info(String.format("First read took %ds", storeReader.timeTakenMillis()));
+ assertEquals("Content written from " + getClass().getSimpleName(), storeReader.content);
+
+ // Subsequent reads will also hit the cache
+ for (int i = 0; i < 5; i++)
+ {
+ storeReader = new TimedStoreReader();
+ storeReader.execute();
+ assertTrue("Subsequent reads should be fast", storeReader.timeTakenMillis() < 100);
+ logger.info(String.format("Cache read took %ds", storeReader.timeTakenMillis()));
+ // The original cached content, still cached...
+ assertEquals("Content written from " + getClass().getSimpleName(), storeReader.content);
+ }
+ }
+
+
+ private class TimedStoreReader extends TimedExecutor
+ {
+ String content;
+
+ @Override
+ protected void doExecute()
+ {
+ content = cachingStore.getReader("any-url").getContentString();
+ logger.info("Read content: " + content);
+ }
+ }
+
+ private abstract class TimedExecutor
+ {
+ private long start;
+ private long finish;
+
+ public void execute()
+ {
+ start = System.currentTimeMillis();
+ doExecute();
+ finish = System.currentTimeMillis();
+ }
+
+ public long timeTakenMillis()
+ {
+ return finish - start;
+ }
+
+ protected abstract void doExecute();
+ }
+}
diff --git a/source/test-resources/cachingstore/test-slow-context.xml b/source/test-resources/cachingstore/test-slow-context.xml
new file mode 100644
index 0000000000..512936c474
--- /dev/null
+++ b/source/test-resources/cachingstore/test-slow-context.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+