diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 7aada6efe8..b5d7d4148e 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -409,6 +409,7 @@ + org.alfresco.service.cmr.repository.ContentService.getRawReader=ACL_METHOD.ROLE_ADMINISTRATOR org.alfresco.service.cmr.repository.ContentService.getReader=ACL_NODE.0.sys:base.ReadContent org.alfresco.service.cmr.repository.ContentService.getWriter=ACL_NODE.0.sys:base.WriteContent org.alfresco.service.cmr.repository.ContentService.isTransformable=ACL_ALLOW diff --git a/source/java/org/alfresco/repo/cache/CacheTest.java b/source/java/org/alfresco/repo/cache/CacheTest.java index 91c476ca45..c1724257ca 100644 --- a/source/java/org/alfresco/repo/cache/CacheTest.java +++ b/source/java/org/alfresco/repo/cache/CacheTest.java @@ -55,6 +55,7 @@ public class CacheTest extends TestCase private SimpleCache standaloneCache; private SimpleCache backingCache; private SimpleCache transactionalCache; + private SimpleCache objectCache; @SuppressWarnings("unchecked") @Override @@ -64,6 +65,7 @@ public class CacheTest extends TestCase standaloneCache = (SimpleCache) ctx.getBean("ehCache1"); backingCache = (SimpleCache) ctx.getBean("backingCache"); transactionalCache = (SimpleCache) ctx.getBean("transactionalCache"); + objectCache = (SimpleCache) ctx.getBean("objectCache"); } @Override @@ -86,6 +88,14 @@ public class CacheTest extends TestCase assertNotNull(backingCache); assertNotNull(standaloneCache); assertNotNull(transactionalCache); + assertNotNull(objectCache); + } + + public void testObjectCache() throws Exception + { + objectCache.put("A", this); + Object obj = objectCache.get("A"); + assertTrue("Object not cached properly", this == obj); } public void testEhcacheAdaptors() throws Exception diff --git a/source/java/org/alfresco/repo/cache/EhCacheAdapter.java b/source/java/org/alfresco/repo/cache/EhCacheAdapter.java index f1bf82f816..0f070ed59e 100644 --- a/source/java/org/alfresco/repo/cache/EhCacheAdapter.java +++ b/source/java/org/alfresco/repo/cache/EhCacheAdapter.java @@ -87,7 +87,7 @@ public class EhCacheAdapter Element element = cache.get(key); if (element != null) { - return (V) element.getValue(); + return (V) element.getObjectValue(); } else { @@ -97,7 +97,8 @@ public class EhCacheAdapter catch (CacheException e) { throw new AlfrescoRuntimeException("Failed to get from EhCache: \n" + - " key: " + key); + " key: " + key, + e); } } diff --git a/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java b/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java index 0c19675f41..367902b56d 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java +++ b/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java @@ -102,7 +102,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase */ protected final ContentWriter getWriter() { - return getStore().getWriter(null, contentUrl); + ContentContext contentCtx = new ContentContext(null, contentUrl); + return getStore().getWriter(contentCtx); } /** @@ -187,7 +188,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase assertFalse("Reader exists failure", reader.exists()); // write something - ContentWriter writer = store.getWriter(null, contentUrl); + ContentContext contentContext = new ContentContext(null, contentUrl); + ContentWriter writer = store.getWriter(contentContext); writer.putContent("ABC"); assertTrue("Store exists should show URL to be present", store.exists(contentUrl)); @@ -298,10 +300,11 @@ public abstract class AbstractContentReadWriteTest extends TestCase String contentUrl = AbstractContentStore.createNewUrl(); ContentStore store = getStore(); - ContentWriter firstWriter = store.getWriter(null, contentUrl); + ContentContext contentCtx = new ContentContext(null, contentUrl); + ContentWriter firstWriter = store.getWriter(contentCtx); try { - ContentWriter secondWriter = store.getWriter(null, contentUrl); + ContentWriter secondWriter = store.getWriter(contentCtx); fail("Store issued two writers for the same URL: " + store); } catch (ContentIOException e) @@ -620,7 +623,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase } // get a new writer from the store, using the existing content and perform a truncation check - ContentWriter writerTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl()); + ContentContext writerTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl()); + ContentWriter writerTruncate = getStore().getWriter(writerTruncateCtx); assertEquals("Content size incorrect", 0, writerTruncate.getSize()); // get the channel with truncation FileChannel fcTruncate = writerTruncate.getFileChannel(true); @@ -628,7 +632,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase assertEquals("Content not truncated", 0, writerTruncate.getSize()); // get a new writer from the store, using the existing content and perform a non-truncation check - ContentWriter writerNoTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl()); + ContentContext writerNoTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl()); + ContentWriter writerNoTruncate = getStore().getWriter(writerNoTruncateCtx); assertEquals("Content size incorrect", 0, writerNoTruncate.getSize()); // get the channel without truncation FileChannel fcNoTruncate = writerNoTruncate.getFileChannel(false); diff --git a/source/java/org/alfresco/repo/content/AbstractContentStore.java b/source/java/org/alfresco/repo/content/AbstractContentStore.java index c1e4fc9ce1..e1d2d4ec18 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentStore.java +++ b/source/java/org/alfresco/repo/content/AbstractContentStore.java @@ -86,6 +86,10 @@ public abstract class AbstractContentStore implements ContentStore * @param contentUrl a URL of the content to check * @return Returns the relative part of the URL * @throws RuntimeException if the URL is not correct + * + * @deprecated Stores can really have any prefix in the URL. This method was + * really specific to the FileContentStore and has been moved into + * it. */ public static String getRelativePart(String contentUrl) throws RuntimeException { diff --git a/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java b/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java index 481719e021..6c4578c960 100644 --- a/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java +++ b/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java @@ -83,7 +83,8 @@ public abstract class AbstractRoutingContentStore implements ContentStore * to make a decision. * * @param ctx the context to use to make the choice - * @return Returns the store most appropriate for the given context + * @return Returns the store most appropriate for the given context and + * never null */ protected abstract ContentStore selectWriteStore(ContentContext ctx); @@ -132,9 +133,19 @@ public abstract class AbstractRoutingContentStore implements ContentStore List stores = getAllStores(); for (ContentStore storeInList : stores) { - if (!store.exists(contentUrl)) + boolean exists = false; + try { - // It is not in the store + exists = storeInList.exists(contentUrl); + if (!exists) + { + // It is not in the store + continue; + } + } + catch (Throwable e) + { + // The API used to allow failure when the URL wasn't there continue; } // We found one @@ -264,6 +275,10 @@ public abstract class AbstractRoutingContentStore implements ContentStore { // Select the store for writing ContentStore store = selectWriteStore(context); + if (store == null) + { + throw new NullPointerException("Unable to find a writer. 'selectWriteStore' may not return null."); + } ContentWriter writer = store.getWriter(context); // Cache the store against the URL storesCacheWriteLock.lock(); diff --git a/source/java/org/alfresco/repo/content/ContentStore.java b/source/java/org/alfresco/repo/content/ContentStore.java index d8d379e55b..45658c25d3 100644 --- a/source/java/org/alfresco/repo/content/ContentStore.java +++ b/source/java/org/alfresco/repo/content/ContentStore.java @@ -71,8 +71,10 @@ public interface ContentStore * reader to {@link ContentReader#exists() check for existence}, although * that check should also be performed. * - * @param contentUrl the path to the content - * @return Returns true if the content exists. + * @param contentUrl the path to the content + * @return Returns true if the content exists, otherwise + * false if the content doesn't exist or if the URL + * is not applicable to this store. * @throws ContentIOException * * @see ContentReader#exists() @@ -90,6 +92,7 @@ public interface ContentStore * * @see #exists(String) * @see ContentReader#exists() + * @see EmptyContentReader */ public ContentReader getReader(String contentUrl) throws ContentIOException; diff --git a/source/java/org/alfresco/repo/content/ContentTestSuite.java b/source/java/org/alfresco/repo/content/ContentTestSuite.java index 9dcf5b038a..80ffb0df94 100644 --- a/source/java/org/alfresco/repo/content/ContentTestSuite.java +++ b/source/java/org/alfresco/repo/content/ContentTestSuite.java @@ -84,6 +84,7 @@ public class ContentTestSuite extends TestSuite suite.addTestSuite(ContentDataTest.class); suite.addTestSuite(MimetypeMapTest.class); suite.addTestSuite(RoutingContentServiceTest.class); + suite.addTestSuite(RoutingContentStoreTest.class); return suite; } diff --git a/source/java/org/alfresco/repo/content/RoutingContentService.java b/source/java/org/alfresco/repo/content/RoutingContentService.java index f6f5c91860..30cc9c0c37 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentService.java +++ b/source/java/org/alfresco/repo/content/RoutingContentService.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; import org.alfresco.repo.avm.AVMNodeConverter; import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; @@ -267,7 +268,31 @@ public class RoutingContentService implements ContentService policy.onContentUpdate(nodeRef, newContent); } } - + + /** {@inheritDoc} */ + public ContentReader getRawReader(String contentUrl) + { + ContentReader reader = store.getReader(contentUrl); + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); + } + // set extra data on the reader + reader.setMimetype(MimetypeMap.MIMETYPE_BINARY); + reader.setEncoding("UTF-8"); + reader.setLocale(I18NUtil.getLocale()); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Direct request for reader: \n" + + " Content URL: " + contentUrl + "\n" + + " Reader: " + reader); + } + return reader; + } + public ContentReader getReader(NodeRef nodeRef, QName propertyQName) { return getReader(nodeRef, propertyQName, true); @@ -318,6 +343,10 @@ public class RoutingContentService implements ContentService // The context of the read is entirely described by the URL ContentReader reader = store.getReader(contentUrl); + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); + } // set extra data on the reader reader.setMimetype(contentData.getMimetype()); diff --git a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java index 313eaa765f..85c662a63a 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java +++ b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java @@ -269,6 +269,21 @@ public class RoutingContentServiceTest extends TestCase assertNull("Reader must be null if the content URL is null", reader); } + public void testGetRawReader() throws Exception + { + ContentReader reader = contentService.getRawReader("blah"); + assertNotNull("A reader is expected with content URL referencing no content", reader); + assertFalse("Reader should not have any content", reader.exists()); + // Now write something + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false); + writer.putContent("ABC from " + getName()); + // Try again + String contentUrl = writer.getContentUrl(); + reader = contentService.getRawReader(contentUrl); + assertNotNull("Expected reader for live, raw content", reader); + assertEquals("Content sizes don't match", writer.getSize(), reader.getSize()); + } + /** * Checks what happens when the physical content disappears */ diff --git a/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java b/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java new file mode 100644 index 0000000000..e9d4466177 --- /dev/null +++ b/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.content; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import junit.framework.TestCase; +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; + +import org.alfresco.repo.cache.EhCacheAdapter; +import org.alfresco.repo.content.filestore.FileContentStore; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; + +/** + * Ensures that the routing of URLs based on context is working. + * + * @see AbstractRoutingContentStore + * @since 2.1 + * + * @author Derek Hulley + */ +public class RoutingContentStoreTest extends TestCase +{ + private ContentStore storeA; + private ContentStore storeB; + private ContentStore routingStore; + + @Override + protected void setUp() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + // Create a subdirectory for A + File storeADir = new File(tempDir, "A"); + storeA = new FileContentStore(storeADir); + // Create a subdirectory for B + File storeBDir = new File(tempDir, "B"); + storeB = new FileContentStore(storeBDir); + // Create the routing store + routingStore = new RandomRoutingContentStore(storeA, storeB); + } + + public void testSetUp() throws Exception + { + assertNotNull(storeA); + assertNotNull(storeB); + assertNotNull(routingStore); + } + + private void checkForContent(String contentUrl, String content) + { + for (ContentStore store : new ContentStore[] {storeA, storeB}) + { + // Does the store have it + if (store.exists(contentUrl)) + { + // Check it + ContentReader reader = store.getReader(contentUrl); + String checkContent = reader.getContentString(); + assertEquals("Content found but is incorrect", content, checkContent); + return; + } + } + fail("Content not found in any of the stores: " + contentUrl); + } + + /** + * Checks that requests for missing content URLs are served. + */ + public void testMissingUrl() + { + ContentReader reader = routingStore.getReader("blah"); + assertNotNull("Missing URL should not return null", reader); + assertFalse("Empty reader should say content doesn't exist.", reader.exists()); + try + { + reader.getContentString(); + fail("Empty reader cannot return content."); + } + catch (Throwable e) + { + // Expected + } + } + + public void testHandlingInCache() + { + for (int i = 0 ; i < 20; i++) + { + ContentContext contentContext = new ContentContext(null, null); + ContentWriter writer = routingStore.getWriter(contentContext); + String content = "This was generated by " + this.getClass().getName() + "#" + getName() + " number " + i; + writer.putContent(content); + // Check that it exists + String contentUrl = writer.getContentUrl(); + checkForContent(contentUrl, content); + + // Now go direct to the routing store and check that it is able to find the appropriate URLs + ContentReader reader = routingStore.getReader(contentUrl); + assertNotNull("Null reader returned", reader); + assertTrue("Reader should be onto live content", reader.exists()); + } + } + + /** + * Checks that content URLs are matched to the appropriate stores when in the cache limit. + */ + public void testReadFindInCache() + { + + } + + /** + * A test routing store that directs content writes to a randomly-chosen store. + * Matching of content URLs back to the stores is handled by the base class. + * + * @author Derek Hulley + */ + private static class RandomRoutingContentStore extends AbstractRoutingContentStore + { + private List stores; + private Random random; + + public RandomRoutingContentStore(ContentStore ... stores) + { + this.random = new Random(); + this.stores = new ArrayList(5); + for (ContentStore store : stores) + { + this.stores.add(store); + } + Cache ehCache = new Cache("RandomRoutingContentStore", 50, false, true, 0L, 0L); + CacheManager cacheManager = new CacheManager(); + cacheManager.addCache(ehCache); + EhCacheAdapter cache = new EhCacheAdapter(); + cache.setCache(ehCache); + super.setStoresCache(cache); + } + + @Override + protected List getAllStores() + { + return stores; + } + + @Override + protected ContentStore selectWriteStore(ContentContext ctx) + { + int size = stores.size(); + int index = (int) Math.floor(random.nextDouble() * (double) size); + return stores.get(index); + } + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java index 3c8d0ba09f..e2c3321445 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java @@ -57,12 +57,22 @@ public class FileContentStore extends AbstractContentStore private boolean allowRandomAccess; /** - * @param rootDirectory the root under which files will be stored. The - * directory will be created if it does not exist. + * @param rootDirectoryStr the root under which files will be stored. + * The directory will be created if it does not exist. + * + * @see FileContentStore#FileContentStore(File) */ public FileContentStore(String rootDirectoryStr) { - rootDirectory = new File(rootDirectoryStr); + this(new File(rootDirectoryStr)); + } + + /** + * @param rootDirectory the root under which files will be stored. + * The directory will be created if it does not exist. + */ + public FileContentStore(File rootDirectory) + { if (!rootDirectory.exists()) { if (!rootDirectory.mkdirs()) @@ -70,11 +80,11 @@ public class FileContentStore extends AbstractContentStore throw new ContentIOException("Failed to create store root: " + rootDirectory, null); } } - rootDirectory = rootDirectory.getAbsoluteFile(); + this.rootDirectory = rootDirectory.getAbsoluteFile(); rootAbsolutePath = rootDirectory.getAbsolutePath(); allowRandomAccess = true; } - + public String toString() { StringBuilder sb = new StringBuilder(36); @@ -183,8 +193,7 @@ public class FileContentStore extends AbstractContentStore } /** - * Creates a file from the given relative URL. The URL must start with - * the required {@link FileContentStore#STORE_PROTOCOL protocol prefix}. + * Creates a file from the given relative URL. * * @param contentUrl the content URL including the protocol prefix * @return Returns a file representing the URL - the file may or may not @@ -195,7 +204,14 @@ public class FileContentStore extends AbstractContentStore private File makeFile(String contentUrl) { // take just the part after the protocol - String relativeUrl = getRelativePart(contentUrl); + String relativeUrl = FileContentStore.getRelativePart(contentUrl); + if (relativeUrl == null) + { + throw new ContentIOException( + "The content URL is not valid for this store: \n" + + " Store: " + this + "\n" + + " Content URL: " + contentUrl); + } // get the file File file = new File(rootDirectory, relativeUrl); // done @@ -366,4 +382,47 @@ public class FileContentStore extends AbstractContentStore } return deleted; } + + /** + * This method can be used to ensure that URLs conform to the required format. + * If subclasses have to parse the URL, then a call to this may not be required - + * provided that the format is checked. + *

+ * The protocol part of the URL (including legacy protocols) + * is stripped out and just the relative path is returned. If no known prefix is + * found, or if the relative part is empty, then null is returned. + * + * @param contentUrl a URL of the content to check + * @return Returns the relative part of the URL. If there is no + * prefix, then the URL is assumed to be the relative part. + */ + public static String getRelativePart(String contentUrl) + { + int index = 0; + if (contentUrl.startsWith(STORE_PROTOCOL)) + { + index = 8; + } + else if (contentUrl.startsWith("file://")) + { + index = 7; + } + else + { + if (contentUrl.length() == 0) + { + throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl); + } + return contentUrl; + } + + // extract the relative part of the URL + String path = contentUrl.substring(index); + // more extensive checks can be added in, but it seems overkill + if (path.length() == 0) + { + throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl); + } + return path; + } } diff --git a/source/java/org/alfresco/service/cmr/repository/ContentService.java b/source/java/org/alfresco/service/cmr/repository/ContentService.java index d56e56e16f..03bc970d16 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentService.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentService.java @@ -54,6 +54,21 @@ import org.alfresco.service.namespace.QName; @PublicService public interface ContentService { + /** + * Fetch content from the low-level stores using a content URL. None of the + * metadata associated with the content will be populated. This method should + * be used only to stream the binary data out when no other metadata is + * required. + *

+ * null is never returned, but the reader should always be checked for + * {@link ContentReader#exists() existence}. + * + * @param contentUrl a content store URL + * @return Returns a reader for the URL that needs to be checked. + */ + @Auditable(key = Auditable.Key.ARG_0, parameters = {"contentUrl"}) + public ContentReader getRawReader(String contentUrl); + /** * Gets a reader for the content associated with the given node property. *

diff --git a/source/test-resources/cache-test-context.xml b/source/test-resources/cache-test-context.xml index a86ca6b368..88f12b190b 100644 --- a/source/test-resources/cache-test-context.xml +++ b/source/test-resources/cache-test-context.xml @@ -45,4 +45,14 @@ + + + + + objectCache + + + + + \ No newline at end of file