ACS-406: S3 Connector: support for content direct access urls throws … (#1080)

-  added implementation for CachingContentStore and AggregatingContentStore
  -  added tests
This commit is contained in:
Cristian Turlica
2020-07-07 12:31:59 +03:00
committed by GitHub
parent 8885b75e0b
commit b16e62761b
4 changed files with 1347 additions and 1045 deletions

View File

@@ -23,8 +23,9 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #L%
*/ */
package org.alfresco.repo.content.caching; package org.alfresco.repo.content.caching;
import java.util.Date;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
@@ -39,440 +40,451 @@ import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.DirectAccessUrl;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.annotation.Required; import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationEventPublisherAware;
/** /**
* Implementation of ContentStore that wraps any other ContentStore (the backing store) * Implementation of ContentStore that wraps any other ContentStore (the backing store)
* transparently providing caching of content in that backing store. * transparently providing caching of content in that backing store.
* <p> * <p>
* CachingContentStore should only be used to wrap content stores that are significantly * CachingContentStore should only be used to wrap content stores that are significantly
* slower that FileContentStore - otherwise performance may actually degrade from its use. * slower that FileContentStore - otherwise performance may actually degrade from its use.
* <p> * <p>
* It is important that cacheOnInbound is set to true for exceptionally slow backing stores. * It is important that cacheOnInbound is set to true for exceptionally slow backing stores.
* <p> * <p>
* This store handles the {@link FileContentStore#SPOOF_PROTOCOL} and can be used to wrap stores * This store handles the {@link FileContentStore#SPOOF_PROTOCOL} and can be used to wrap stores
* that do not handle the protocol out of the box e.g. the S3 connector's store. * that do not handle the protocol out of the box e.g. the S3 connector's store.
* *
* @author Matt Ward * @author Matt Ward
*/ */
public class CachingContentStore implements ContentStore, ApplicationEventPublisherAware, BeanNameAware public class CachingContentStore implements ContentStore, ApplicationEventPublisherAware, BeanNameAware
{ {
private final static Log log = LogFactory.getLog(CachingContentStore.class); private final static Log log = LogFactory.getLog(CachingContentStore.class);
// NUM_LOCKS absolutely must be a power of 2 for the use of locks to be evenly balanced // NUM_LOCKS absolutely must be a power of 2 for the use of locks to be evenly balanced
private final static int numLocks = 256; private final static int numLocks = 256;
private final static ReentrantReadWriteLock[] locks; private final static ReentrantReadWriteLock[] locks;
private ContentStore backingStore; private ContentStore backingStore;
private ContentCache cache; private ContentCache cache;
private QuotaManagerStrategy quota = new UnlimitedQuotaStrategy(); private QuotaManagerStrategy quota = new UnlimitedQuotaStrategy();
private boolean cacheOnInbound; private boolean cacheOnInbound;
private int maxCacheTries = 2; private int maxCacheTries = 2;
private ApplicationEventPublisher eventPublisher; private ApplicationEventPublisher eventPublisher;
private String beanName; private String beanName;
static static
{ {
locks = new ReentrantReadWriteLock[numLocks]; locks = new ReentrantReadWriteLock[numLocks];
for (int i = 0; i < numLocks; i++) for (int i = 0; i < numLocks; i++)
{ {
locks[i] = new ReentrantReadWriteLock(); locks[i] = new ReentrantReadWriteLock();
} }
} }
public CachingContentStore() public CachingContentStore()
{ {
} }
public CachingContentStore(ContentStore backingStore, ContentCache cache, boolean cacheOnInbound) public CachingContentStore(ContentStore backingStore, ContentCache cache, boolean cacheOnInbound)
{ {
this.backingStore = backingStore; this.backingStore = backingStore;
this.cache = cache; this.cache = cache;
this.cacheOnInbound = cacheOnInbound; this.cacheOnInbound = cacheOnInbound;
} }
/** /**
* Initialisation method, should be called once the CachingContentStore has been constructed. * Initialisation method, should be called once the CachingContentStore has been constructed.
*/ */
public void init() public void init()
{ {
eventPublisher.publishEvent(new CachingContentStoreCreatedEvent(this)); eventPublisher.publishEvent(new CachingContentStoreCreatedEvent(this));
} }
@Override @Override
public boolean isContentUrlSupported(String contentUrl) public boolean isContentUrlSupported(String contentUrl)
{ {
return backingStore.isContentUrlSupported(contentUrl); return backingStore.isContentUrlSupported(contentUrl);
} }
@Override @Override
public boolean isWriteSupported() public boolean isWriteSupported()
{ {
return backingStore.isWriteSupported(); return backingStore.isWriteSupported();
} }
@Override @Override
public long getSpaceFree() public long getSpaceFree()
{ {
return backingStore.getSpaceFree(); return backingStore.getSpaceFree();
} }
@Override @Override
public long getSpaceTotal() public long getSpaceTotal()
{ {
return backingStore.getSpaceTotal(); return backingStore.getSpaceTotal();
} }
@Override @Override
public String getRootLocation() public String getRootLocation()
{ {
return backingStore.getRootLocation(); return backingStore.getRootLocation();
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* <p> * <p>
* For {@link #SPOOF_PROTOCOL spoofed} URLs, the URL always exists. * For {@link #SPOOF_PROTOCOL spoofed} URLs, the URL always exists.
*/ */
@Override @Override
public boolean exists(String contentUrl) public boolean exists(String contentUrl)
{ {
if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL))
{ {
return true; return true;
} }
else else
{ {
return backingStore.exists(contentUrl); return backingStore.exists(contentUrl);
} }
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* <p> * <p>
* This store handles the {@link FileContentStore#SPOOF_PROTOCOL} so that underlying stores do not need * This store handles the {@link FileContentStore#SPOOF_PROTOCOL} so that underlying stores do not need
* to implement anything <a href="https://issues.alfresco.com/jira/browse/ACE-4516">related to spoofing</a>. * to implement anything <a href="https://issues.alfresco.com/jira/browse/ACE-4516">related to spoofing</a>.
*/ */
@Override @Override
public ContentReader getReader(String contentUrl) public ContentReader getReader(String contentUrl)
{ {
// Handle the spoofed URL // Handle the spoofed URL
if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL))
{ {
return new SpoofedTextContentReader(contentUrl); return new SpoofedTextContentReader(contentUrl);
} }
// Use pool of locks - which one is determined by a hash of the URL. // Use pool of locks - which one is determined by a hash of the URL.
// This will stop the content from being read/cached multiple times from the backing store // This will stop the content from being read/cached multiple times from the backing store
// when it should only be read once - cached versions should be returned after that. // when it should only be read once - cached versions should be returned after that.
ReadLock readLock = readWriteLock(contentUrl).readLock(); ReadLock readLock = readWriteLock(contentUrl).readLock();
readLock.lock(); readLock.lock();
try try
{ {
if (cache.contains(contentUrl)) if (cache.contains(contentUrl))
{ {
return cache.getReader(contentUrl); return cache.getReader(contentUrl);
} }
} }
catch(CacheMissException e) catch(CacheMissException e)
{ {
// Fall through to cacheAndRead(url); // Fall through to cacheAndRead(url);
} }
finally finally
{ {
readLock.unlock(); readLock.unlock();
} }
return cacheAndRead(contentUrl); return cacheAndRead(contentUrl);
} }
private ContentReader cacheAndRead(String url) private ContentReader cacheAndRead(String url)
{ {
WriteLock writeLock = readWriteLock(url).writeLock(); WriteLock writeLock = readWriteLock(url).writeLock();
writeLock.lock(); writeLock.lock();
try try
{ {
for (int i = 0; i < maxCacheTries; i++) for (int i = 0; i < maxCacheTries; i++)
{ {
ContentReader backingStoreReader = backingStore.getReader(url); ContentReader backingStoreReader = backingStore.getReader(url);
long contentSize = backingStoreReader.getSize(); long contentSize = backingStoreReader.getSize();
if (!quota.beforeWritingCacheFile(contentSize)) if (!quota.beforeWritingCacheFile(contentSize))
{ {
return backingStoreReader; return backingStoreReader;
} }
ContentReader reader = attemptCacheAndRead(url, backingStoreReader); ContentReader reader = attemptCacheAndRead(url, backingStoreReader);
if (reader != null) if (reader != null)
{ {
boolean keepCacheFile = quota.afterWritingCacheFile(contentSize); boolean keepCacheFile = quota.afterWritingCacheFile(contentSize);
if (keepCacheFile) if (keepCacheFile)
{ {
return reader; return reader;
} }
else else
{ {
// Quota strategy has requested cache file not to be kept. // Quota strategy has requested cache file not to be kept.
cache.deleteFile(url); cache.deleteFile(url);
cache.remove(url); cache.remove(url);
return backingStore.getReader(url); return backingStore.getReader(url);
} }
} }
} }
// Have tried multiple times to cache the item and read it back from the cache // Have tried multiple times to cache the item and read it back from the cache
// but there is a recurring problem - give up and return the item from the backing store. // but there is a recurring problem - give up and return the item from the backing store.
if (log.isWarnEnabled()) if (log.isWarnEnabled())
{ {
log.warn("Attempted " + maxCacheTries + " times to cache content item and failed - " log.warn("Attempted " + maxCacheTries + " times to cache content item and failed - "
+ "returning reader from backing store instead [" + + "returning reader from backing store instead [" +
"backingStore=" + backingStore + "backingStore=" + backingStore +
", url=" + url + ", url=" + url +
"]"); "]");
} }
return backingStore.getReader(url); return backingStore.getReader(url);
} }
finally finally
{ {
writeLock.unlock(); writeLock.unlock();
} }
} }
/** /**
* Attempt to read content into a cached file and return a reader onto it. If the content is * Attempt to read content into a cached file and return a reader onto it. If the content is
* already in the cache (possibly due to a race condition between the read/write locks) then * already in the cache (possibly due to a race condition between the read/write locks) then
* a reader onto that content is returned. * a reader onto that content is returned.
* <p> * <p>
* If it is not possible to cache the content and/or get a reader onto the cached content then * If it is not possible to cache the content and/or get a reader onto the cached content then
* <code>null</code> is returned and the method ensure that the URL is not stored in the cache. * <code>null</code> is returned and the method ensure that the URL is not stored in the cache.
* *
* @param url URL to cache. * @param url URL to cache.
* @return A reader onto the cached content file or null if unable to provide one. * @return A reader onto the cached content file or null if unable to provide one.
*/ */
private ContentReader attemptCacheAndRead(String url, ContentReader backingStoreReader) private ContentReader attemptCacheAndRead(String url, ContentReader backingStoreReader)
{ {
ContentReader reader = null; ContentReader reader = null;
try try
{ {
if (!cache.contains(url)) if (!cache.contains(url))
{ {
if (cache.put(url, backingStoreReader)) if (cache.put(url, backingStoreReader))
{ {
reader = cache.getReader(url); reader = cache.getReader(url);
} }
} }
else else
{ {
reader = cache.getReader(url); reader = cache.getReader(url);
} }
} }
catch(CacheMissException e) catch(CacheMissException e)
{ {
cache.remove(url); cache.remove(url);
} }
return reader; return reader;
} }
@Override @Override
public ContentWriter getWriter(final ContentContext context) public ContentWriter getWriter(final ContentContext context)
{ {
if (cacheOnInbound) if (cacheOnInbound)
{ {
final ContentWriter bsWriter = backingStore.getWriter(context); final ContentWriter bsWriter = backingStore.getWriter(context);
if (!quota.beforeWritingCacheFile(0)) if (!quota.beforeWritingCacheFile(0))
{ {
return bsWriter; return bsWriter;
} }
// Writing will be performed straight to the cache. // Writing will be performed straight to the cache.
final String url = bsWriter.getContentUrl(); final String url = bsWriter.getContentUrl();
final BackingStoreAwareCacheWriter cacheWriter = new BackingStoreAwareCacheWriter(cache.getWriter(url), bsWriter); final BackingStoreAwareCacheWriter cacheWriter = new BackingStoreAwareCacheWriter(cache.getWriter(url), bsWriter);
// When finished writing perform these actions. // When finished writing perform these actions.
cacheWriter.addListener(new ContentStreamListener() cacheWriter.addListener(new ContentStreamListener()
{ {
@Override @Override
public void contentStreamClosed() throws ContentIOException public void contentStreamClosed() throws ContentIOException
{ {
// Finished writing to the cache, so copy to the backing store - // Finished writing to the cache, so copy to the backing store -
// ensuring that the encoding attributes are set to the same as for the cache writer. // ensuring that the encoding attributes are set to the same as for the cache writer.
bsWriter.setEncoding(cacheWriter.getEncoding()); bsWriter.setEncoding(cacheWriter.getEncoding());
bsWriter.setLocale(cacheWriter.getLocale()); bsWriter.setLocale(cacheWriter.getLocale());
bsWriter.setMimetype(cacheWriter.getMimetype()); bsWriter.setMimetype(cacheWriter.getMimetype());
bsWriter.putContent(cacheWriter.getReader()); bsWriter.putContent(cacheWriter.getReader());
boolean contentUrlChanged = !url.equals(bsWriter.getContentUrl()); boolean contentUrlChanged = !url.equals(bsWriter.getContentUrl());
// MNT-11758 fix, re-cache files for which content url has changed after write to backing store (e.g. XAM, Centera) // MNT-11758 fix, re-cache files for which content url has changed after write to backing store (e.g. XAM, Centera)
if (!quota.afterWritingCacheFile(cacheWriter.getSize()) || contentUrlChanged) if (!quota.afterWritingCacheFile(cacheWriter.getSize()) || contentUrlChanged)
{ {
if (contentUrlChanged) if (contentUrlChanged)
{ {
// MNT-11758 fix, cache file with new and correct contentUrl after write operation to backing store completed // MNT-11758 fix, cache file with new and correct contentUrl after write operation to backing store completed
cache.put(bsWriter.getContentUrl(), cacheWriter.getReader()); cache.put(bsWriter.getContentUrl(), cacheWriter.getReader());
} }
// Quota manager has requested that the new cache file is not kept. // Quota manager has requested that the new cache file is not kept.
cache.deleteFile(url); cache.deleteFile(url);
cache.remove(url); cache.remove(url);
} }
} }
}); });
return cacheWriter; return cacheWriter;
} }
else else
{ {
// No need to invalidate the cache for this content URL, since a content URL // No need to invalidate the cache for this content URL, since a content URL
// is only ever written to once. // is only ever written to once.
return backingStore.getWriter(context); return backingStore.getWriter(context);
} }
} }
@Override @Override
public boolean delete(String contentUrl) public boolean delete(String contentUrl)
{ {
if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL))
{ {
// This is not a failure but the content can never actually be deleted // This is not a failure but the content can never actually be deleted
return false; return false;
} }
ReentrantReadWriteLock readWriteLock = readWriteLock(contentUrl); ReentrantReadWriteLock readWriteLock = readWriteLock(contentUrl);
ReadLock readLock = readWriteLock.readLock(); ReadLock readLock = readWriteLock.readLock();
readLock.lock(); readLock.lock();
try try
{ {
if (!cache.contains(contentUrl)) if (!cache.contains(contentUrl))
{ {
// The item isn't in the cache, so simply delete from the backing store // The item isn't in the cache, so simply delete from the backing store
return backingStore.delete(contentUrl); return backingStore.delete(contentUrl);
} }
} }
finally finally
{ {
readLock.unlock(); readLock.unlock();
} }
WriteLock writeLock = readWriteLock.writeLock(); WriteLock writeLock = readWriteLock.writeLock();
writeLock.lock(); writeLock.lock();
try try
{ {
// Double check the content still exists in the cache // Double check the content still exists in the cache
if (cache.contains(contentUrl)) if (cache.contains(contentUrl))
{ {
// The item is in the cache, so remove. // The item is in the cache, so remove.
cache.remove(contentUrl); cache.remove(contentUrl);
} }
// Whether the item was in the cache or not, it must still be deleted from the backing store. // Whether the item was in the cache or not, it must still be deleted from the backing store.
return backingStore.delete(contentUrl); return backingStore.delete(contentUrl);
} }
finally finally
{ {
writeLock.unlock(); writeLock.unlock();
} }
} }
/** /**
* Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than * Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than
* per URL, so some contention is expected. * per URL, so some contention is expected.
* *
* @param url String * @param url String
* @return ReentrantReadWriteLock * @return ReentrantReadWriteLock
*/ */
public ReentrantReadWriteLock readWriteLock(String url) public ReentrantReadWriteLock readWriteLock(String url)
{ {
return locks[lockIndex(url)]; return locks[lockIndex(url)];
} }
private int lockIndex(String url) private int lockIndex(String url)
{ {
return url.hashCode() & (numLocks - 1); return url.hashCode() & (numLocks - 1);
} }
@Required @Required
public void setBackingStore(ContentStore backingStore) public void setBackingStore(ContentStore backingStore)
{ {
this.backingStore = backingStore; this.backingStore = backingStore;
} }
public String getBackingStoreType() public String getBackingStoreType()
{ {
return backingStore.getClass().getName(); return backingStore.getClass().getName();
} }
public String getBackingStoreDescription() public String getBackingStoreDescription()
{ {
return backingStore.toString(); return backingStore.toString();
} }
@Required @Required
public void setCache(ContentCache cache) public void setCache(ContentCache cache)
{ {
this.cache = cache; this.cache = cache;
} }
public ContentCache getCache() public ContentCache getCache()
{ {
return this.cache; return this.cache;
} }
public void setCacheOnInbound(boolean cacheOnInbound) public void setCacheOnInbound(boolean cacheOnInbound)
{ {
this.cacheOnInbound = cacheOnInbound; this.cacheOnInbound = cacheOnInbound;
} }
public boolean isCacheOnInbound() public boolean isCacheOnInbound()
{ {
return this.cacheOnInbound; return this.cacheOnInbound;
} }
public int getMaxCacheTries() public int getMaxCacheTries()
{ {
return this.maxCacheTries; return this.maxCacheTries;
} }
public void setMaxCacheTries(int maxCacheTries) public void setMaxCacheTries(int maxCacheTries)
{ {
this.maxCacheTries = maxCacheTries; this.maxCacheTries = maxCacheTries;
} }
/** /**
* Sets the QuotaManagerStrategy that will be used. * Sets the QuotaManagerStrategy that will be used.
* *
* @param quota QuotaManagerStrategy * @param quota QuotaManagerStrategy
*/ */
@Required @Required
public void setQuota(QuotaManagerStrategy quota) public void setQuota(QuotaManagerStrategy quota)
{ {
this.quota = quota; this.quota = quota;
} }
public QuotaManagerStrategy getQuota() public QuotaManagerStrategy getQuota()
{ {
return this.quota; return this.quota;
} }
@Override @Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher)
{ {
this.eventPublisher = applicationEventPublisher; this.eventPublisher = applicationEventPublisher;
} }
@Override @Override
public void setBeanName(String name) public void setBeanName(String name)
{ {
this.beanName = name; this.beanName = name;
} }
public String getBeanName() public String getBeanName()
{ {
return this.beanName; return this.beanName;
} }
}
public boolean isDirectAccessSupported()
{
return backingStore.isDirectAccessSupported();
}
public DirectAccessUrl getDirectAccessUrl(String contentUrl, Date expiresAt)
{
return backingStore.getDirectAccessUrl(contentUrl, expiresAt);
}
}

