Added a raw download servlet at URL http://.../alfresco/dr?contentUrl=...?ticket=...

Added ContentService.getRawReader to get content directly using a content URL.  To access this, you need to be admin.
Fixed EHCacheAdapter to handle non-Serializable values.
Added tests for above and for AbstractRoutingContentStore.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5841 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2007-06-04 14:44:29 +00:00
parent c2b6a11cd7
commit 6ff0696bf9
14 changed files with 371 additions and 22 deletions

View File

@@ -409,6 +409,7 @@
<property name="afterInvocationManager"><ref local="afterInvocationManager"/></property> <property name="afterInvocationManager"><ref local="afterInvocationManager"/></property>
<property name="objectDefinitionSource"> <property name="objectDefinitionSource">
<value> <value>
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.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.getWriter=ACL_NODE.0.sys:base.WriteContent
org.alfresco.service.cmr.repository.ContentService.isTransformable=ACL_ALLOW org.alfresco.service.cmr.repository.ContentService.isTransformable=ACL_ALLOW

View File

@@ -55,6 +55,7 @@ public class CacheTest extends TestCase
private SimpleCache<String, Serializable> standaloneCache; private SimpleCache<String, Serializable> standaloneCache;
private SimpleCache<String, Serializable> backingCache; private SimpleCache<String, Serializable> backingCache;
private SimpleCache<String, Serializable> transactionalCache; private SimpleCache<String, Serializable> transactionalCache;
private SimpleCache<String, Object> objectCache;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
@@ -64,6 +65,7 @@ public class CacheTest extends TestCase
standaloneCache = (SimpleCache<String, Serializable>) ctx.getBean("ehCache1"); standaloneCache = (SimpleCache<String, Serializable>) ctx.getBean("ehCache1");
backingCache = (SimpleCache<String, Serializable>) ctx.getBean("backingCache"); backingCache = (SimpleCache<String, Serializable>) ctx.getBean("backingCache");
transactionalCache = (SimpleCache<String, Serializable>) ctx.getBean("transactionalCache"); transactionalCache = (SimpleCache<String, Serializable>) ctx.getBean("transactionalCache");
objectCache = (SimpleCache<String, Object>) ctx.getBean("objectCache");
} }
@Override @Override
@@ -86,6 +88,14 @@ public class CacheTest extends TestCase
assertNotNull(backingCache); assertNotNull(backingCache);
assertNotNull(standaloneCache); assertNotNull(standaloneCache);
assertNotNull(transactionalCache); 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 public void testEhcacheAdaptors() throws Exception

View File

@@ -87,7 +87,7 @@ public class EhCacheAdapter<K extends Serializable, V extends Object>
Element element = cache.get(key); Element element = cache.get(key);
if (element != null) if (element != null)
{ {
return (V) element.getValue(); return (V) element.getObjectValue();
} }
else else
{ {
@@ -97,7 +97,8 @@ public class EhCacheAdapter<K extends Serializable, V extends Object>
catch (CacheException e) catch (CacheException e)
{ {
throw new AlfrescoRuntimeException("Failed to get from EhCache: \n" + throw new AlfrescoRuntimeException("Failed to get from EhCache: \n" +
" key: " + key); " key: " + key,
e);
} }
} }

View File

@@ -102,7 +102,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase
*/ */
protected final ContentWriter getWriter() 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()); assertFalse("Reader exists failure", reader.exists());
// write something // write something
ContentWriter writer = store.getWriter(null, contentUrl); ContentContext contentContext = new ContentContext(null, contentUrl);
ContentWriter writer = store.getWriter(contentContext);
writer.putContent("ABC"); writer.putContent("ABC");
assertTrue("Store exists should show URL to be present", store.exists(contentUrl)); 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(); String contentUrl = AbstractContentStore.createNewUrl();
ContentStore store = getStore(); ContentStore store = getStore();
ContentWriter firstWriter = store.getWriter(null, contentUrl); ContentContext contentCtx = new ContentContext(null, contentUrl);
ContentWriter firstWriter = store.getWriter(contentCtx);
try try
{ {
ContentWriter secondWriter = store.getWriter(null, contentUrl); ContentWriter secondWriter = store.getWriter(contentCtx);
fail("Store issued two writers for the same URL: " + store); fail("Store issued two writers for the same URL: " + store);
} }
catch (ContentIOException e) 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 // 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()); assertEquals("Content size incorrect", 0, writerTruncate.getSize());
// get the channel with truncation // get the channel with truncation
FileChannel fcTruncate = writerTruncate.getFileChannel(true); FileChannel fcTruncate = writerTruncate.getFileChannel(true);
@@ -628,7 +632,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase
assertEquals("Content not truncated", 0, writerTruncate.getSize()); assertEquals("Content not truncated", 0, writerTruncate.getSize());
// get a new writer from the store, using the existing content and perform a non-truncation check // 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()); assertEquals("Content size incorrect", 0, writerNoTruncate.getSize());
// get the channel without truncation // get the channel without truncation
FileChannel fcNoTruncate = writerNoTruncate.getFileChannel(false); FileChannel fcNoTruncate = writerNoTruncate.getFileChannel(false);

View File

