diff --git a/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java b/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java index e29dae1621..5cad001984 100644 --- a/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java +++ b/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java @@ -23,8 +23,9 @@ * along with Alfresco. If not, see . * #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.ReadLock; 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.ContentStreamListener; 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.LogFactory; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.annotation.Required; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; - -/** - * 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. - *

- * 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. - * - * @author Matt Ward - */ -public class CachingContentStore implements ContentStore, ApplicationEventPublisherAware, BeanNameAware -{ - 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 - private final static int numLocks = 256; - private final static ReentrantReadWriteLock[] locks; - private ContentStore backingStore; - private ContentCache cache; - private QuotaManagerStrategy quota = new UnlimitedQuotaStrategy(); - private boolean cacheOnInbound; - private int maxCacheTries = 2; - private ApplicationEventPublisher eventPublisher; - private String beanName; - - static - { - locks = new ReentrantReadWriteLock[numLocks]; - for (int i = 0; i < numLocks; i++) - { - locks[i] = new ReentrantReadWriteLock(); - } - } - - public CachingContentStore() - { - } - - public CachingContentStore(ContentStore backingStore, ContentCache cache, boolean cacheOnInbound) - { - this.backingStore = backingStore; - this.cache = cache; - this.cacheOnInbound = cacheOnInbound; - } - - /** - * Initialisation method, should be called once the CachingContentStore has been constructed. - */ - public void init() - { - eventPublisher.publishEvent(new CachingContentStoreCreatedEvent(this)); - } - - @Override - public boolean isContentUrlSupported(String contentUrl) - { - return backingStore.isContentUrlSupported(contentUrl); - } - - @Override - public boolean isWriteSupported() - { - return backingStore.isWriteSupported(); - } - - @Override - public long getSpaceFree() - { - return backingStore.getSpaceFree(); - } - - @Override - public long getSpaceTotal() - { - return backingStore.getSpaceTotal(); - } - - @Override - public String getRootLocation() - { - return backingStore.getRootLocation(); - } - - /** - * {@inheritDoc} - *

- * For {@link #SPOOF_PROTOCOL spoofed} URLs, the URL always exists. - */ - @Override - public boolean exists(String contentUrl) - { - if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) - { - return true; - } - else - { - return backingStore.exists(contentUrl); - } - } - - /** - * {@inheritDoc} - *

- * This store handles the {@link FileContentStore#SPOOF_PROTOCOL} so that underlying stores do not need - * to implement anything related to spoofing. - */ - @Override - public ContentReader getReader(String contentUrl) - { - // Handle the spoofed URL - if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) - { - return new SpoofedTextContentReader(contentUrl); - } - - // 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 - // when it should only be read once - cached versions should be returned after that. - ReadLock readLock = readWriteLock(contentUrl).readLock(); - readLock.lock(); - try - { - if (cache.contains(contentUrl)) - { - return cache.getReader(contentUrl); - } - } - catch(CacheMissException e) - { - // Fall through to cacheAndRead(url); - } - finally - { - readLock.unlock(); - } - - return cacheAndRead(contentUrl); - } - - - private ContentReader cacheAndRead(String url) - { - WriteLock writeLock = readWriteLock(url).writeLock(); - writeLock.lock(); - try - { - for (int i = 0; i < maxCacheTries; i++) - { - ContentReader backingStoreReader = backingStore.getReader(url); - long contentSize = backingStoreReader.getSize(); - - if (!quota.beforeWritingCacheFile(contentSize)) - { - return backingStoreReader; - } - - ContentReader reader = attemptCacheAndRead(url, backingStoreReader); - - if (reader != null) - { - boolean keepCacheFile = quota.afterWritingCacheFile(contentSize); - if (keepCacheFile) - { - return reader; - } - else - { - // Quota strategy has requested cache file not to be kept. - cache.deleteFile(url); - cache.remove(url); - return backingStore.getReader(url); - } - } - } - // 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. - if (log.isWarnEnabled()) - { - log.warn("Attempted " + maxCacheTries + " times to cache content item and failed - " - + "returning reader from backing store instead [" + - "backingStore=" + backingStore + - ", url=" + url + - "]"); - } - return backingStore.getReader(url); - } - finally - { - writeLock.unlock(); - } - } - - - /** - * 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 - * a reader onto that content is returned. - *

- * If it is not possible to cache the content and/or get a reader onto the cached content then - * null is returned and the method ensure that the URL is not stored in the cache. - * - * @param url URL to cache. - * @return A reader onto the cached content file or null if unable to provide one. - */ - private ContentReader attemptCacheAndRead(String url, ContentReader backingStoreReader) - { - ContentReader reader = null; - try - { - if (!cache.contains(url)) - { - if (cache.put(url, backingStoreReader)) - { - reader = cache.getReader(url); - } - } - else - { - reader = cache.getReader(url); - } - } - catch(CacheMissException e) - { - cache.remove(url); - } - - return reader; - } - - @Override - public ContentWriter getWriter(final ContentContext context) - { - if (cacheOnInbound) - { - final ContentWriter bsWriter = backingStore.getWriter(context); - - if (!quota.beforeWritingCacheFile(0)) - { - return bsWriter; - } - - // Writing will be performed straight to the cache. - final String url = bsWriter.getContentUrl(); - final BackingStoreAwareCacheWriter cacheWriter = new BackingStoreAwareCacheWriter(cache.getWriter(url), bsWriter); - - // When finished writing perform these actions. - cacheWriter.addListener(new ContentStreamListener() - { - @Override - public void contentStreamClosed() throws ContentIOException - { - // 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. - bsWriter.setEncoding(cacheWriter.getEncoding()); - bsWriter.setLocale(cacheWriter.getLocale()); - bsWriter.setMimetype(cacheWriter.getMimetype()); - bsWriter.putContent(cacheWriter.getReader()); - 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) - if (!quota.afterWritingCacheFile(cacheWriter.getSize()) || contentUrlChanged) - { - if (contentUrlChanged) - { - // MNT-11758 fix, cache file with new and correct contentUrl after write operation to backing store completed - cache.put(bsWriter.getContentUrl(), cacheWriter.getReader()); - } - // Quota manager has requested that the new cache file is not kept. - cache.deleteFile(url); - cache.remove(url); - } - } - }); - - return cacheWriter; - } - 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); - } - } - - @Override - public boolean delete(String contentUrl) - { - if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) - { - // This is not a failure but the content can never actually be deleted - return false; - } - - ReentrantReadWriteLock readWriteLock = readWriteLock(contentUrl); - ReadLock readLock = readWriteLock.readLock(); - readLock.lock(); - try - { - if (!cache.contains(contentUrl)) - { - // The item isn't in the cache, so simply delete from the backing store - return backingStore.delete(contentUrl); - } - } - finally - { - readLock.unlock(); - } - - WriteLock writeLock = readWriteLock.writeLock(); - writeLock.lock(); - try - { - // Double check the content still exists in the cache - if (cache.contains(contentUrl)) - { - // The item is in the cache, so remove. - cache.remove(contentUrl); - - } - // Whether the item was in the cache or not, it must still be deleted from the backing store. - return backingStore.delete(contentUrl); - } - finally - { - writeLock.unlock(); - } - } - - /** - * Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than - * per URL, so some contention is expected. - * - * @param url String - * @return ReentrantReadWriteLock - */ - public ReentrantReadWriteLock readWriteLock(String url) - { - return locks[lockIndex(url)]; - } - - private int lockIndex(String url) - { - return url.hashCode() & (numLocks - 1); - } - - @Required - public void setBackingStore(ContentStore backingStore) - { - this.backingStore = backingStore; - } - - public String getBackingStoreType() - { - return backingStore.getClass().getName(); - } - - public String getBackingStoreDescription() - { - return backingStore.toString(); - } - - @Required - public void setCache(ContentCache cache) - { - this.cache = cache; - } - - public ContentCache getCache() - { - return this.cache; - } - - public void setCacheOnInbound(boolean cacheOnInbound) - { - this.cacheOnInbound = cacheOnInbound; - } - - public boolean isCacheOnInbound() - { - return this.cacheOnInbound; - } - - public int getMaxCacheTries() - { - return this.maxCacheTries; - } - - public void setMaxCacheTries(int maxCacheTries) - { - this.maxCacheTries = maxCacheTries; - } - - /** - * Sets the QuotaManagerStrategy that will be used. - * - * @param quota QuotaManagerStrategy - */ - @Required - public void setQuota(QuotaManagerStrategy quota) - { - this.quota = quota; - } - - public QuotaManagerStrategy getQuota() - { - return this.quota; - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) - { - this.eventPublisher = applicationEventPublisher; - } - - @Override - public void setBeanName(String name) - { - this.beanName = name; - } - - public String getBeanName() - { - return this.beanName; - } -} + +/** + * 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. + *

+ * 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. + * + * @author Matt Ward + */ +public class CachingContentStore implements ContentStore, ApplicationEventPublisherAware, BeanNameAware +{ + 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 + private final static int numLocks = 256; + private final static ReentrantReadWriteLock[] locks; + private ContentStore backingStore; + private ContentCache cache; + private QuotaManagerStrategy quota = new UnlimitedQuotaStrategy(); + private boolean cacheOnInbound; + private int maxCacheTries = 2; + private ApplicationEventPublisher eventPublisher; + private String beanName; + + static + { + locks = new ReentrantReadWriteLock[numLocks]; + for (int i = 0; i < numLocks; i++) + { + locks[i] = new ReentrantReadWriteLock(); + } + } + + public CachingContentStore() + { + } + + public CachingContentStore(ContentStore backingStore, ContentCache cache, boolean cacheOnInbound) + { + this.backingStore = backingStore; + this.cache = cache; + this.cacheOnInbound = cacheOnInbound; + } + + /** + * Initialisation method, should be called once the CachingContentStore has been constructed. + */ + public void init() + { + eventPublisher.publishEvent(new CachingContentStoreCreatedEvent(this)); + } + + @Override + public boolean isContentUrlSupported(String contentUrl) + { + return backingStore.isContentUrlSupported(contentUrl); + } + + @Override + public boolean isWriteSupported() + { + return backingStore.isWriteSupported(); + } + + @Override + public long getSpaceFree() + { + return backingStore.getSpaceFree(); + } + + @Override + public long getSpaceTotal() + { + return backingStore.getSpaceTotal(); + } + + @Override + public String getRootLocation() + { + return backingStore.getRootLocation(); + } + + /** + * {@inheritDoc} + *

+ * For {@link #SPOOF_PROTOCOL spoofed} URLs, the URL always exists. + */ + @Override + public boolean exists(String contentUrl) + { + if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) + { + return true; + } + else + { + return backingStore.exists(contentUrl); + } + } + + /** + * {@inheritDoc} + *

+ * This store handles the {@link FileContentStore#SPOOF_PROTOCOL} so that underlying stores do not need + * to implement anything related to spoofing. + */ + @Override + public ContentReader getReader(String contentUrl) + { + // Handle the spoofed URL + if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) + { + return new SpoofedTextContentReader(contentUrl); + } + + // 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 + // when it should only be read once - cached versions should be returned after that. + ReadLock readLock = readWriteLock(contentUrl).readLock(); + readLock.lock(); + try + { + if (cache.contains(contentUrl)) + { + return cache.getReader(contentUrl); + } + } + catch(CacheMissException e) + { + // Fall through to cacheAndRead(url); + } + finally + { + readLock.unlock(); + } + + return cacheAndRead(contentUrl); + } + + + private ContentReader cacheAndRead(String url) + { + WriteLock writeLock = readWriteLock(url).writeLock(); + writeLock.lock(); + try + { + for (int i = 0; i < maxCacheTries; i++) + { + ContentReader backingStoreReader = backingStore.getReader(url); + long contentSize = backingStoreReader.getSize(); + + if (!quota.beforeWritingCacheFile(contentSize)) + { + return backingStoreReader; + } + + ContentReader reader = attemptCacheAndRead(url, backingStoreReader); + + if (reader != null) + { + boolean keepCacheFile = quota.afterWritingCacheFile(contentSize); + if (keepCacheFile) + { + return reader; + } + else + { + // Quota strategy has requested cache file not to be kept. + cache.deleteFile(url); + cache.remove(url); + return backingStore.getReader(url); + } + } + } + // 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. + if (log.isWarnEnabled()) + { + log.warn("Attempted " + maxCacheTries + " times to cache content item and failed - " + + "returning reader from backing store instead [" + + "backingStore=" + backingStore + + ", url=" + url + + "]"); + } + return backingStore.getReader(url); + } + finally + { + writeLock.unlock(); + } + } + + + /** + * 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 + * a reader onto that content is returned. + *

+ * If it is not possible to cache the content and/or get a reader onto the cached content then + * null is returned and the method ensure that the URL is not stored in the cache. + * + * @param url URL to cache. + * @return A reader onto the cached content file or null if unable to provide one. + */ + private ContentReader attemptCacheAndRead(String url, ContentReader backingStoreReader) + { + ContentReader reader = null; + try + { + if (!cache.contains(url)) + { + if (cache.put(url, backingStoreReader)) + { + reader = cache.getReader(url); + } + } + else + { + reader = cache.getReader(url); + } + } + catch(CacheMissException e) + { + cache.remove(url); + } + + return reader; + } + + @Override + public ContentWriter getWriter(final ContentContext context) + { + if (cacheOnInbound) + { + final ContentWriter bsWriter = backingStore.getWriter(context); + + if (!quota.beforeWritingCacheFile(0)) + { + return bsWriter; + } + + // Writing will be performed straight to the cache. + final String url = bsWriter.getContentUrl(); + final BackingStoreAwareCacheWriter cacheWriter = new BackingStoreAwareCacheWriter(cache.getWriter(url), bsWriter); + + // When finished writing perform these actions. + cacheWriter.addListener(new ContentStreamListener() + { + @Override + public void contentStreamClosed() throws ContentIOException + { + // 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. + bsWriter.setEncoding(cacheWriter.getEncoding()); + bsWriter.setLocale(cacheWriter.getLocale()); + bsWriter.setMimetype(cacheWriter.getMimetype()); + bsWriter.putContent(cacheWriter.getReader()); + 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) + if (!quota.afterWritingCacheFile(cacheWriter.getSize()) || contentUrlChanged) + { + if (contentUrlChanged) + { + // MNT-11758 fix, cache file with new and correct contentUrl after write operation to backing store completed + cache.put(bsWriter.getContentUrl(), cacheWriter.getReader()); + } + // Quota manager has requested that the new cache file is not kept. + cache.deleteFile(url); + cache.remove(url); + } + } + }); + + return cacheWriter; + } + 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); + } + } + + @Override + public boolean delete(String contentUrl) + { + if (contentUrl.startsWith(FileContentStore.SPOOF_PROTOCOL)) + { + // This is not a failure but the content can never actually be deleted + return false; + } + + ReentrantReadWriteLock readWriteLock = readWriteLock(contentUrl); + ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try + { + if (!cache.contains(contentUrl)) + { + // The item isn't in the cache, so simply delete from the backing store + return backingStore.delete(contentUrl); + } + } + finally + { + readLock.unlock(); + } + + WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try + { + // Double check the content still exists in the cache + if (cache.contains(contentUrl)) + { + // The item is in the cache, so remove. + cache.remove(contentUrl); + + } + // Whether the item was in the cache or not, it must still be deleted from the backing store. + return backingStore.delete(contentUrl); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than + * per URL, so some contention is expected. + * + * @param url String + * @return ReentrantReadWriteLock + */ + public ReentrantReadWriteLock readWriteLock(String url) + { + return locks[lockIndex(url)]; + } + + private int lockIndex(String url) + { + return url.hashCode() & (numLocks - 1); + } + + @Required + public void setBackingStore(ContentStore backingStore) + { + this.backingStore = backingStore; + } + + public String getBackingStoreType() + { + return backingStore.getClass().getName(); + } + + public String getBackingStoreDescription() + { + return backingStore.toString(); + } + + @Required + public void setCache(ContentCache cache) + { + this.cache = cache; + } + + public ContentCache getCache() + { + return this.cache; + } + + public void setCacheOnInbound(boolean cacheOnInbound) + { + this.cacheOnInbound = cacheOnInbound; + } + + public boolean isCacheOnInbound() + { + return this.cacheOnInbound; + } + + public int getMaxCacheTries() + { + return this.maxCacheTries; + } + + public void setMaxCacheTries(int maxCacheTries) + { + this.maxCacheTries = maxCacheTries; + } + + /** + * Sets the QuotaManagerStrategy that will be used. + * + * @param quota QuotaManagerStrategy + */ + @Required + public void setQuota(QuotaManagerStrategy quota) + { + this.quota = quota; + } + + public QuotaManagerStrategy getQuota() + { + return this.quota; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) + { + this.eventPublisher = applicationEventPublisher; + } + + @Override + public void setBeanName(String name) + { + this.beanName = name; + } + + public String getBeanName() + { + return this.beanName; + } + + public boolean isDirectAccessSupported() + { + return backingStore.isDirectAccessSupported(); + } + + public DirectAccessUrl getDirectAccessUrl(String contentUrl, Date expiresAt) + { + return backingStore.getDirectAccessUrl(contentUrl, expiresAt); + } +} diff --git a/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java b/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java index c5eeabac8f..15cc9c5246 100644 --- a/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java +++ b/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java @@ -25,6 +25,7 @@ */ package org.alfresco.repo.content.replication; +import java.util.Date; import java.util.List; import java.util.concurrent.locks.Lock; 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.ContentReader; 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.LogFactory; @@ -262,4 +264,115 @@ public class AggregatingContentStore extends AbstractContentStore } return deleted; } + + /** + * @return Returns true 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(); + } + } } diff --git a/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java b/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java index 65eda56d02..ffac9eea8b 100644 --- a/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java +++ b/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java @@ -1,490 +1,523 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * 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 . - * #L% - */ -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.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.Locale; - -import org.alfresco.repo.content.ContentContext; -import org.alfresco.repo.content.ContentStore; -import org.alfresco.repo.content.caching.quota.QuotaManagerStrategy; -import org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategy; -import org.alfresco.repo.content.filestore.SpoofedTextContentReader; -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.junit.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; - private ContentReader sourceContent; - private ContentReader cachedContent; - - @Mock - private ContentStore backingStore; - - @Mock - private ContentCache cache; - - - @Before - public void setUp() throws Exception - { - cachingStore = new CachingContentStore(backingStore, cache, false); - cachingStore.setQuota(new UnlimitedQuotaStrategy()); - - sourceContent = mock(ContentReader.class, "sourceContent"); - cachedContent = mock(ContentReader.class, "cachedContent"); - } - - - @Test - public void getReaderForItemInCache() - { - when(cache.contains("url")).thenReturn(true); - when(cache.getReader("url")).thenReturn(cachedContent); - - ContentReader returnedReader = cachingStore.getReader("url"); - - assertSame(returnedReader, cachedContent); - verify(backingStore, never()).getReader(anyString()); - } - - - @Test - // Item isn't in cache, so will be cached and returned. - public void getReaderForItemMissingFromCache() - { - when(cache.getReader("url")).thenReturn(cachedContent); - when(backingStore.getReader("url")).thenReturn(sourceContent); - when(sourceContent.getSize()).thenReturn(1274L); - when(cache.put("url", sourceContent)).thenReturn(true); - - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - when(quota.beforeWritingCacheFile(1274L)).thenReturn(true); - when(quota.afterWritingCacheFile(1274L)).thenReturn(true); - - ContentReader returnedReader = cachingStore.getReader("url"); - - assertSame(returnedReader, cachedContent); - verify(quota).afterWritingCacheFile(1274L); - // Check backing store reader is only acquired once - verify(backingStore, only()).getReader("url"); - } - - - @Test - public void getReaderForItemMissingFromCacheWillGiveUpAfterRetrying() - { - when(cache.getReader("url")).thenThrow(new CacheMissException("url")); - when(backingStore.getReader("url")).thenReturn(sourceContent); - when(cache.put("url", sourceContent)).thenReturn(true); - - ContentReader returnedReader = cachingStore.getReader("url"); - - // Upon failure, item is removed from cache - verify(cache, atLeastOnce()).remove("url"); - - // The content comes direct from the backing store - assertSame(returnedReader, sourceContent); - } - - - @Test - public void getReaderForItemMissingFromCacheWillRetryAndCanSucceed() - { - when(cache.getReader("url")). - thenThrow(new CacheMissException("url")). - thenReturn(cachedContent); - when(backingStore.getReader("url")).thenReturn(sourceContent); - when(cache.put("url", sourceContent)).thenReturn(true); - - ContentReader returnedReader = cachingStore.getReader("url"); - - assertSame(returnedReader, cachedContent); - } - - - @Test - public void getReaderForItemMissingFromCacheButNoContentToCache() - { - when(cache.getReader("url")).thenThrow(new CacheMissException("url")); - when(backingStore.getReader("url")).thenReturn(sourceContent); - when(cache.put("url", sourceContent)).thenReturn(false); - - cachingStore.getReader("url"); - } - - - @Test - // When attempting to read uncached content. - public void quotaManagerCanVetoCacheFileWriting() - { - when(backingStore.getReader("url")).thenReturn(sourceContent); - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - when(sourceContent.getSize()).thenReturn(1274L); - when(quota.beforeWritingCacheFile(1274L)).thenReturn(false); - - ContentReader returnedReader = cachingStore.getReader("url"); - - verify(cache, never()).put("url", sourceContent); - assertSame(returnedReader, sourceContent); - verify(quota, never()).afterWritingCacheFile(anyLong()); - } - - - @Test - public void getWriterWhenNotCacheOnInbound() - { - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - - ContentContext ctx = ContentContext.NULL_CONTEXT; - - cachingStore.getWriter(ctx); - - verify(backingStore).getWriter(ctx); - // No quota manager interaction - as no caching happening. - verify(quota, never()).beforeWritingCacheFile(anyLong()); - verify(quota, never()).afterWritingCacheFile(anyLong()); - } - - - @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); - when(readerFromCacheWriter.exists()).thenReturn(true); - when(cacheWriter.getSize()).thenReturn(54321L); - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - - // Quota manager interceptor is fired. - when(quota.beforeWritingCacheFile(0L)).thenReturn(true); - - 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); - // Post caching quota manager hook is fired. - verify(quota).afterWritingCacheFile(54321L); - } - - - @Test - // When attempting to perform write-through caching, i.e. cacheOnInbound = true - public void quotaManagerCanVetoInboundCaching() - { - cachingStore = new CachingContentStore(backingStore, cache, true); - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - - ContentContext ctx = ContentContext.NULL_CONTEXT; - ContentWriter backingStoreWriter = mock(ContentWriter.class); - when(backingStore.getWriter(ctx)).thenReturn(backingStoreWriter); - when(quota.beforeWritingCacheFile(0L)).thenReturn(false); - - ContentWriter returnedWriter = cachingStore.getWriter(ctx); - - assertSame("Should be writing direct to backing store", backingStoreWriter, returnedWriter); - verify(quota, never()).afterWritingCacheFile(anyLong()); - } - - - @Test - public void quotaManagerCanRequestFileDeletionFromCacheAfterWrite() - { - 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); - when(readerFromCacheWriter.exists()).thenReturn(true); - when(cacheWriter.getSize()).thenReturn(54321L); - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - - // Quota manager interceptor is fired. - when(quota.beforeWritingCacheFile(0L)).thenReturn(true); - - 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()); - - // Don't keep the new cache file - when(quota.afterWritingCacheFile(54321L)).thenReturn(false); - - // Simulate a stream close - arg.getValue().contentStreamClosed(); - // Check behaviour of the listener - verify(bsWriter).putContent(readerFromCacheWriter); - // Post caching quota manager hook is fired. - verify(quota).afterWritingCacheFile(54321L); - // The item should be deleted from the cache (lookup table and content cache file) - verify(cache).deleteFile("url"); - verify(cache).remove("url"); - } - - @Test - public void quotaManagerCanRequestFileDeletionFromCacheAfterWriteWhenNotCacheOnInbound() - { - when(cache.getReader("url")).thenReturn(cachedContent); - when(backingStore.getReader("url")).thenReturn(sourceContent); - when(sourceContent.getSize()).thenReturn(1274L); - when(cache.put("url", sourceContent)).thenReturn(true); - - QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); - cachingStore.setQuota(quota); - - // Don't veto writing the cache file. - when(quota.beforeWritingCacheFile(1274L)).thenReturn(true); - // Do request cache file deletion. - when(quota.afterWritingCacheFile(1234L)).thenReturn(false); - - ContentReader returnedReader = cachingStore.getReader("url"); - - // Was the file deleted? - verify(cache).deleteFile("url"); - verify(cache).remove("url"); - // As the cache file has been deleted, the reader must come from the backing store - // rather than the cache. - assertSame(returnedReader, sourceContent); - } - - @Test(expected=RuntimeException.class) - // Check that exceptions raised by the backing store's putContent(ContentReader) - // aren't swallowed and can therefore cause the transaction to fail. - public void exceptionRaisedWhenCopyingTempToBackingStoreIsPropogatedCorrectly() - 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); - - doThrow(new RuntimeException()).when(bsWriter).putContent(any(ContentReader.class)); - - cachingStore.getWriter(ctx); - - // Get the stream listener and trigger it - ArgumentCaptor arg = ArgumentCaptor.forClass(ContentStreamListener.class); - verify(cacheWriter).addListener(arg.capture()); - // Simulate a stream close - arg.getValue().contentStreamClosed(); - } - - - @Test - public void encodingAttrsCopiedToBackingStoreWriter() - { - 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); - - when(cacheWriter.getEncoding()).thenReturn("UTF-8"); - when(cacheWriter.getLocale()).thenReturn(Locale.UK); - when(cacheWriter.getMimetype()).thenReturn("not/real/mimetype"); - - cachingStore.getWriter(ctx); - - // Get the stream listener and trigger it - ArgumentCaptor arg = ArgumentCaptor.forClass(ContentStreamListener.class); - verify(cacheWriter).addListener(arg.capture()); - // Simulate a stream close - arg.getValue().contentStreamClosed(); - - verify(bsWriter).setEncoding("UTF-8"); - verify(bsWriter).setLocale(Locale.UK); - verify(bsWriter).setMimetype("not/real/mimetype"); - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Tests for spoofed content follow... - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Test - public void spoofedGetReader() - { - cachingStore = new CachingContentStore(backingStore, cache, true); - String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); - ContentReader reader = cachingStore.getReader(url); - assertTrue(reader.exists()); - assertEquals(1024, reader.getSize()); - verify(backingStore, never()).getReader(anyString()); - } - - @Test - public void spoofedDelete() - { - cachingStore = new CachingContentStore(backingStore, cache, true); - String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); - boolean deleted = cachingStore.delete(url); - assertFalse(deleted); - verify(backingStore, never()).delete(anyString()); - } - - @Test - public void spoofedExists() - { - cachingStore = new CachingContentStore(backingStore, cache, true); - String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); - boolean exists = cachingStore.exists(url); - assertTrue(exists); - verify(backingStore, never()).exists(anyString()); - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // 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 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 delegatedDelete() - { - when(backingStore.delete("url")).thenReturn(true); - assertTrue(cachingStore.delete("url")); - - when(backingStore.delete("url")).thenReturn(false); - assertFalse(cachingStore.delete("url")); - } -} +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +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.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Locale; + +import org.alfresco.repo.content.ContentContext; +import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.caching.quota.QuotaManagerStrategy; +import org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategy; +import org.alfresco.repo.content.filestore.SpoofedTextContentReader; +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.service.cmr.repository.DirectAccessUrl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.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; + private ContentReader sourceContent; + private ContentReader cachedContent; + + @Mock + private ContentStore backingStore; + + @Mock + private ContentCache cache; + + + @Before + public void setUp() throws Exception + { + cachingStore = new CachingContentStore(backingStore, cache, false); + cachingStore.setQuota(new UnlimitedQuotaStrategy()); + + sourceContent = mock(ContentReader.class, "sourceContent"); + cachedContent = mock(ContentReader.class, "cachedContent"); + } + + + @Test + public void getReaderForItemInCache() + { + when(cache.contains("url")).thenReturn(true); + when(cache.getReader("url")).thenReturn(cachedContent); + + ContentReader returnedReader = cachingStore.getReader("url"); + + assertSame(returnedReader, cachedContent); + verify(backingStore, never()).getReader(anyString()); + } + + + @Test + // Item isn't in cache, so will be cached and returned. + public void getReaderForItemMissingFromCache() + { + when(cache.getReader("url")).thenReturn(cachedContent); + when(backingStore.getReader("url")).thenReturn(sourceContent); + when(sourceContent.getSize()).thenReturn(1274L); + when(cache.put("url", sourceContent)).thenReturn(true); + + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + when(quota.beforeWritingCacheFile(1274L)).thenReturn(true); + when(quota.afterWritingCacheFile(1274L)).thenReturn(true); + + ContentReader returnedReader = cachingStore.getReader("url"); + + assertSame(returnedReader, cachedContent); + verify(quota).afterWritingCacheFile(1274L); + // Check backing store reader is only acquired once + verify(backingStore, only()).getReader("url"); + } + + + @Test + public void getReaderForItemMissingFromCacheWillGiveUpAfterRetrying() + { + when(cache.getReader("url")).thenThrow(new CacheMissException("url")); + when(backingStore.getReader("url")).thenReturn(sourceContent); + when(cache.put("url", sourceContent)).thenReturn(true); + + ContentReader returnedReader = cachingStore.getReader("url"); + + // Upon failure, item is removed from cache + verify(cache, atLeastOnce()).remove("url"); + + // The content comes direct from the backing store + assertSame(returnedReader, sourceContent); + } + + + @Test + public void getReaderForItemMissingFromCacheWillRetryAndCanSucceed() + { + when(cache.getReader("url")). + thenThrow(new CacheMissException("url")). + thenReturn(cachedContent); + when(backingStore.getReader("url")).thenReturn(sourceContent); + when(cache.put("url", sourceContent)).thenReturn(true); + + ContentReader returnedReader = cachingStore.getReader("url"); + + assertSame(returnedReader, cachedContent); + } + + + @Test + public void getReaderForItemMissingFromCacheButNoContentToCache() + { + when(cache.getReader("url")).thenThrow(new CacheMissException("url")); + when(backingStore.getReader("url")).thenReturn(sourceContent); + when(cache.put("url", sourceContent)).thenReturn(false); + + cachingStore.getReader("url"); + } + + + @Test + // When attempting to read uncached content. + public void quotaManagerCanVetoCacheFileWriting() + { + when(backingStore.getReader("url")).thenReturn(sourceContent); + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + when(sourceContent.getSize()).thenReturn(1274L); + when(quota.beforeWritingCacheFile(1274L)).thenReturn(false); + + ContentReader returnedReader = cachingStore.getReader("url"); + + verify(cache, never()).put("url", sourceContent); + assertSame(returnedReader, sourceContent); + verify(quota, never()).afterWritingCacheFile(anyLong()); + } + + + @Test + public void getWriterWhenNotCacheOnInbound() + { + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + + ContentContext ctx = ContentContext.NULL_CONTEXT; + + cachingStore.getWriter(ctx); + + verify(backingStore).getWriter(ctx); + // No quota manager interaction - as no caching happening. + verify(quota, never()).beforeWritingCacheFile(anyLong()); + verify(quota, never()).afterWritingCacheFile(anyLong()); + } + + + @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); + when(readerFromCacheWriter.exists()).thenReturn(true); + when(cacheWriter.getSize()).thenReturn(54321L); + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + + // Quota manager interceptor is fired. + when(quota.beforeWritingCacheFile(0L)).thenReturn(true); + + 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); + // Post caching quota manager hook is fired. + verify(quota).afterWritingCacheFile(54321L); + } + + + @Test + // When attempting to perform write-through caching, i.e. cacheOnInbound = true + public void quotaManagerCanVetoInboundCaching() + { + cachingStore = new CachingContentStore(backingStore, cache, true); + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + + ContentContext ctx = ContentContext.NULL_CONTEXT; + ContentWriter backingStoreWriter = mock(ContentWriter.class); + when(backingStore.getWriter(ctx)).thenReturn(backingStoreWriter); + when(quota.beforeWritingCacheFile(0L)).thenReturn(false); + + ContentWriter returnedWriter = cachingStore.getWriter(ctx); + + assertSame("Should be writing direct to backing store", backingStoreWriter, returnedWriter); + verify(quota, never()).afterWritingCacheFile(anyLong()); + } + + + @Test + public void quotaManagerCanRequestFileDeletionFromCacheAfterWrite() + { + 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); + when(readerFromCacheWriter.exists()).thenReturn(true); + when(cacheWriter.getSize()).thenReturn(54321L); + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + + // Quota manager interceptor is fired. + when(quota.beforeWritingCacheFile(0L)).thenReturn(true); + + 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()); + + // Don't keep the new cache file + when(quota.afterWritingCacheFile(54321L)).thenReturn(false); + + // Simulate a stream close + arg.getValue().contentStreamClosed(); + // Check behaviour of the listener + verify(bsWriter).putContent(readerFromCacheWriter); + // Post caching quota manager hook is fired. + verify(quota).afterWritingCacheFile(54321L); + // The item should be deleted from the cache (lookup table and content cache file) + verify(cache).deleteFile("url"); + verify(cache).remove("url"); + } + + @Test + public void quotaManagerCanRequestFileDeletionFromCacheAfterWriteWhenNotCacheOnInbound() + { + when(cache.getReader("url")).thenReturn(cachedContent); + when(backingStore.getReader("url")).thenReturn(sourceContent); + when(sourceContent.getSize()).thenReturn(1274L); + when(cache.put("url", sourceContent)).thenReturn(true); + + QuotaManagerStrategy quota = mock(QuotaManagerStrategy.class); + cachingStore.setQuota(quota); + + // Don't veto writing the cache file. + when(quota.beforeWritingCacheFile(1274L)).thenReturn(true); + // Do request cache file deletion. + when(quota.afterWritingCacheFile(1234L)).thenReturn(false); + + ContentReader returnedReader = cachingStore.getReader("url"); + + // Was the file deleted? + verify(cache).deleteFile("url"); + verify(cache).remove("url"); + // As the cache file has been deleted, the reader must come from the backing store + // rather than the cache. + assertSame(returnedReader, sourceContent); + } + + @Test(expected=RuntimeException.class) + // Check that exceptions raised by the backing store's putContent(ContentReader) + // aren't swallowed and can therefore cause the transaction to fail. + public void exceptionRaisedWhenCopyingTempToBackingStoreIsPropogatedCorrectly() + 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); + + doThrow(new RuntimeException()).when(bsWriter).putContent(any(ContentReader.class)); + + cachingStore.getWriter(ctx); + + // Get the stream listener and trigger it + ArgumentCaptor arg = ArgumentCaptor.forClass(ContentStreamListener.class); + verify(cacheWriter).addListener(arg.capture()); + // Simulate a stream close + arg.getValue().contentStreamClosed(); + } + + + @Test + public void encodingAttrsCopiedToBackingStoreWriter() + { + 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); + + when(cacheWriter.getEncoding()).thenReturn("UTF-8"); + when(cacheWriter.getLocale()).thenReturn(Locale.UK); + when(cacheWriter.getMimetype()).thenReturn("not/real/mimetype"); + + cachingStore.getWriter(ctx); + + // Get the stream listener and trigger it + ArgumentCaptor arg = ArgumentCaptor.forClass(ContentStreamListener.class); + verify(cacheWriter).addListener(arg.capture()); + // Simulate a stream close + arg.getValue().contentStreamClosed(); + + verify(bsWriter).setEncoding("UTF-8"); + verify(bsWriter).setLocale(Locale.UK); + verify(bsWriter).setMimetype("not/real/mimetype"); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for spoofed content follow... + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void spoofedGetReader() + { + cachingStore = new CachingContentStore(backingStore, cache, true); + String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); + ContentReader reader = cachingStore.getReader(url); + assertTrue(reader.exists()); + assertEquals(1024, reader.getSize()); + verify(backingStore, never()).getReader(anyString()); + } + + @Test + public void spoofedDelete() + { + cachingStore = new CachingContentStore(backingStore, cache, true); + String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); + boolean deleted = cachingStore.delete(url); + assertFalse(deleted); + verify(backingStore, never()).delete(anyString()); + } + + @Test + public void spoofedExists() + { + cachingStore = new CachingContentStore(backingStore, cache, true); + String url = SpoofedTextContentReader.createContentUrl(Locale.ENGLISH, 0L, 1024L); + boolean exists = cachingStore.exists(url); + assertTrue(exists); + verify(backingStore, never()).exists(anyString()); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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 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 delegatedDelete() + { + when(backingStore.delete("url")).thenReturn(true); + assertTrue(cachingStore.delete("url")); + + when(backingStore.delete("url")).thenReturn(false); + assertFalse(cachingStore.delete("url")); + } + + @Test + public void isDirectAccessSupported() + { + assertFalse(cachingStore.isDirectAccessSupported()); + + when(backingStore.isDirectAccessSupported()).thenReturn(true); + assertTrue(cachingStore.isDirectAccessSupported()); + } + + @Test + public void getDirectAccessUrlUnsupported() + { + try + { + when(backingStore.getDirectAccessUrl(anyString(), any())).thenThrow(new UnsupportedOperationException()); + cachingStore.getDirectAccessUrl("url", null); + fail(); + } + catch (UnsupportedOperationException e) + { + // expected + } + } + + @Test + public void getDirectAccessUrl() + { + when(backingStore.getDirectAccessUrl(anyString(), any())).thenReturn(new DirectAccessUrl()); + cachingStore.getDirectAccessUrl("url", null); + } +} diff --git a/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java b/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java index 7b78a1368d..b98bcd8fe4 100644 --- a/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java +++ b/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java @@ -23,8 +23,8 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.content.replication; - +package org.alfresco.repo.content.replication; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -32,136 +32,280 @@ import java.util.List; import org.alfresco.repo.content.AbstractWritableContentStoreTest; import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.UnsupportedContentUrlException; import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.DirectAccessUrl; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.GUID; import org.alfresco.util.TempFileProvider; import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; 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.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; - -/** - * Tests read and write functionality for the aggregating store. - *

- * - * @see org.alfresco.repo.content.replication.AggregatingContentStore - * - * @author Derek Hulley - * @author Mark Rogers - */ -@Category(OwnJVMTestsCategory.class) -public class AggregatingContentStoreTest extends AbstractWritableContentStoreTest -{ - private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency"; - - private AggregatingContentStore aggregatingStore; - private ContentStore primaryStore; - private List secondaryStores; - - @Before - public void before() throws Exception - { - File tempDir = TempFileProvider.getTempDir(); - // create a primary file store - String storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate(); - primaryStore = new FileContentStore(ctx, storeDir); - // create some secondary file stores - secondaryStores = new ArrayList(3); - for (int i = 0; i < 4; i++) - { - storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate(); - FileContentStore store = new FileContentStore(ctx, storeDir); - secondaryStores.add(store); - } - // Create the aggregating store - aggregatingStore = new AggregatingContentStore(); - aggregatingStore.setPrimaryStore(primaryStore); - aggregatingStore.setSecondaryStores(secondaryStores); - } - - @Override - public ContentStore getStore() - { - return aggregatingStore; - } - - /** - * 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. - * - * @return - * Returns a writer for new content - */ - protected ContentWriter getWriter() - { - ContentStore store = getStore(); - return store.getWriter(ContentStore.NEW_CONTENT_CONTEXT); - } - - - /** - * {@inheritDoc} - *

- * This implementation creates some content in the store and returns the new content URL. - */ - protected String getExistingContentUrl() - { - ContentWriter writer = getWriter(); - writer.putContent("Content for getExistingContentUrl"); - return writer.getContentUrl(); - } - - 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 not exist - */ - private void checkForUrl(String contentUrl, boolean mustExist) +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests read and write functionality for the aggregating store. + *

+ * + * @see org.alfresco.repo.content.replication.AggregatingContentStore + * + * @author Derek Hulley + * @author Mark Rogers + */ +@Category(OwnJVMTestsCategory.class) +public class AggregatingContentStoreTest extends AbstractWritableContentStoreTest +{ + private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency"; + + private AggregatingContentStore aggregatingStore; + private ContentStore primaryStore; + private List secondaryStores; + + @Mock + ContentStore primaryStoreMock; + @Mock + ContentStore secondaryStoreMock; + @Mock + AggregatingContentStore aggregatingContentStoreMock; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Before + public void before() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + // create a primary file store + String storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate(); + primaryStore = new FileContentStore(ctx, storeDir); + // create some secondary file stores + secondaryStores = new ArrayList(3); + for (int i = 0; i < 4; i++) + { + storeDir = tempDir.getAbsolutePath() + File.separatorChar + GUID.generate(); + FileContentStore store = new FileContentStore(ctx, storeDir); + secondaryStores.add(store); + } + // Create the aggregating store + aggregatingStore = new AggregatingContentStore(); + aggregatingStore.setPrimaryStore(primaryStore); + aggregatingStore.setSecondaryStores(secondaryStores); + } + + @Override + public ContentStore getStore() + { + return aggregatingStore; + } + + /** + * 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. + * + * @return + * Returns a writer for new content + */ + protected ContentWriter getWriter() + { + ContentStore store = getStore(); + return store.getWriter(ContentStore.NEW_CONTENT_CONTEXT); + } + + + /** + * {@inheritDoc} + *

+ * This implementation creates some content in the store and returns the new content URL. + */ + protected String getExistingContentUrl() + { + ContentWriter writer = getWriter(); + writer.putContent("Content for getExistingContentUrl"); + return writer.getContentUrl(); + } + + 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 not exist + */ + private void checkForUrl(String contentUrl, boolean mustExist) { ContentReader reader = getReader(contentUrl); - assertEquals("Reader state differs from expected: " + reader, mustExist, reader.exists()); - } - - public void testDelete() throws Exception - { - - // write some content - ContentWriter writer = getWriter(); - writer.putContent(SOME_CONTENT); - String contentUrl = writer.getContentUrl(); - - ContentReader reader = primaryStore.getReader(contentUrl); - assertTrue("Content was not in the primary store", reader.exists()); - assertEquals("The content was incorrect", SOME_CONTENT, reader.getContentString()); - - getStore().delete(contentUrl); - checkForUrl(contentUrl, false); - } - - public void testReadFromSecondaryStore() - { - // pick a secondary store and write some content to it - ContentStore secondaryStore = secondaryStores.get(2); - ContentWriter writer = secondaryStore.getWriter(ContentContext.NULL_CONTEXT); - writer.putContent(SOME_CONTENT); - String contentUrl = writer.getContentUrl(); - - checkForUrl(contentUrl, true); - } - - -} + assertEquals("Reader state differs from expected: " + reader, mustExist, reader.exists()); + } + + public void testDelete() throws Exception + { + + // write some content + ContentWriter writer = getWriter(); + writer.putContent(SOME_CONTENT); + String contentUrl = writer.getContentUrl(); + + ContentReader reader = primaryStore.getReader(contentUrl); + assertTrue("Content was not in the primary store", reader.exists()); + assertEquals("The content was incorrect", SOME_CONTENT, reader.getContentString()); + + getStore().delete(contentUrl); + checkForUrl(contentUrl, false); + } + + public void testReadFromSecondaryStore() + { + // pick a secondary store and write some content to it + ContentStore secondaryStore = secondaryStores.get(2); + ContentWriter writer = secondaryStore.getWriter(ContentContext.NULL_CONTEXT); + writer.putContent(SOME_CONTENT); + String contentUrl = writer.getContentUrl(); + + 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); + } +}