mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-31 17:39:05 +00:00
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
This commit is contained in:
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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 + "]");
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* CachingContentStore should only be used to wrap content stores that are significantly
|
||||
* slower that FileContentStore - otherwise performance may actually degrade from its use.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<ContentStreamListener> 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* @param contentUrl
|
||||
* @return true if the URL exists in the in-memory cache. It <em>may</em> 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);
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* Binary content data itself is stored on disk in temporary files managed
|
||||
* by Alfresco (see {@link org.alfresco.util.TempFileProvider}).
|
||||
* <p>
|
||||
* 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<String, String> memoryStore;
|
||||
|
||||
public ContentCacheImpl()
|
||||
{
|
||||
// TODO: Configuration to be moved out into Spring
|
||||
memoryStore = new EhCacheAdapter<String, String>();
|
||||
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:
|
||||
* <p>
|
||||
* store://2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin
|
||||
* <p>
|
||||
* will become:
|
||||
* <p>
|
||||
* 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("://", "/");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user