View File

@@ -25,6 +25,7 @@
*/ */
package org.alfresco.repo.content.replication; package org.alfresco.repo.content.replication;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
@@ -39,6 +40,7 @@ import org.alfresco.repo.content.caching.CachingContentStore;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.DirectAccessUrl;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -262,4 +264,115 @@ public class AggregatingContentStore extends AbstractContentStore
} }
return deleted; return deleted;
} }
/**
* @return Returns <tt>true</tt> if at least one store supports direct access
*/
public boolean isDirectAccessSupported()
{
// Check the primary store
boolean isDirectAccessSupported = primaryStore.isDirectAccessSupported();
if (!isDirectAccessSupported)
{
// Direct access is not supported by the primary store so we have to check the
// other stores
for (ContentStore store : secondaryStores)
{
isDirectAccessSupported = store.isDirectAccessSupported();
if (isDirectAccessSupported)
{
break;
}
}
}
return isDirectAccessSupported;
}
public DirectAccessUrl getDirectAccessUrl(String contentUrl, Date expiresAt)
{
if (primaryStore == null)
{
throw new AlfrescoRuntimeException("ReplicatingContentStore not initialised");
}
// get a read lock so that we are sure that no replication is underway
readLock.lock();
try
{
// Keep track of the unsupported state of the content URL - it might be a rubbish URL
boolean contentUrlSupported = true;
boolean directAccessUrlSupported = true;
DirectAccessUrl directAccessUrl = null;
// Check the primary store
try
{
directAccessUrl = primaryStore.getDirectAccessUrl(contentUrl, expiresAt);
}
catch (UnsupportedOperationException e)
{
// The store does not support direct access URL
directAccessUrlSupported = false;
}
catch (UnsupportedContentUrlException e)
{
// The store can't handle the content URL
contentUrlSupported = false;
}
if (directAccessUrl != null)
{
return directAccessUrl;
}
// the content is not in the primary store so we have to go looking for it
for (ContentStore store : secondaryStores)
{
try
{
directAccessUrl = store.getDirectAccessUrl(contentUrl, expiresAt);
}
catch (UnsupportedOperationException e)
{
// The store does not support direct access URL
directAccessUrlSupported = false;
}
catch (UnsupportedContentUrlException e)
{
// The store can't handle the content URL
contentUrlSupported = false;
}
if (directAccessUrl != null)
{
break;
}
}
if (directAccessUrl == null)
{
if (!directAccessUrlSupported)
{
// The direct access URL was not supported
throw new UnsupportedOperationException("Retrieving direct access URLs is not supported by this content store.");
}
else if (!contentUrlSupported)
{
// The content URL was not supported
throw new UnsupportedContentUrlException(this, contentUrl);
}
}
return directAccessUrl;
}
finally
{
readLock.unlock();
}
}
} }

