From 065229b36bf164283db6d8f2a9a3de1a7bfe9c60 Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Wed, 10 Aug 2011 14:27:29 +0000 Subject: [PATCH] ALF-9613: caching content store http://issues.alfresco.com/jira/browse/ALF-9613 git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@29662 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../content/caching/CacheMissException.java | 38 +++ .../content/caching/CachingContentStore.java | 222 +++++++++++++++ .../CachingContentStoreSpringTest.java | 123 ++++++++ .../caching/CachingContentStoreTest.java | 269 ++++++++++++++++++ .../repo/content/caching/ContentCache.java | 83 ++++++ .../content/caching/ContentCacheImpl.java | 189 ++++++++++++ 6 files changed, 924 insertions(+) create mode 100644 source/java/org/alfresco/repo/content/caching/CacheMissException.java create mode 100644 source/java/org/alfresco/repo/content/caching/CachingContentStore.java create mode 100644 source/java/org/alfresco/repo/content/caching/CachingContentStoreSpringTest.java create mode 100644 source/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java create mode 100644 source/java/org/alfresco/repo/content/caching/ContentCache.java create mode 100644 source/java/org/alfresco/repo/content/caching/ContentCacheImpl.java 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("://", "/"); + } +}