@@ -86,6 +86,10 @@ public abstract class AbstractContentStore implements ContentStore
* @param contentUrl a URL of the content to check * @param contentUrl a URL of the content to check
* @return Returns the relative part of the URL * @return Returns the relative part of the URL
* @throws RuntimeException if the URL is not correct * @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 public static String getRelativePart(String contentUrl) throws RuntimeException
{ {

View File

@@ -83,7 +83,8 @@ public abstract class AbstractRoutingContentStore implements ContentStore
* to make a decision. * to make a decision.
* *
* @param ctx the context to use to make the choice * @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
* <b>never <tt>null</tt></b>
*/ */
protected abstract ContentStore selectWriteStore(ContentContext ctx); protected abstract ContentStore selectWriteStore(ContentContext ctx);
@@ -132,9 +133,19 @@ public abstract class AbstractRoutingContentStore implements ContentStore
List<ContentStore> stores = getAllStores(); List<ContentStore> stores = getAllStores();
for (ContentStore storeInList : stores) 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; continue;
} }
// We found one // We found one
@@ -264,6 +275,10 @@ public abstract class AbstractRoutingContentStore implements ContentStore
{ {
// Select the store for writing // Select the store for writing
ContentStore store = selectWriteStore(context); 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); ContentWriter writer = store.getWriter(context);
// Cache the store against the URL // Cache the store against the URL
storesCacheWriteLock.lock(); storesCacheWriteLock.lock();

View File

@@ -71,8 +71,10 @@ public interface ContentStore
* reader to {@link ContentReader#exists() check for existence}, although * reader to {@link ContentReader#exists() check for existence}, although
* that check should also be performed. * that check should also be performed.
* *
* @param contentUrl the path to the content * @param contentUrl the path to the content
* @return Returns true if the content exists. * @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 * @throws ContentIOException
* *
* @see ContentReader#exists() * @see ContentReader#exists()
@@ -90,6 +92,7 @@ public interface ContentStore
* *
* @see #exists(String) * @see #exists(String)
* @see ContentReader#exists() * @see ContentReader#exists()
* @see EmptyContentReader
*/ */
public ContentReader getReader(String contentUrl) throws ContentIOException; public ContentReader getReader(String contentUrl) throws ContentIOException;

View File

@@ -84,6 +84,7 @@ public class ContentTestSuite extends TestSuite
suite.addTestSuite(ContentDataTest.class); suite.addTestSuite(ContentDataTest.class);
suite.addTestSuite(MimetypeMapTest.class); suite.addTestSuite(MimetypeMapTest.class);
suite.addTestSuite(RoutingContentServiceTest.class); suite.addTestSuite(RoutingContentServiceTest.class);
suite.addTestSuite(RoutingContentStoreTest.class);
return suite; return suite;
} }

View File

@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.i18n.I18NUtil;
import org.alfresco.repo.avm.AVMNodeConverter; import org.alfresco.repo.avm.AVMNodeConverter;
import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy;
import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy;
@@ -268,6 +269,30 @@ public class RoutingContentService implements ContentService
} }
} }
/** {@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) public ContentReader getReader(NodeRef nodeRef, QName propertyQName)
{ {
return getReader(nodeRef, propertyQName, true); 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 // The context of the read is entirely described by the URL
ContentReader reader = store.getReader(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 // set extra data on the reader
reader.setMimetype(contentData.getMimetype()); reader.setMimetype(contentData.getMimetype());

View File

@@ -269,6 +269,21 @@ public class RoutingContentServiceTest extends TestCase
assertNull("Reader must be null if the content URL is null", reader); 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 * Checks what happens when the physical content disappears
*/ */

View File

@@ -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<ContentStore> stores;
private Random random;
public RandomRoutingContentStore(ContentStore ... stores)
{
this.random = new Random();
this.stores = new ArrayList<ContentStore>(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<String, ContentStore> cache = new EhCacheAdapter<String, ContentStore>();
cache.setCache(ehCache);
super.setStoresCache(cache);
}
@Override
protected List<ContentStore> 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);
}
}
}

View File

@@ -57,12 +57,22 @@ public class FileContentStore extends AbstractContentStore
private boolean allowRandomAccess; private boolean allowRandomAccess;
/** /**
* @param rootDirectory the root under which files will be stored. The * @param rootDirectoryStr the root under which files will be stored.
* directory will be created if it does not exist. * The directory will be created if it does not exist.
*
* @see FileContentStore#FileContentStore(File)
*/ */
public FileContentStore(String rootDirectoryStr) 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.exists())
{ {
if (!rootDirectory.mkdirs()) if (!rootDirectory.mkdirs())
@@ -70,7 +80,7 @@ public class FileContentStore extends AbstractContentStore
throw new ContentIOException("Failed to create store root: " + rootDirectory, null); throw new ContentIOException("Failed to create store root: " + rootDirectory, null);
} }
} }
rootDirectory = rootDirectory.getAbsoluteFile(); this.rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath(); rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true; allowRandomAccess = true;
} }
@@ -183,8 +193,7 @@ public class FileContentStore extends AbstractContentStore
} }
/** /**
* Creates a file from the given relative URL. The URL must start with * Creates a file from the given relative URL.
* the required {@link FileContentStore#STORE_PROTOCOL protocol prefix}.
* *
* @param contentUrl the content URL including the protocol prefix * @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not * @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) private File makeFile(String contentUrl)
{ {
// take just the part after the protocol // 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 // get the file
File file = new File(rootDirectory, relativeUrl); File file = new File(rootDirectory, relativeUrl);
// done // done
@@ -366,4 +382,47 @@ public class FileContentStore extends AbstractContentStore
} }
return deleted; 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.
* <p>
* 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 <tt>null</tt> 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;
}
} }

View File

@@ -54,6 +54,21 @@ import org.alfresco.service.namespace.QName;
@PublicService @PublicService
public interface ContentService 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.
* <p>
* <tt>null</tt> 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. * Gets a reader for the content associated with the given node property.
* <p> * <p>

View File

@@ -45,4 +45,14 @@
</property> </property>
</bean> </bean>
<bean name="objectCache" class="org.alfresco.repo.cache.EhCacheAdapter">
<property name="cache">
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean" >
<property name="cacheName">
<value>objectCache</value>
</property>
</bean>
</property>
</bean>
</beans> </beans>