View File

@@ -23,8 +23,8 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #L%
*/ */
package org.alfresco.repo.content.replication; package org.alfresco.repo.content.replication;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -32,136 +32,280 @@ import java.util.List;
import org.alfresco.repo.content.AbstractWritableContentStoreTest; import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.UnsupportedContentUrlException;
import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.DirectAccessUrl;
import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.alfresco.util.TempFileProvider; import org.alfresco.util.TempFileProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category; import org.junit.experimental.categories.Category;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/** import static org.mockito.ArgumentMatchers.any;
* Tests read and write functionality for the aggregating store. import static org.mockito.ArgumentMatchers.anyString;
* <p> import static org.mockito.ArgumentMatchers.eq;
* import static org.mockito.Mockito.verify;
* @see org.alfresco.repo.content.replication.AggregatingContentStore import static org.mockito.Mockito.when;
*
* @author Derek Hulley /**
* @author Mark Rogers * Tests read and write functionality for the aggregating store.
*/ * <p>
@Category(OwnJVMTestsCategory.class) *
public class AggregatingContentStoreTest extends AbstractWritableContentStoreTest * @see org.alfresco.repo.content.replication.AggregatingContentStore
{ *
private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency"; * @author Derek Hulley
* @author Mark Rogers
private AggregatingContentStore aggregatingStore; */
private ContentStore primaryStore; @Category(OwnJVMTestsCategory.class)
private List<ContentStore> secondaryStores; public class AggregatingContentStoreTest extends AbstractWritableContentStoreTest
{
@Before private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency";
public void before() throws Exception
{ private AggregatingContentStore aggregatingStore;
File tempDir = TempFileProvider.getTempDir(); private ContentStore primaryStore;
// create a primary file store private List<ContentStore> secondaryStores;
String storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate();
primaryStore = new FileContentStore(ctx, storeDir); @Mock
// create some secondary file stores ContentStore primaryStoreMock;
secondaryStores = new ArrayList<ContentStore>(3); @Mock
for (int i = 0; i < 4; i++) ContentStore secondaryStoreMock;
{ @Mock
storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate(); AggregatingContentStore aggregatingContentStoreMock;
FileContentStore store = new FileContentStore(ctx, storeDir);
secondaryStores.add(store); @Rule
} public MockitoRule rule = MockitoJUnit.rule();
// Create the aggregating store
aggregatingStore = new AggregatingContentStore(); @Before
aggregatingStore.setPrimaryStore(primaryStore); public void before() throws Exception
aggregatingStore.setSecondaryStores(secondaryStores); {
} File tempDir = TempFileProvider.getTempDir();
// create a primary file store
@Override String storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate();
public ContentStore getStore() primaryStore = new FileContentStore(ctx, storeDir);
{ // create some secondary file stores
return aggregatingStore; secondaryStores = new ArrayList<ContentStore>(3);
} for (int i = 0; i < 4; i++)
{
/** storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate();
* Get a writer into the store. This test class assumes that the store is writable and FileContentStore store = new FileContentStore(ctx, storeDir);
* that it therefore supports the ability to write content. secondaryStores.add(store);
* }
* @return // Create the aggregating store
* Returns a writer for new content aggregatingStore = new AggregatingContentStore();
*/ aggregatingStore.setPrimaryStore(primaryStore);
protected ContentWriter getWriter() aggregatingStore.setSecondaryStores(secondaryStores);
{ }
ContentStore store = getStore();
return store.getWriter(ContentStore.NEW_CONTENT_CONTEXT); @Override
} public ContentStore getStore()
{
return aggregatingStore;
/** }
* {@inheritDoc}
* <p> /**
* This implementation creates some content in the store and returns the new content URL. * Get a writer into the store. This test class assumes that the store is writable and
*/ * that it therefore supports the ability to write content.
protected String getExistingContentUrl() *
{ * @return
ContentWriter writer = getWriter(); * Returns a writer for new content
writer.putContent("Content for getExistingContentUrl"); */
return writer.getContentUrl(); protected ContentWriter getWriter()
} {
ContentStore store = getStore();
public void testAddContent() throws Exception return store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
{ }
ContentWriter writer = getWriter();
writer.putContent(SOME_CONTENT);
String contentUrl = writer.getContentUrl(); /**
* {@inheritDoc}
checkForUrl(contentUrl, true); * <p>
} * This implementation creates some content in the store and returns the new content URL.
*/
/** protected String getExistingContentUrl()
* Checks that the url is present in each of the stores {
* ContentWriter writer = getWriter();
* @param contentUrl String writer.putContent("Content for getExistingContentUrl");
* @param mustExist true if the content must exist, false if it must <b>not</b> exist return writer.getContentUrl();
*/ }
private void checkForUrl(String contentUrl, boolean mustExist)
public void testAddContent() throws Exception
{
ContentWriter writer = getWriter();
writer.putContent(SOME_CONTENT);
String contentUrl = writer.getContentUrl();
checkForUrl(contentUrl, true);
}
/**
* Checks that the url is present in each of the stores
*
* @param contentUrl String
* @param mustExist true if the content must exist, false if it must <b>not</b> exist
*/
private void checkForUrl(String contentUrl, boolean mustExist)
{ {
ContentReader reader = getReader(contentUrl); ContentReader reader = getReader(contentUrl);
assertEquals("Reader state differs from expected: " + reader, mustExist, reader.exists()); assertEquals("Reader state differs from expected: " + reader, mustExist, reader.exists());
} }
public void testDelete() throws Exception public void testDelete() throws Exception
{ {
// write some content // write some content
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
writer.putContent(SOME_CONTENT); writer.putContent(SOME_CONTENT);
String contentUrl = writer.getContentUrl(); String contentUrl = writer.getContentUrl();
ContentReader reader = primaryStore.getReader(contentUrl); ContentReader reader = primaryStore.getReader(contentUrl);
assertTrue("Content was not in the primary store", reader.exists()); assertTrue("Content was not in the primary store", reader.exists());
assertEquals("The content was incorrect", SOME_CONTENT, reader.getContentString()); assertEquals("The content was incorrect", SOME_CONTENT, reader.getContentString());
getStore().delete(contentUrl); getStore().delete(contentUrl);
checkForUrl(contentUrl, false); checkForUrl(contentUrl, false);
} }
public void testReadFromSecondaryStore() public void testReadFromSecondaryStore()
{ {
// pick a secondary store and write some content to it // pick a secondary store and write some content to it
ContentStore secondaryStore = secondaryStores.get(2); ContentStore secondaryStore = secondaryStores.get(2);
ContentWriter writer = secondaryStore.getWriter(ContentContext.NULL_CONTEXT); ContentWriter writer = secondaryStore.getWriter(ContentContext.NULL_CONTEXT);
writer.putContent(SOME_CONTENT); writer.putContent(SOME_CONTENT);
String contentUrl = writer.getContentUrl(); String contentUrl = writer.getContentUrl();
checkForUrl(contentUrl, true); checkForUrl(contentUrl, true);
} }
@Test
} public void testIsDirectAccessSupported()
{
// Create the aggregating store
AggregatingContentStore aggStore = new AggregatingContentStore();
aggStore.setPrimaryStore(primaryStoreMock);
aggStore.setSecondaryStores(List.of(secondaryStoreMock));
// By default it is unsupported
assertFalse(aggStore.isDirectAccessSupported());
// Supported if at least one store supports direct access
{
when(primaryStoreMock.isDirectAccessSupported()).thenReturn(false);
when(secondaryStoreMock.isDirectAccessSupported()).thenReturn(true);
assertTrue(aggStore.isDirectAccessSupported());
when(primaryStoreMock.isDirectAccessSupported()).thenReturn(true);
when(secondaryStoreMock.isDirectAccessSupported()).thenReturn(true);
assertTrue(aggStore.isDirectAccessSupported());
when(primaryStoreMock.isDirectAccessSupported()).thenReturn(true);
when(secondaryStoreMock.isDirectAccessSupported()).thenReturn(false);
assertTrue(aggStore.isDirectAccessSupported());
}
}
@Test
public void testGetDirectAccessUrl()
{
// Create the aggregating store
AggregatingContentStore aggStore = new AggregatingContentStore();
aggStore.setPrimaryStore(primaryStoreMock);
aggStore.setSecondaryStores(List.of(secondaryStoreMock));
UnsupportedOperationException unsupportedExc = new UnsupportedOperationException();
UnsupportedContentUrlException unsupportedContentUrlExc = new UnsupportedContentUrlException(aggStore, "");
// By default it is unsupported
DirectAccessUrl directAccessUrl = aggStore.getDirectAccessUrl("url", null);
assertNull(directAccessUrl);
// Direct access not supported
try
{
when(primaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedExc);
aggStore.getDirectAccessUrl("urlDANotSupported", null);
fail();
}
catch (UnsupportedOperationException e)
{
// Expected
}
try
{
when(primaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedContentUrlExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedExc);
aggStore.getDirectAccessUrl("urlDANotSupported", null);
fail();
}
catch (UnsupportedOperationException e)
{
// Expected
}
try
{
when(primaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlDANotSupported"), any())).thenThrow(unsupportedContentUrlExc);
aggStore.getDirectAccessUrl("urlDANotSupported", null);
fail();
}
catch (UnsupportedOperationException e)
{
// Expected
}
// Content url not supported
try
{
when(primaryStoreMock.getDirectAccessUrl(eq("urlNotSupported"), any())).thenThrow(unsupportedContentUrlExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlNotSupported"), any())).thenThrow(unsupportedContentUrlExc);
aggStore.getDirectAccessUrl("urlNotSupported", null);
fail();
}
catch (UnsupportedContentUrlException e)
{
// Expected
}
when(primaryStoreMock.getDirectAccessUrl(eq("urlPriSupported"), any())).thenReturn(new DirectAccessUrl());
when(secondaryStoreMock.getDirectAccessUrl(eq("urlPriSupported"), any())).thenThrow(unsupportedExc);
directAccessUrl = aggStore.getDirectAccessUrl("urlPriSupported", null);
assertNotNull(directAccessUrl);
when(primaryStoreMock.getDirectAccessUrl(eq("urlPriSupported"), any())).thenReturn(new DirectAccessUrl());
when(secondaryStoreMock.getDirectAccessUrl(eq("urlPriSupported"), any())).thenThrow(unsupportedContentUrlExc);
directAccessUrl = aggStore.getDirectAccessUrl("urlPriSupported", null);
assertNotNull(directAccessUrl);
when(primaryStoreMock.getDirectAccessUrl(eq("urlSecSupported"), any())).thenThrow(unsupportedExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlSecSupported"), any())).thenReturn(new DirectAccessUrl());
directAccessUrl = aggStore.getDirectAccessUrl("urlSecSupported", null);
assertNotNull(directAccessUrl);
when(primaryStoreMock.getDirectAccessUrl(eq("urlSecSupported"), any())).thenThrow(unsupportedContentUrlExc);
when(secondaryStoreMock.getDirectAccessUrl(eq("urlSecSupported"), any())).thenReturn(new DirectAccessUrl());
directAccessUrl = aggStore.getDirectAccessUrl("urlSecSupported", null);
assertNotNull(directAccessUrl);
when(primaryStoreMock.getDirectAccessUrl(eq("urlPriSupported"), any())).thenReturn(new DirectAccessUrl());
when(secondaryStoreMock.getDirectAccessUrl(eq("urlSecSupported"), any())).thenReturn(new DirectAccessUrl());
directAccessUrl = aggStore.getDirectAccessUrl("urlPriSupported", null);
assertNotNull(directAccessUrl);
directAccessUrl = aggStore.getDirectAccessUrl("urlSecSupported", null);
assertNotNull(directAccessUrl);
}
}