Allow other types of content URLs other than store://...

Enforce restriction that all content URLs must be of form protocol://identifier
Allow for read-only stores.
Improved tests so that it is easier, when writing a new store, to determine if the store is compliant or not.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5899 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2007-06-09 00:43:02 +00:00
parent 9b03b15674
commit f30ccf8d6c
28 changed files with 1685 additions and 628 deletions

View File

@@ -39,9 +39,8 @@ import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.audit.AuditConfiguration; import org.alfresco.repo.audit.AuditConfiguration;
import org.alfresco.repo.audit.AuditDAO; import org.alfresco.repo.audit.AuditDAO;
import org.alfresco.repo.audit.AuditException;
import org.alfresco.repo.audit.AuditState; import org.alfresco.repo.audit.AuditState;
import org.alfresco.repo.content.AbstractContentStore; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil;
@@ -383,12 +382,12 @@ public class HibernateAuditDAO extends HibernateDaoSupport implements AuditDAO,
{ {
AuditConfigImpl auditConfig = new AuditConfigImpl(); AuditConfigImpl auditConfig = new AuditConfigImpl();
InputStream is = new BufferedInputStream(auditInfo.getAuditConfiguration().getInputStream()); InputStream is = new BufferedInputStream(auditInfo.getAuditConfiguration().getInputStream());
String url = AbstractContentStore.createNewUrl(); ContentWriter writer = contentStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
ContentWriter writer = contentStore.getWriter(null, url);
writer.setMimetype(MimetypeMap.MIMETYPE_XML); writer.setMimetype(MimetypeMap.MIMETYPE_XML);
writer.setEncoding("UTF-8"); writer.setEncoding("UTF-8");
writer.putContent(is); writer.putContent(is);
auditConfig.setConfigURL(url); String contentUrl = writer.getContentUrl();
auditConfig.setConfigURL(contentUrl);
getSession().save(auditConfig); getSession().save(auditConfig);
return auditConfig; return auditConfig;
} }

View File

@@ -81,7 +81,7 @@ public abstract class AbstractContentAccessor implements ContentAccessor
{ {
if (contentUrl == null || contentUrl.length() == 0) if (contentUrl == null || contentUrl.length() == 0)
{ {
throw new IllegalArgumentException("contentUrl must be a valid String"); throw new IllegalArgumentException("contentUrl is invalid:" + contentUrl);
} }
this.contentUrl = contentUrl; this.contentUrl = contentUrl;

View File

@@ -24,15 +24,15 @@
*/ */
package org.alfresco.repo.content; package org.alfresco.repo.content;
import java.util.Calendar; import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Set; import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.GUID; import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/** /**
* Base class providing support for different types of content stores. * Base class providing support for different types of content stores.
@@ -41,94 +41,112 @@ import org.alfresco.util.GUID;
* reasons of replication and backup, the most important functionality * reasons of replication and backup, the most important functionality
* provided is the generation of new content URLs and the checking of * provided is the generation of new content URLs and the checking of
* existing URLs. * existing URLs.
* <p>
* Implementations must override either of the <b>getWriter</b> methods;
* {@link #getWriter(ContentContext)} or {@link #getWriterInternal(ContentReader, String)}.
*
* @see #getWriter(ContentReader, String)
* @see #getWriterInternal(ContentReader, String)
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
public abstract class AbstractContentStore implements ContentStore public abstract class AbstractContentStore implements ContentStore
{ {
private static Log logger = LogFactory.getLog(AbstractContentStore.class);
/** Helper */
private static final int PROTOCOL_DELIMETER_LENGTH = PROTOCOL_DELIMITER.length();
/** /**
* Creates a new content URL. This must be supported by all * Checks that the content conforms to the format <b>protocol://identifier</b>
* stores that are compatible with Alfresco. * as specified in the contract of the {@link ContentStore} interface.
* *
* @return Returns a new and unique content URL * @param contentUrl the content URL to check
* @return Returns <tt>true</tt> if the content URL is valid
*
* @since 2.1
*/ */
public static String createNewUrl() public static final boolean isValidContentUrl(String contentUrl)
{ {
Calendar calendar = new GregorianCalendar(); if (contentUrl == null)
int year = calendar.get(Calendar.YEAR); {
int month = calendar.get(Calendar.MONTH) + 1; // 0-based return false;
int day = calendar.get(Calendar.DAY_OF_MONTH); }
int hour = calendar.get(Calendar.HOUR_OF_DAY); int index = contentUrl.indexOf(ContentStore.PROTOCOL_DELIMITER);
int minute = calendar.get(Calendar.MINUTE); if (index <= 0)
// create the URL {
StringBuilder sb = new StringBuilder(20); return false;
sb.append(STORE_PROTOCOL) }
.append(year).append('/') if (contentUrl.length() <= (index + PROTOCOL_DELIMETER_LENGTH))
.append(month).append('/') {
.append(day).append('/') return false;
.append(hour).append('/') }
.append(minute).append('/') return true;
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
// done
return newContentUrl;
} }
/** /**
* This method can be used to ensure that URLs conform to the * Splits the content URL into its component parts as separated by
* required format. If subclasses have to parse the URL, * {@link ContentStore#PROTOCOL_DELIMITER protocol delimiter}.
* 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.
* *
* @param contentUrl a URL of the content to check * @param contentUrl the content URL to split
* @return Returns the relative part of the URL * @return Returns the protocol and identifier portions of the content URL,
* @throws RuntimeException if the URL is not correct * both of which will not be <tt>null</tt>
* @throws UnsupportedContentUrlException if the content URL is invalid
* *
* @deprecated Stores can really have any prefix in the URL. This method was * @since 2.1
* really specific to the FileContentStore and has been moved into
* it.
*/ */
public static String getRelativePart(String contentUrl) throws RuntimeException protected Pair<String, String> getContentUrlParts(String contentUrl)
{ {
int index = 0; if (contentUrl == null)
if (contentUrl.startsWith(STORE_PROTOCOL))
{ {
index = 8; throw new IllegalArgumentException("The contentUrl may not be null");
} }
else if (contentUrl.startsWith("file://")) int index = contentUrl.indexOf(ContentStore.PROTOCOL_DELIMITER);
if (index <= 0)
{ {
index = 7; throw new UnsupportedContentUrlException(this, contentUrl);
} }
else String protocol = contentUrl.substring(0, index);
String identifier = contentUrl.substring(
index + PROTOCOL_DELIMETER_LENGTH,
contentUrl.length());
if (identifier.length() == 0)
{ {
throw new AlfrescoRuntimeException( throw new UnsupportedContentUrlException(this, contentUrl);
"All content URLs must start with " + STORE_PROTOCOL + ": \n" +
" the invalid url is: " + contentUrl);
} }
return new Pair<String, String>(protocol, identifier);
// 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() < 8)
{
throw new AlfrescoRuntimeException(
"The content URL is invalid: \n" +
" content url: " + contentUrl);
}
return path;
} }
/** /**
* Simple implementation that uses the * Override this method to supply a efficient and direct check of the URL supplied.
* {@link ContentReader#exists() reader's exists} method as its implementation. * The default implementation checks whether {@link ContentStore#getReader(String)}
* throws the {@link UnsupportedContentUrlException} exception.
*
* @since 2.1
*/ */
public boolean exists(String contentUrl) throws ContentIOException public boolean isContentUrlSupported(String contentUrl)
{ {
ContentReader reader = getReader(contentUrl); try
return reader.exists(); {
getReader(contentUrl);
return true;
}
catch (UnsupportedContentUrlException e)
{
// It is not supported
return false;
}
}
/**
* Override if the derived class supports the operation.
*
* @throws UnsupportedOperationException always
*
* @since 2.1
*/
public boolean delete(String contentUrl)
{
throw new UnsupportedOperationException();
} }
/** /**
@@ -136,16 +154,124 @@ public abstract class AbstractContentStore implements ContentStore
* *
* @see ContentStore#getUrls(java.util.Date, java.util.Date) * @see ContentStore#getUrls(java.util.Date, java.util.Date)
*/ */
public final Set<String> getUrls() throws ContentIOException public final Set<String> getUrls()
{ {
return getUrls(null, null); return getUrls(null, null);
} }
/**
* Override if the derived class supports the operation.
*
* @throws UnsupportedOperationException always
*
* @since 2.1
*/
public Set<String> getUrls(Date createdAfter, Date createdBefore)
{
throw new UnsupportedOperationException();
}
/**
* Implement to supply a store-specific writer for the given existing content
* and optional target content URL.
*
* @param existingContentReader a reader onto any content to initialize the new writer with
* @param newContentUrl an optional target for the new content
*
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentExistsException
* if the content URL is already in use
* @throws ContentIOException
* if an IO error occurs
*
* @since 2.1
*/
protected ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl)
{
throw new UnsupportedOperationException("Override getWriterInternal (preferred) or getWriter");
}
/**
* An implementation that does some sanity checking before requesting a writer from the
* store. If this method is not overridden, then an implementation of
* {@link #getWriterInternal(ContentReader, String)} must be supplied.
*
* @see #getWriterInternal(ContentReader, String)
* @since 2.1
*/
public ContentWriter getWriter(ContentContext context)
{
ContentReader existingContentReader = context.getExistingContentReader();
String contentUrl = context.getContentUrl();
// Check if the store handles writes
if (!isWriteSupported())
{
if (logger.isDebugEnabled())
{
logger.debug(
"Write requests are not supported for this store:\n" +
" Store: " + this + "\n" +
" Context: " + context);
}
throw new UnsupportedOperationException("Write operations are not supported by this store: " + this);
}
// Check the content URL
if (contentUrl != null)
{
if (!isContentUrlSupported(contentUrl))
{
if (logger.isDebugEnabled())
{
logger.debug(
"Specific writer content URL is unsupported: \n" +
" Store: " + this + "\n" +
" Context: " + context);
}
throw new UnsupportedContentUrlException(this, contentUrl);
}
else if (exists(contentUrl))
{
if (logger.isDebugEnabled())
{
logger.debug(
"The content location is already used: \n" +
" Store: " + this + "\n" +
" Context: " + context);
}
throw new ContentExistsException(this, contentUrl);
}
}
// Get the writer
ContentWriter writer = getWriterInternal(existingContentReader, contentUrl);
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"Fetched new writer: \n" +
" Store: " + this + "\n" +
" Context: " + context + "\n" +
" Writer: " + writer);
}
return writer;
}
/**
* Simple implementation that uses the
* {@link ContentReader#exists() reader's exists} method as its implementation.
* Override this method if a more efficient implementation is possible.
*/
public boolean exists(String contentUrl)
{
ContentReader reader = getReader(contentUrl);
return reader.exists();
}
/** /**
* @see ContentContext * @see ContentContext
* @see ContentStore#getWriter(ContentContext) * @see ContentStore#getWriter(ContentContext)
*/ */
public final ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException public final ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl)
{ {
ContentContext ctx = new ContentContext(existingContentReader, newContentUrl); ContentContext ctx = new ContentContext(existingContentReader, newContentUrl);
return getWriter(ctx); return getWriter(ctx);

View File

@@ -121,9 +121,10 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
*/ */
public final ContentReader getReader() throws ContentIOException public final ContentReader getReader() throws ContentIOException
{ {
String contentUrl = getContentUrl();
if (!isClosed()) if (!isClosed())
{ {
return null; return new EmptyContentReader(contentUrl);
} }
ContentReader reader = createReader(); ContentReader reader = createReader();
if (reader == null) if (reader == null)
@@ -131,7 +132,7 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" + throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" +
" writer: " + this); " writer: " + this);
} }
else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl())) else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(contentUrl))
{ {
throw new AlfrescoRuntimeException("ContentReader has different URL: \n" + throw new AlfrescoRuntimeException("ContentReader has different URL: \n" +
" writer: " + this + "\n" + " writer: " + this + "\n" +
@@ -235,7 +236,7 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
// this is a use-once object // this is a use-once object
if (channel != null) if (channel != null)
{ {
throw new RuntimeException("A channel has already been opened"); throw new ContentIOException("A channel has already been opened");
} }
WritableByteChannel directChannel = getDirectWritableChannel(); WritableByteChannel directChannel = getDirectWritableChannel();
channel = getCallbackWritableChannel(directChannel, listeners); channel = getCallbackWritableChannel(directChannel, listeners);

View File

@@ -0,0 +1,286 @@
/*
* 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.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.Set;
import javax.transaction.UserTransaction;
import junit.framework.TestCase;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
/**
* Abstract base class that provides a set of tests for implementations
* of {@link ContentStore}.
*
* @see ContentStore
* @see org.alfresco.service.cmr.repository.ContentReader
* @see org.alfresco.service.cmr.repository.ContentWriter
*
* @author Derek Hulley
*/
public abstract class AbstractReadOnlyContentStoreTest extends TestCase
{
private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
private static Log logger = LogFactory.getLog(AbstractReadOnlyContentStoreTest.class);
protected TransactionService transactionService;
private UserTransaction txn;
public AbstractReadOnlyContentStoreTest()
{
super();
}
/**
* Starts a transaction
*/
@Override
public void setUp() throws Exception
{
transactionService = (TransactionService) ctx.getBean("TransactionService");
txn = transactionService.getUserTransaction();
txn.begin();
}
/**
* Rolls back the transaction
*/
@Override
public void tearDown() throws Exception
{
try { txn.rollback(); } catch (Throwable e) {e.printStackTrace();}
}
/**
* Fetch the store to be used during a test. This method is invoked once per test - it is
* therefore safe to use <code>setUp</code> to initialise resources.
* <p>
* Usually tests will construct a static instance of the store to use throughout all the
* tests.
*
* @return Returns the <b>same instance</b> of a store for all invocations.
*/
protected abstract ContentStore getStore();
/**
* Gets a reader for the given content URL from the store
*
* @see #getStore()
*/
protected final ContentReader getReader(String contentUrl)
{
return getStore().getReader(contentUrl);
}
/**
* Fetch a valid URL from the store. The default implementation will attempt to get
* all the available URLs from the store and pick the first one. Writable store tests
* can create some content to be sure of its existence.
*
* @return
* Return any valid URL for the store, or <tt>null</tt> if the store is empty.
*/
protected String getExistingContentUrl()
{
ContentStore store = getStore();
try
{
Set<String> contentUrls = store.getUrls();
if (contentUrls.size() > 0)
{
return (String) contentUrls.toArray()[0];
}
else
{
// We can't do anything with this
return null;
}
}
catch (UnsupportedOperationException e)
{
// The store doesn't support this
return null;
}
}
public void testSetUp() throws Exception
{
// check that the store remains the same
ContentStore store = getStore();
assertNotNull("No store provided", store);
assertTrue("The same instance of the store must be returned for getStore", store == getStore());
}
/**
* Helper to ensure that illegal content URLs are flagged for
* <b>getReader()</b> and <b>exists()</b> requests.
*/
private void checkIllegalReadContentUrl(ContentStore store, String contentUrl)
{
assertFalse("This check is for unsupported content URLs only", store.isContentUrlSupported(contentUrl));
try
{
store.getReader(contentUrl);
fail("Expected UnsupportedContentUrlException for getReader(), but got nothing: " + contentUrl);
}
catch (UnsupportedContentUrlException e)
{
// Expected
}
try
{
store.exists(contentUrl);
fail("Expected UnsupportedContentUrlException for exists(), but got nothing: " + contentUrl);
}
catch (UnsupportedContentUrlException e)
{
// Expected
}
}
/**
* Checks that the error handling for <i>inappropriate</i> content URLs
*/
public void testIllegalReadableContentUrls()
{
ContentStore store = getStore();
checkIllegalReadContentUrl(store, "://bogus");
checkIllegalReadContentUrl(store, "bogus://");
checkIllegalReadContentUrl(store, "bogus://bogus");
}
/**
* Checks that the various methods of obtaining a reader are supported.
*/
public void testGetReaderForExistingContentUrl() throws Exception
{
ContentStore store = getStore();
String contentUrl = getExistingContentUrl();
if (contentUrl == null)
{
logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
}
// Get the reader
assertTrue("URL returned in set seems to no longer exist", store.exists(contentUrl));
ContentReader reader = store.getReader(contentUrl);
assertNotNull("Reader should never be null", reader);
assertTrue("Reader says content doesn't exist", reader.exists());
assertFalse("Reader should not be closed before a read", reader.isClosed());
assertFalse("The reader channel should not be open yet", reader.isChannelOpen());
// Open the channel
ReadableByteChannel readChannel = reader.getReadableChannel();
readChannel.read(ByteBuffer.wrap(new byte[500]));
assertFalse("Reader should not be closed during a read", reader.isClosed());
assertTrue("The reader channel should be open during a read", reader.isChannelOpen());
// Close the channel
readChannel.close();
assertTrue("Reader should be closed after a read", reader.isClosed());
assertFalse("The reader channel should be closed after a read", reader.isChannelOpen());
}
/**
* Tests random access reading
* <p>
* Only executes if the reader implements {@link RandomAccessContent}.
*/
public void testRandomAccessRead() throws Exception
{
ContentStore store = getStore();
String contentUrl = getExistingContentUrl();
if (contentUrl == null)
{
logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
}
// Get the reader
ContentReader reader = store.getReader(contentUrl);
assertNotNull("Reader should never be null", reader);
FileChannel fileChannel = reader.getFileChannel();
assertNotNull("No channel given", fileChannel);
// check that no other content access is allowed
try
{
reader.getReadableChannel();
fail("Second channel access allowed");
}
catch (RuntimeException e)
{
// expected
}
fileChannel.close();
}
public void testBlockedWriteOperations() throws Exception
{
ContentStore store = getStore();
if (store.isWriteSupported())
{
// Just ignore this test
return;
}
// Ensure that we can't get a writer
try
{
store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
fail("Read-only store provided a writer: " + store);
}
catch (UnsupportedOperationException e)
{
// Expected
}
String contentUrl = getExistingContentUrl();
if (contentUrl == null)
{
logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
}
// Ensure that we can't delete a URL
try
{
store.delete(contentUrl);
fail("Read-only store allowed deletion: " + store);
}
catch (UnsupportedOperationException e)
{
// Expected
}
}
}

View File

@@ -32,6 +32,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
@@ -101,10 +102,28 @@ public abstract class AbstractRoutingContentStore implements ContentStore
{ {
// Check if the store is in the cache // Check if the store is in the cache
ContentStore store = storesByContentUrl.get(contentUrl); ContentStore store = storesByContentUrl.get(contentUrl);
if (store != null && store.exists(contentUrl)) if (store != null)
{ {
// We found a store and can use it // We found a store that was previously used
return store; try
{
// It is possible for content to be removed from a store and
// it might have moved into another store.
if (store.exists(contentUrl))
{
// We found a store and can use it
return store;
}
}
catch (UnsupportedContentUrlException e)
{
// This is odd. The store that previously supported the content URL
// no longer does so. I can't think of a reason why that would be.
throw new AlfrescoRuntimeException(
"Found a content store that previously supported a URL, but no longer does: \n" +
" Store: " + store + "\n" +
" Content URL: " + contentUrl);
}
} }
} }
finally finally
@@ -129,23 +148,30 @@ public abstract class AbstractRoutingContentStore implements ContentStore
} }
return store; return store;
} }
else
{
store = null;
}
// It isn't, so search all the stores // It isn't, so search all the stores
List<ContentStore> stores = getAllStores(); List<ContentStore> stores = getAllStores();
// Keep track of the unsupported state of the content URL - it might be a rubbish URL
boolean contentUrlSupported = false;
for (ContentStore storeInList : stores) for (ContentStore storeInList : stores)
{ {
boolean exists = false; boolean exists = false;
try try
{ {
exists = storeInList.exists(contentUrl); exists = storeInList.exists(contentUrl);
if (!exists) // At least the content URL was supported
{ contentUrlSupported = true;
// It is not in the store
continue;
}
} }
catch (Throwable e) catch (UnsupportedContentUrlException e)
{ {
// The API used to allow failure when the URL wasn't there // The store can't handle the content URL
}
if (!exists)
{
// It is not in the store
continue; continue;
} }
// We found one // We found one
@@ -154,6 +180,11 @@ public abstract class AbstractRoutingContentStore implements ContentStore
storesByContentUrl.put(contentUrl, store); storesByContentUrl.put(contentUrl, store);
break; break;
} }
// Check if the content URL was supported
if (!contentUrlSupported)
{
throw new UnsupportedContentUrlException(this, contentUrl);
}
// Done // Done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
@@ -171,25 +202,49 @@ public abstract class AbstractRoutingContentStore implements ContentStore
} }
/** /**
* This operation has to be performed on all the stores in order to maintain the * @return Returns <tt>true</tt> if the URL is supported by any of the stores.
* {@link ContentStore#exists(String)} contract.
*/ */
public boolean delete(String contentUrl) throws ContentIOException public boolean isContentUrlSupported(String contentUrl)
{ {
boolean deleted = true;
List<ContentStore> stores = getAllStores(); List<ContentStore> stores = getAllStores();
boolean supported = false;
for (ContentStore store : stores) for (ContentStore store : stores)
{ {
deleted &= store.delete(contentUrl); if (store.isContentUrlSupported(contentUrl))
{
supported = true;
break;
}
} }
// Done // Done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("Deleted content URL from stores: \n" + logger.debug("The url " + (supported ? "is" : "is not") + " supported by at least one store.");
" Stores: " + stores.size() + "\n" +
" Deleted: " + deleted);
} }
return deleted; return supported;
}
/**
* @return Returns <tt>true</tt> if write is supported by any of the stores.
*/
public boolean isWriteSupported()
{
List<ContentStore> stores = getAllStores();
boolean supported = false;
for (ContentStore store : stores)
{
if (store.isWriteSupported())
{
supported = true;
break;
}
}
// Done
if (logger.isDebugEnabled())
{
logger.debug("Writing " + (supported ? "is" : "is not") + " supported by at least one store.");
}
return supported;
} }
/** /**
@@ -228,6 +283,87 @@ public abstract class AbstractRoutingContentStore implements ContentStore
} }
} }
/**
* Selects a store for the given context and caches store that was used.
*
* @see #selectWriteStore(ContentContext)
*/
public ContentWriter getWriter(ContentContext context) throws ContentIOException
{
String contentUrl = context.getContentUrl();
if (contentUrl != null)
{
// Check to see if it is in the cache
storesCacheReadLock.lock();
try
{
// Check if the store is in the cache
ContentStore store = storesByContentUrl.get(contentUrl);
if (store != null)
{
throw new ContentExistsException(this, contentUrl);
}
/*
* We could go further and check each store for the existence of the URL,
* but that would be overkill. The main problem we need to prevent is
* the simultaneous access of the same store. The router represents
* a single store and therefore if the URL is present in any of the stores,
* it is effectively present in all of them.
*/
}
finally
{
storesCacheReadLock.unlock();
}
}
// Select the store for writing
ContentStore store = selectWriteStore(context);
// Check that we were given a valid store
if (store == null)
{
throw new NullPointerException(
"Unable to find a writer. 'selectWriteStore' may not return null: \n" +
" Router: " + this + "\n" +
" Chose: " + store);
}
else if (!store.isWriteSupported())
{
throw new AlfrescoRuntimeException(
"A write store was chosen that doesn't support writes: \n" +
" Router: " + this + "\n" +
" Chose: " + store);
}
ContentWriter writer = store.getWriter(context);
// Cache the store against the URL
storesCacheWriteLock.lock();
try
{
storesByContentUrl.put(contentUrl, store);
}
finally
{
storesCacheWriteLock.unlock();
}
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"Got writer and cache URL from store: \n" +
" Context: " + context + "\n" +
" Writer: " + writer + "\n" +
" Store: " + store);
}
return writer;
}
/**
* @see
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException
{
return getWriter(new ContentContext(existingContentReader, newContentUrl));
}
/** /**
* Compile a set of URLs from all stores. * Compile a set of URLs from all stores.
*/ */
@@ -267,47 +403,27 @@ public abstract class AbstractRoutingContentStore implements ContentStore
} }
/** /**
* Selects a store for the given context and caches store that was used. * This operation has to be performed on all the stores in order to maintain the
* * {@link ContentStore#exists(String)} contract.
* @see #selectWriteStore(ContentContext)
*/ */
public ContentWriter getWriter(ContentContext context) throws ContentIOException public boolean delete(String contentUrl) throws ContentIOException
{ {
// Select the store for writing boolean deleted = true;
ContentStore store = selectWriteStore(context); List<ContentStore> stores = getAllStores();
if (store == null) for (ContentStore store : stores)
{ {
throw new NullPointerException("Unable to find a writer. 'selectWriteStore' may not return null."); if (store.isWriteSupported())
} {
ContentWriter writer = store.getWriter(context); deleted &= store.delete(contentUrl);
// Cache the store against the URL }
storesCacheWriteLock.lock();
try
{
String contentUrl = writer.getContentUrl();
storesByContentUrl.put(contentUrl, store);
}
finally
{
storesCacheWriteLock.unlock();
} }
// Done // Done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug( logger.debug("Deleted content URL from stores: \n" +
"Got writer and cache URL from store: \n" + " Stores: " + stores.size() + "\n" +
" Context: " + context + "\n" + " Deleted: " + deleted);
" Writer: " + writer + "\n" +
" Store: " + store);
} }
return writer; return deleted;
}
/**
* @see
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException
{
return getWriter(new ContentContext(existingContentReader, newContentUrl));
} }
} }

View File

@@ -38,113 +38,224 @@ import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import javax.transaction.UserTransaction;
import junit.framework.TestCase;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log;
import org.alfresco.util.ApplicationContextHelper; import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
/** /**
* Abstract base class that provides a set of tests for implementations * Abstract base class that provides a set of tests for implementations
* of the content readers and writers. * of {@link ContentStore}.
* *
* @see ContentStore
* @see org.alfresco.service.cmr.repository.ContentReader * @see org.alfresco.service.cmr.repository.ContentReader
* @see org.alfresco.service.cmr.repository.ContentWriter * @see org.alfresco.service.cmr.repository.ContentWriter
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
public abstract class AbstractContentReadWriteTest extends TestCase public abstract class AbstractWritableContentStoreTest extends AbstractReadOnlyContentStoreTest
{ {
private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); private static Log logger = LogFactory.getLog(AbstractWritableContentStoreTest.class);
protected TransactionService transactionService; public AbstractWritableContentStoreTest()
private String contentUrl;
private UserTransaction txn;
public AbstractContentReadWriteTest()
{ {
super(); super();
} }
@Override
public void setUp() throws Exception
{
contentUrl = AbstractContentStore.createNewUrl();
transactionService = (TransactionService) ctx.getBean("TransactionService");
txn = transactionService.getUserTransaction();
txn.begin();
}
public void tearDown() throws Exception
{
txn.rollback();
}
/** /**
* Fetch the store to be used during a test. This method is invoked once per test - it is * @inheritDoc
* therefore safe to use <code>setUp</code> to initialise resources.
* <p> * <p>
* Usually tests will construct a static instance of the store to use throughout all the * This implementation creates some content in the store and returns the new content URL.
* tests.
*
* @return Returns the <b>same instance</b> of a store for all invocations.
*/ */
protected abstract ContentStore getStore(); protected String getExistingContentUrl()
/**
* @see #getStore()
*/
protected final ContentWriter getWriter()
{ {
ContentContext contentCtx = new ContentContext(null, contentUrl); ContentWriter writer = getWriter();
return getStore().getWriter(contentCtx); writer.putContent("Content for " + getName());
return writer.getContentUrl();
} }
/** /**
* @see #getStore() * 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 final ContentReader getReader() protected ContentWriter getWriter()
{ {
return getStore().getReader(contentUrl); ContentStore store = getStore();
return store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
} }
public void testSetUp() throws Exception public void testSetUp() throws Exception
{ {
assertNotNull("setUp() not executed: no content URL present");
// check that the store remains the same // check that the store remains the same
ContentStore store = getStore(); ContentStore store = getStore();
assertNotNull("No store provided", store); assertNotNull("No store provided", store);
assertTrue("The same instance of the store must be returned for getStore", store == getStore()); assertTrue("The same instance of the store must be returned for getStore", store == getStore());
} }
public void testContentUrl() throws Exception public void testWritable() throws Exception
{ {
ContentReader reader = getReader(); ContentStore store = getStore();
ContentWriter writer = getWriter(); assertTrue("The store cannot be read-only", store.isWriteSupported());
// the contract is that both the reader and writer must refer to the same
// content -> the URL must be the same
String readerContentUrl = reader.getContentUrl();
String writerContentUrl = writer.getContentUrl();
assertNotNull("Reader url is invalid", readerContentUrl);
assertNotNull("Writer url is invalid", writerContentUrl);
assertEquals("Reader and writer must reference same content",
readerContentUrl,
writerContentUrl);
// check that the content URL is correct
assertTrue("Content URL doesn't start with correct prefix",
readerContentUrl.startsWith(ContentStore.STORE_PROTOCOL));
} }
public void testMimetypAbdEncodingAndLocale() throws Exception /**
* Helper to ensure that illegal content URLs are flagged for <b>getWriter</b> requests
*/
private void checkIllegalWritableContentUrl(ContentStore store, String contentUrl)
{
assertFalse("This check is for unsupported content URLs only", store.isContentUrlSupported(contentUrl));
ContentContext bogusContentCtx = new ContentContext(null, contentUrl);
try
{
store.getWriter(bogusContentCtx);
fail("Expected UnsupportedContentUrlException, but got nothing");
}
catch (UnsupportedContentUrlException e)
{
// Expected
}
}
/**
* Checks that the error handling for <i>inappropriate</i> content URLs
*/
public void testIllegalWritableContentUrls()
{
ContentStore store = getStore();
checkIllegalWritableContentUrl(store, "://bogus");
checkIllegalWritableContentUrl(store, "bogus://");
checkIllegalWritableContentUrl(store, "bogus://bogus");
}
/**
* Get a writer and write a little bit of content before reading it.
*/
public void testSimpleUse()
{
ContentStore store = getStore();
String content = "Content for " + getName();
ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
assertNotNull("Writer may not be null", writer);
// Ensure that the URL is available
String contentUrlBefore = writer.getContentUrl();
assertNotNull("Content URL may not be null for unused writer", contentUrlBefore);
assertTrue("URL is not valid: " + contentUrlBefore, AbstractContentStore.isValidContentUrl(contentUrlBefore));
// Write something
writer.putContent(content);
String contentUrlAfter = writer.getContentUrl();
assertTrue("URL is not valid: " + contentUrlBefore, AbstractContentStore.isValidContentUrl(contentUrlAfter));
assertEquals("The content URL may not change just because the writer has put content", contentUrlBefore, contentUrlAfter);
// Get the readers
ContentReader reader = store.getReader(contentUrlBefore);
assertNotNull("Reader from store is null", reader);
assertEquals(reader.getContentUrl(), writer.getContentUrl());
String checkContent = reader.getContentString();
assertEquals("Content is different", content, checkContent);
}
/**
* Checks that the various methods of obtaining a reader are supported.
*/
public void testGetReader() throws Exception
{
ContentStore store = getStore();
ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
String contentUrl = writer.getContentUrl();
// Check that a reader is available from the store
ContentReader readerFromStoreBeforeWrite = store.getReader(contentUrl);
assertNotNull("A reader must always be available from the store", readerFromStoreBeforeWrite);
// check that a reader is available from the writer
ContentReader readerFromWriterBeforeWrite = writer.getReader();
assertNotNull("A reader must always be available from the writer", readerFromWriterBeforeWrite);
String content = "Content for " + getName();
// write some content
long before = System.currentTimeMillis();
writer.setMimetype("text/plain");
writer.setEncoding("UTF-8");
writer.setLocale(Locale.CHINESE);
writer.putContent(content);
long after = System.currentTimeMillis();
// get a reader from the store
ContentReader readerFromStore = store.getReader(contentUrl);
assertNotNull(readerFromStore);
assertTrue(readerFromStore.exists());
// Store-provided readers don't have context other than URLs
// assertEquals(writer.getContentData(), readerFromStore.getContentData());
assertEquals(content, readerFromStore.getContentString());
// get a reader from the writer
ContentReader readerFromWriter = writer.getReader();
assertNotNull(readerFromWriter);
assertTrue(readerFromWriter.exists());
assertEquals(writer.getContentData(), readerFromWriter.getContentData());
assertEquals(content, readerFromWriter.getContentString());
// get another reader from the reader
ContentReader readerFromReader = readerFromWriter.getReader();
assertNotNull(readerFromReader);
assertTrue(readerFromReader.exists());
assertEquals(writer.getContentData(), readerFromReader.getContentData());
assertEquals(content, readerFromReader.getContentString());
// check that the length is correct
int length = content.getBytes(writer.getEncoding()).length;
assertEquals("Reader content length is incorrect", length, readerFromWriter.getSize());
// check that the last modified time is correct
long modifiedTimeCheck = readerFromWriter.getLastModified();
// On some versionms of Linux (e.g. Centos) this test won't work as the
// modified time accuracy is only to the second.
long beforeSeconds = before/1000L;
long afterSeconds = after/1000L;
long modifiedTimeCheckSeconds = modifiedTimeCheck/1000L;
assertTrue("Reader last modified is incorrect", beforeSeconds <= modifiedTimeCheckSeconds);
assertTrue("Reader last modified is incorrect", modifiedTimeCheckSeconds <= afterSeconds);
}
/**
* Check that a reader is immutable, i.e. that a reader fetched before a
* write doesn't suddenly become aware of the content once it has been written.
*/
public void testReaderImmutability()
{
ContentWriter writer = getWriter();
ContentReader readerBeforeWrite = writer.getReader();
assertNotNull(readerBeforeWrite);
assertFalse(readerBeforeWrite.exists());
// Write some content
writer.putContent("Content for " + getName());
assertFalse("Reader's state changed after write", readerBeforeWrite.exists());
try
{
readerBeforeWrite.getContentString();
fail("Reader's state changed after write");
}
catch (ContentIOException e)
{
// Expected
}
// A new reader should work
ContentReader readerAfterWrite = writer.getReader();
assertTrue("New reader after write should be directed to new content", readerAfterWrite.exists());
}
public void testMimetypAndEncodingAndLocale() throws Exception
{ {
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
// set mimetype and encoding // set mimetype and encoding
@@ -172,104 +283,25 @@ public abstract class AbstractContentReadWriteTest extends TestCase
assertEquals("Encoding and decoding of strings failed", content, contentCheck); assertEquals("Encoding and decoding of strings failed", content, contentCheck);
} }
public void testExists() throws Exception
{
ContentStore store = getStore();
// make up a URL
String contentUrl = AbstractContentStore.createNewUrl();
// it should not exist in the store
assertFalse("Store exists fails with new URL", store.exists(contentUrl));
// get a reader
ContentReader reader = store.getReader(contentUrl);
assertNotNull("Reader must be present, even for missing content", reader);
assertFalse("Reader exists failure", reader.exists());
// write something
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));
}
public void testGetReader() throws Exception
{
ContentWriter writer = getWriter();
// check that no reader is available from the writer just yet
ContentReader nullReader = writer.getReader();
assertNull("No reader expected", nullReader);
String content = "ABC";
// write some content
long before = System.currentTimeMillis();
writer.setMimetype("text/plain");
writer.setEncoding("UTF-8");
writer.setLocale(Locale.CHINESE);
writer.putContent(content);
long after = System.currentTimeMillis();
// get a reader from the writer
ContentReader readerFromWriter = writer.getReader();
assertEquals("URL incorrect", writer.getContentUrl(), readerFromWriter.getContentUrl());
assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromWriter.getMimetype());
assertEquals("Encoding incorrect", writer.getEncoding(), readerFromWriter.getEncoding());
assertEquals("Locale incorrect", writer.getLocale(), readerFromWriter.getLocale());
// get another reader from the reader
ContentReader readerFromReader = readerFromWriter.getReader();
assertEquals("URL incorrect", writer.getContentUrl(), readerFromReader.getContentUrl());
assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromReader.getMimetype());
assertEquals("Encoding incorrect", writer.getEncoding(), readerFromReader.getEncoding());
assertEquals("Locale incorrect", writer.getLocale(), readerFromReader.getLocale());
// check the content
String contentCheck = readerFromWriter.getContentString();
assertEquals("Content is incorrect", content, contentCheck);
// check that the length is correct
int length = content.getBytes(writer.getEncoding()).length;
assertEquals("Reader content length is incorrect", length, readerFromWriter.getSize());
// check that the last modified time is correct
long modifiedTimeCheck = readerFromWriter.getLastModified();
// On some versionms of Linux (e.g. Centos) this test won't work as the
// modified time accuracy is only to the second.
long beforeSeconds = before/1000L;
long afterSeconds = after/1000L;
long modifiedTimeCheckSeconds = modifiedTimeCheck/1000L;
assertTrue("Reader last modified is incorrect", beforeSeconds <= modifiedTimeCheckSeconds);
assertTrue("Reader last modified is incorrect", modifiedTimeCheckSeconds <= afterSeconds);
}
public void testClosedState() throws Exception public void testClosedState() throws Exception
{ {
ContentReader reader = getReader();
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
ContentReader readerBeforeWrite = writer.getReader();
// check that streams are not flagged as closed // check that streams are not flagged as closed
assertFalse("Reader stream should not be closed", reader.isClosed()); assertFalse("Reader stream should not be closed", readerBeforeWrite.isClosed());
assertFalse("Writer stream should not be closed", writer.isClosed()); assertFalse("Writer stream should not be closed", writer.isClosed());
// check that the write doesn't supply a reader
ContentReader writerGivenReader = writer.getReader();
assertNull("No reader should be available before a write has finished", writerGivenReader);
// write some stuff // write some stuff
writer.putContent("ABC"); writer.putContent("ABC");
// check that the write has been closed // check that the write has been closed
assertTrue("Writer stream should be closed", writer.isClosed()); assertTrue("Writer stream should be closed", writer.isClosed());
// check that we can get a reader from the writer // check that we can get a reader from the writer
writerGivenReader = writer.getReader(); ContentReader readerAfterWrite = writer.getReader();
assertNotNull("No reader given by closed writer", writerGivenReader); assertNotNull("No reader given by closed writer", readerAfterWrite);
assertFalse("Readers should still be closed", reader.isClosed()); assertFalse("Before-content reader should not be affected by content updates", readerBeforeWrite.isClosed());
assertFalse("Readers should still be closed", writerGivenReader.isClosed()); assertFalse("After content reader should not be closed", readerAfterWrite.isClosed());
// check that the instance is new each time // check that the instance is new each time
ContentReader newReaderA = writer.getReader(); ContentReader newReaderA = writer.getReader();
@@ -278,180 +310,61 @@ public abstract class AbstractContentReadWriteTest extends TestCase
// check that the readers refer to the same URL // check that the readers refer to the same URL
assertEquals("Readers should refer to same URL", assertEquals("Readers should refer to same URL",
reader.getContentUrl(), writerGivenReader.getContentUrl()); readerBeforeWrite.getContentUrl(), readerAfterWrite.getContentUrl());
// read their content // read their content
String contentCheck = reader.getContentString(); try
assertEquals("Incorrect content", "ABC", contentCheck); {
contentCheck = writerGivenReader.getContentString(); readerBeforeWrite.getContentString();
}
catch (Throwable e)
{
// The content doesn't exist for this reader
}
String contentCheck = readerAfterWrite.getContentString();
assertEquals("Incorrect content", "ABC", contentCheck); assertEquals("Incorrect content", "ABC", contentCheck);
// check closed state of readers // check closed state of readers
assertTrue("Reader should be closed", reader.isClosed()); assertFalse("Before-content reader stream should not be closed", readerBeforeWrite.isClosed());
assertTrue("Reader should be closed", writerGivenReader.isClosed()); assertTrue("After-content reader should be closed after reading", readerAfterWrite.isClosed());
} }
/** public void testGetUrls()
* Checks that the store disallows concurrent writers to be issued to the same URL.
*/
@SuppressWarnings("unused")
public void testConcurrentWriteDetection() throws Exception
{ {
String contentUrl = AbstractContentStore.createNewUrl();
ContentStore store = getStore(); ContentStore store = getStore();
ContentContext contentCtx = new ContentContext(null, contentUrl);
ContentWriter firstWriter = store.getWriter(contentCtx);
try try
{ {
ContentWriter secondWriter = store.getWriter(contentCtx); store.getUrls();
fail("Store issued two writers for the same URL: " + store);
} }
catch (ContentIOException e) catch (UnsupportedOperationException e)
{ {
// expected logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
} }
ContentWriter writer = getWriter();
writer.putContent("Content for " + getName());
Set<String> contentUrls = store.getUrls();
String contentUrl = writer.getContentUrl();
assertTrue("New content not found in URL set", contentUrls.contains(contentUrl));
} }
/** public void testDeleteSimple() throws Exception
* Checks that the writer can have a listener attached
*/
public void testWriteStreamListener() throws Exception
{ {
ContentStore store = getStore();
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
writer.putContent("Content for " + getName());
final boolean[] streamClosed = new boolean[] {false}; // has to be final String contentUrl = writer.getContentUrl();
ContentStreamListener listener = new ContentStreamListener() assertTrue("Content must now exist", store.exists(contentUrl));
try
{ {
public void contentStreamClosed() throws ContentIOException store.delete(contentUrl);
{ }
streamClosed[0] = true; catch (UnsupportedOperationException e)
} {
}; logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
writer.setRetryingTransactionHelper(null); return;
writer.addListener(listener); }
assertFalse("Content must now be removed", store.exists(contentUrl));
// write some content
writer.putContent("ABC");
// check that the listener was called
assertTrue("Write stream listener was not called for the stream close", streamClosed[0]);
}
/**
* The simplest test. Write a string and read it again, checking that we receive the same values.
* If the resource accessed by {@link #getReader()} and {@link #getWriter()} is not the same, then
* values written and read won't be the same.
*/
public void testWriteAndReadString() throws Exception
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
String content = "ABC";
writer.putContent(content);
assertTrue("Stream close not detected", writer.isClosed());
String check = reader.getContentString();
assertTrue("Read and write may not share same resource", check.length() > 0);
assertEquals("Write and read didn't work", content, check);
}
public void testStringTruncation() throws Exception
{
String content = "1234567890";
ContentWriter writer = getWriter();
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
writer.setEncoding("UTF-8"); // shorter format i.t.o. bytes used
// write the content
writer.putContent(content);
// get a reader - get it in a larger format i.t.o. bytes
ContentReader reader = writer.getReader();
String checkContent = reader.getContentString(5);
assertEquals("Truncated strings don't match", "12345", checkContent);
}
public void testReadAndWriteFile() throws Exception
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
File sourceFile = File.createTempFile(getName(), ".txt");
sourceFile.deleteOnExit();
// dump some content into the temp file
String content = "ABC";
FileOutputStream os = new FileOutputStream(sourceFile);
os.write(content.getBytes());
os.flush();
os.close();
// put our temp file's content
writer.putContent(sourceFile);
assertTrue("Stream close not detected", writer.isClosed());
// create a sink temp file
File sinkFile = File.createTempFile(getName(), ".txt");
sinkFile.deleteOnExit();
// get the content into our temp file
reader.getContent(sinkFile);
// read the sink file manually
FileInputStream is = new FileInputStream(sinkFile);
byte[] buffer = new byte[100];
int count = is.read(buffer);
assertEquals("No content read", 3, count);
is.close();
String check = new String(buffer, 0, count);
assertEquals("Write out of and read into files failed", content, check);
}
public void testReadAndWriteStreamByPull() throws Exception
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
String content = "ABC";
// put the content using a stream
InputStream is = new ByteArrayInputStream(content.getBytes());
writer.putContent(is);
assertTrue("Stream close not detected", writer.isClosed());
// get the content using a stream
ByteArrayOutputStream os = new ByteArrayOutputStream(100);
reader.getContent(os);
byte[] bytes = os.toByteArray();
String check = new String(bytes);
assertEquals("Write out and read in using streams failed", content, check);
}
public void testReadAndWriteStreamByPush() throws Exception
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
String content = "ABC";
// get the content output stream
OutputStream os = writer.getContentOutputStream();
os.write(content.getBytes());
assertFalse("Stream has not been closed", writer.isClosed());
// close the stream and check again
os.close();
assertTrue("Stream close not detected", writer.isClosed());
// pull the content from a stream
InputStream is = reader.getContentInputStream();
byte[] buffer = new byte[100];
int count = is.read(buffer);
assertEquals("No content read", 3, count);
is.close();
String check = new String(buffer, 0, count);
assertEquals("Write out of and read into files failed", content, check);
} }
/** /**
@@ -459,19 +372,18 @@ public abstract class AbstractContentReadWriteTest extends TestCase
* <p> * <p>
* Only applies when {@link #getStore()} returns a value. * Only applies when {@link #getStore()} returns a value.
*/ */
public void testDelete() throws Exception public void testDeleteReaderStates() throws Exception
{ {
ContentStore store = getStore(); ContentStore store = getStore();
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
String content = "ABC"; String content = "Content for " + getName();
String contentUrl = writer.getContentUrl(); String contentUrl = writer.getContentUrl();
// write some bytes, but don't close the stream // write some bytes, but don't close the stream
OutputStream os = writer.getContentOutputStream(); OutputStream os = writer.getContentOutputStream();
os.write(content.getBytes()); os.write(content.getBytes());
os.flush(); // make sure that the bytes get persisted os.flush(); // make sure that the bytes get persisted
// close the stream // close the stream
os.close(); os.close();
@@ -539,6 +451,147 @@ public abstract class AbstractContentReadWriteTest extends TestCase
} }
} }
/**
* Checks that the writer can have a listener attached
*/
public void testWriteStreamListener() throws Exception
{
ContentWriter writer = getWriter();
final boolean[] streamClosed = new boolean[] {false}; // has to be final
ContentStreamListener listener = new ContentStreamListener()
{
public void contentStreamClosed() throws ContentIOException
{
streamClosed[0] = true;
}
};
writer.setRetryingTransactionHelper(null);
writer.addListener(listener);
// write some content
writer.putContent("ABC");
// check that the listener was called
assertTrue("Write stream listener was not called for the stream close", streamClosed[0]);
}
/**
* The simplest test. Write a string and read it again, checking that we receive the same values.
* If the resource accessed by {@link #getReader()} and {@link #getWriter()} is not the same, then
* values written and read won't be the same.
*/
public void testWriteAndReadString() throws Exception
{
ContentWriter writer = getWriter();
String content = "ABC";
writer.putContent(content);
assertTrue("Stream close not detected", writer.isClosed());
ContentReader reader = writer.getReader();
String check = reader.getContentString();
assertTrue("Read and write may not share same resource", check.length() > 0);
assertEquals("Write and read didn't work", content, check);
}
public void testStringTruncation() throws Exception
{
String content = "1234567890";
ContentWriter writer = getWriter();
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
writer.setEncoding("UTF-8"); // shorter format i.t.o. bytes used
// write the content
writer.putContent(content);
// get a reader - get it in a larger format i.t.o. bytes
ContentReader reader = writer.getReader();
String checkContent = reader.getContentString(5);
assertEquals("Truncated strings don't match", "12345", checkContent);
}
public void testReadAndWriteFile() throws Exception
{
ContentWriter writer = getWriter();
File sourceFile = File.createTempFile(getName(), ".txt");
sourceFile.deleteOnExit();
// dump some content into the temp file
String content = "ABC";
FileOutputStream os = new FileOutputStream(sourceFile);
os.write(content.getBytes());
os.flush();
os.close();
// put our temp file's content
writer.putContent(sourceFile);
assertTrue("Stream close not detected", writer.isClosed());
// create a sink temp file
File sinkFile = File.createTempFile(getName(), ".txt");
sinkFile.deleteOnExit();
// get the content into our temp file
ContentReader reader = writer.getReader();
reader.getContent(sinkFile);
// read the sink file manually
FileInputStream is = new FileInputStream(sinkFile);
byte[] buffer = new byte[100];
int count = is.read(buffer);
assertEquals("No content read", 3, count);
is.close();
String check = new String(buffer, 0, count);
assertEquals("Write out of and read into files failed", content, check);
}
public void testReadAndWriteStreamByPull() throws Exception
{
ContentWriter writer = getWriter();
String content = "ABC";
// put the content using a stream
InputStream is = new ByteArrayInputStream(content.getBytes());
writer.putContent(is);
assertTrue("Stream close not detected", writer.isClosed());
// get the content using a stream
ByteArrayOutputStream os = new ByteArrayOutputStream(100);
ContentReader reader = writer.getReader();
reader.getContent(os);
byte[] bytes = os.toByteArray();
String check = new String(bytes);
assertEquals("Write out and read in using streams failed", content, check);
}
public void testReadAndWriteStreamByPush() throws Exception
{
ContentWriter writer = getWriter();
String content = "ABC";
// get the content output stream
OutputStream os = writer.getContentOutputStream();
os.write(content.getBytes());
assertFalse("Stream has not been closed", writer.isClosed());
// close the stream and check again
os.close();
assertTrue("Stream close not detected", writer.isClosed());
// pull the content from a stream
ContentReader reader = writer.getReader();
InputStream is = reader.getContentInputStream();
byte[] buffer = new byte[100];
int count = is.read(buffer);
assertEquals("No content read", 3, count);
is.close();
String check = new String(buffer, 0, count);
assertEquals("Write out of and read into files failed", content, check);
}
/** /**
* Tests retrieval of all content URLs * Tests retrieval of all content URLs
* <p> * <p>
@@ -547,7 +600,17 @@ public abstract class AbstractContentReadWriteTest extends TestCase
public void testListUrls() throws Exception public void testListUrls() throws Exception
{ {
ContentStore store = getStore(); ContentStore store = getStore();
// Ensure that this test can be done
try
{
store.getUrls();
}
catch (UnsupportedOperationException e)
{
logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
}
// Proceed with the test
ContentWriter writer = getWriter(); ContentWriter writer = getWriter();
Set<String> contentUrls = store.getUrls(); Set<String> contentUrls = store.getUrls();
@@ -566,14 +629,6 @@ public abstract class AbstractContentReadWriteTest extends TestCase
// check that the query for content created before this time yesterday doesn't return the URL // check that the query for content created before this time yesterday doesn't return the URL
contentUrls = store.getUrls(null, yesterday); contentUrls = store.getUrls(null, yesterday);
assertFalse("URL was younger than required, but still shows up", contentUrls.contains(contentUrl)); assertFalse("URL was younger than required, but still shows up", contentUrls.contains(contentUrl));
// delete the content
boolean deleted = store.delete(contentUrl);
if (deleted)
{
contentUrls = store.getUrls();
assertFalse("Successfully deleted URL still shown by store", contentUrls.contains(contentUrl));
}
} }
/** /**
@@ -623,7 +678,7 @@ 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
ContentContext writerTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl()); ContentContext writerTruncateCtx = new ContentContext(writer.getReader(), null);
ContentWriter writerTruncate = getStore().getWriter(writerTruncateCtx); 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
@@ -632,7 +687,7 @@ 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
ContentContext writerNoTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl()); ContentContext writerNoTruncateCtx = new ContentContext(writer.getReader(), null);
ContentWriter writerNoTruncate = getStore().getWriter(writerNoTruncateCtx); 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

View File

@@ -61,15 +61,32 @@ public class ContentDataTest extends TestCase
ContentData checkProperty = ContentData.createContentProperty(propertyStr); ContentData checkProperty = ContentData.createContentProperty(propertyStr);
assertEquals("Conversion from string failed", property, checkProperty); assertEquals("Conversion from string failed", property, checkProperty);
property = new ContentData("uuu", "mmm", 123L, "eee", I18NUtil.getLocale()); property = new ContentData("test://uuu", "mmm", 123L, "eee", I18NUtil.getLocale());
// convert to a string // convert to a string
propertyStr = property.toString(); propertyStr = property.toString();
assertEquals("Incorrect property string representation", assertEquals("Incorrect property string representation",
"contentUrl=uuu|mimetype=mmm|size=123|encoding=eee|locale=" + localeStr, propertyStr); "contentUrl=test://uuu|mimetype=mmm|size=123|encoding=eee|locale=" + localeStr, propertyStr);
// convert back // convert back
checkProperty = ContentData.createContentProperty(propertyStr); checkProperty = ContentData.createContentProperty(propertyStr);
assertEquals("Conversion from string failed", property, checkProperty); assertEquals("Conversion from string failed", property, checkProperty);
} }
public void testEquals()
{
ContentData contentData1 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_BINARY, 600L, "UTF-8", Locale.ENGLISH);
ContentData contentData2 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_BINARY, 600L, "UTF-8", Locale.ENGLISH);
ContentData contentData3 = new ContentData("abc://XXX", MimetypeMap.MIMETYPE_BINARY, 600L, "UTF-8", Locale.ENGLISH);
ContentData contentData4 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_TEXT_PLAIN, 600L, "UTF-8", Locale.ENGLISH);
ContentData contentData5 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_BINARY, 500L, "UTF-8", Locale.ENGLISH);
ContentData contentData6 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_BINARY, 600L, "UTF-16", Locale.ENGLISH);
ContentData contentData7 = new ContentData("abc://xxx", MimetypeMap.MIMETYPE_BINARY, 600L, "UTF-8", Locale.CHINESE);
assertEquals(contentData1, contentData2);
assertNotSame(contentData1, contentData3);
assertNotSame(contentData1, contentData4);
assertNotSame(contentData1, contentData5);
assertNotSame(contentData1, contentData6);
assertNotSame(contentData1, contentData7);
}
} }

View File

@@ -0,0 +1,76 @@
/*
* 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 org.alfresco.error.AlfrescoRuntimeException;
/**
* Exception produced when a request is made to write content to a location
* already in use, either by content being written or previously written.
*
* @see ContentStore#getWriter(ContentContext)
* @since 2.1
* @author Derek Hulley
*/
public class ContentExistsException extends AlfrescoRuntimeException
{
private static final long serialVersionUID = 5154068664249490612L;
private ContentStore contentStore;
private String contentUrl;
/**
* @param contentStore the originating content store
* @param contentUrl the offending content URL
*/
public ContentExistsException(ContentStore contentStore, String contentUrl)
{
this(contentStore, contentUrl,
"Content with the given URL already exists in the store: \n" +
" Store: " + contentStore.getClass().getName() + "\n" +
" Content URL: " + contentUrl);
}
/**
* @param contentStore the originating content store
* @param contentUrl the offending content URL
*/
public ContentExistsException(ContentStore contentStore, String contentUrl, String msg)
{
super(msg);
this.contentStore = contentStore;
this.contentUrl = contentUrl;
}
public ContentStore getContentStore()
{
return contentStore;
}
public String getContentUrl()
{
return contentUrl;
}
}

View File

@@ -42,7 +42,9 @@ import org.alfresco.service.cmr.repository.ContentWriter;
* providing persistence and retrieval of the content against a * providing persistence and retrieval of the content against a
* <code>content URL</code>. * <code>content URL</code>.
* <p> * <p>
* The URL format is <b>store://year/month/day/GUID.bin</b> <br> * Content URLs must consist of a prefix or protocol followed by an
* implementation-specific identifier. For example, the content URL format
* for file stores is <b>store://year/month/day/GUID.bin</b> <br>
* <ul> * <ul>
* <li> <b>store://</b>: prefix identifying an Alfresco content stores * <li> <b>store://</b>: prefix identifying an Alfresco content stores
* regardless of the persistence mechanism. </li> * regardless of the persistence mechanism. </li>
@@ -53,16 +55,53 @@ import org.alfresco.service.cmr.repository.ContentWriter;
* <li> <b>minute</b>: 0-based minute of the hour </li> * <li> <b>minute</b>: 0-based minute of the hour </li>
* <li> <b>GUID</b>: A unique identifier </li> * <li> <b>GUID</b>: A unique identifier </li>
* </ul> * </ul>
* The old <b>file://</b> prefix must still be supported - and functionality * <p>
* around this can be found in the {@link org.alfresco.repo.content.AbstractContentStore} * Where the store cannot handle a particular content URL request, the
* implementation. * {@link UnsupportedContentUrlException} must be generated. This will allow
* various implementations to provide fallback code to other stores where
* possible.
* <p>
* Where a store cannot serve a particular request because the functionality
* is just not available, the <code>UnsupportedOperationException</code> should
* be thrown. Once again, there may be fallback handling provided for these
* situations.
* *
* @since 1.0
* @author Derek Hulley * @author Derek Hulley
*/ */
public interface ContentStore public interface ContentStore
{ {
/** <b>store://</b> is the new prefix for all content URLs */ /**
public static final String STORE_PROTOCOL = "store://"; * An empty content context used to retrieve completely new content.
*
* @see ContentStore#getWriter(ContentContext)
*/
public static final ContentContext NEW_CONTENT_CONTEXT = new ContentContext(null, null);
/**
* The delimiter that must be found in all URLS, i.e <b>://</b>
*/
public static final String PROTOCOL_DELIMITER = "://";
/**
* Check if the content URL format is supported by the store.
*
* @param contentUrl the content URL to check
* @return Returns <tt>true</tt> if none of the other methods on the store
* will throw an {@link UnsupportedContentUrlException} when given
* this URL.
*
* @since 2.1
*/
public boolean isContentUrlSupported(String contentUrl);
/**
* Check if the store supports write requests.
*
* @return Return true is the store supports write operations
*
* @since 2.1
*/
public boolean isWriteSupported();
/** /**
* Check for the existence of content in the store. * Check for the existence of content in the store.
@@ -71,30 +110,37 @@ 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
* @return Returns true if the content exists, otherwise * the path to the content
* false if the content doesn't exist or if the URL * @return
* is not applicable to this store. * Returns true if the content exists, otherwise false if the content doesn't
* exist or <b>if the URL is not applicable to this store</b>.
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentIOException * @throws ContentIOException
* if an IO error occurs
* *
* @see ContentReader#exists() * @see ContentReader#exists()
*/ */
public boolean exists(String contentUrl) throws ContentIOException; public boolean exists(String contentUrl);
/** /**
* Get the accessor with which to read from the content at the given URL. * Get the accessor with which to read from the content at the given URL.
* The reader is <b>stateful</b> and can <b>only be used once</b>. * The reader is <b>stateful</b> and can <b>only be used once</b>.
* *
* @param contentUrl the path to where the content is located * @param contentUrl the path to where the content is located
* @return Returns a read-only content accessor for the given URL. There may * @return Returns a read-only content accessor for the given URL. There may
* be no content at the given URL, but the reader must still be returned. * be no content at the given URL, but the reader must still be returned.
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentIOException * @throws ContentIOException
* if an IO error occurs
* *
* @see #exists(String) * @see #exists(String)
* @see ContentReader#exists() * @see ContentReader#exists()
* @see EmptyContentReader * @see EmptyContentReader
*/ */
public ContentReader getReader(String contentUrl) throws ContentIOException; public ContentReader getReader(String contentUrl);
/** /**
* Get an accessor with which to write content to a location * Get an accessor with which to write content to a location
@@ -110,15 +156,24 @@ public interface ContentStore
* can enable this by copying the existing content into the new location * can enable this by copying the existing content into the new location
* before supplying a writer onto the new content. * before supplying a writer onto the new content.
* *
* @param context the context of content. * @param context
* @return Returns a write-only content accessor * the context of content.
* @throws ContentIOException if completely new content storage could not be created * @return
* Returns a write-only content accessor
* @throws UnsupportedOperationException
* if the store is unable to provide the information
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentExistsException
* if the content URL is already in use
* @throws ContentIOException
* if an IO error occurs
* *
* @see #getWriter(ContentReader, String) * @see #getWriteSupport()
* @see ContentWriter#addListener(ContentStreamListener) * @see ContentWriter#addListener(ContentStreamListener)
* @see ContentWriter#getContentUrl() * @see ContentWriter#getContentUrl()
*/ */
public ContentWriter getWriter(ContentContext context) throws ContentIOException; public ContentWriter getWriter(ContentContext context);
/** /**
* Shortcut method to {@link #getWriter(ContentContext)}. * Shortcut method to {@link #getWriter(ContentContext)}.
@@ -127,26 +182,37 @@ public interface ContentStore
* *
* @deprecated * @deprecated
*/ */
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException; public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl);
/** /**
* Get all URLs for the store, regardless of creation time. * Get all URLs for the store, regardless of creation time.
* @return
* Returns a set of all unique content URLs in the store
* @throws ContentIOException
* if an IO error occurs
* @throws UnsupportedOperationException
* if the store is unable to provide the information
* *
* @see #getUrls(Date, Date) * @see #getUrls(Date, Date)
*/ */
public Set<String> getUrls() throws ContentIOException; public Set<String> getUrls();
/** /**
* Get a set of all content URLs in the store. This indicates all content * Get a set of all content URLs in the store. This indicates all content
* available for reads. * available for reads.
* *
* @param createdAfter all URLs returned must have been created after this date. May be null. * @param createdAfter
* @param createdBefore all URLs returned must have been created before this date. May be null. * all URLs returned must have been created after this date. May be null.
* @return Returns a complete set of the unique URLs of all available content * @param createdBefore
* in the store * all URLs returned must have been created before this date. May be null.
* @return
* Returns a complete set of the unique URLs of all available content in the store
* @throws UnsupportedOperationException
* if the store is unable to provide the information
* @throws ContentIOException * @throws ContentIOException
* if an IO error occurs
*/ */
public Set<String> getUrls(Date createdAfter, Date createdBefore) throws ContentIOException; public Set<String> getUrls(Date createdAfter, Date createdBefore);
/** /**
* Deletes the content at the given URL. * Deletes the content at the given URL.
@@ -154,11 +220,17 @@ public interface ContentStore
* A delete cannot be forced since it is much better to have the * A delete cannot be forced since it is much better to have the
* file remain longer than desired rather than deleted prematurely. * file remain longer than desired rather than deleted prematurely.
* *
* @param contentUrl the URL of the content to delete * @param contentUrl
* @return Return true if the content was deleted (either by this or * the URL of the content to delete
* another operation), otherwise false. If the content no longer * @return
* exists, then <tt>true</tt> is returned. * Returns <tt>true</tt> if the content was deleted (either by this or another operation),
* otherwise false. If the content no longer exists, then <tt>true</tt> is returned.
* @throws UnsupportedOperationException
* if the store is unable to perform the action
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentIOException * @throws ContentIOException
* if an IO error occurs
*/ */
public boolean delete(String contentUrl) throws ContentIOException; public boolean delete(String contentUrl);
} }

View File

@@ -27,6 +27,7 @@ package org.alfresco.repo.content;
import org.alfresco.repo.content.cleanup.ContentStoreCleanerTest; import org.alfresco.repo.content.cleanup.ContentStoreCleanerTest;
import org.alfresco.repo.content.filestore.FileContentStoreTest; import org.alfresco.repo.content.filestore.FileContentStoreTest;
import org.alfresco.repo.content.filestore.NoRandomAccessFileContentStoreTest; import org.alfresco.repo.content.filestore.NoRandomAccessFileContentStoreTest;
import org.alfresco.repo.content.filestore.ReadOnlyFileContentStoreTest;
import org.alfresco.repo.content.metadata.HtmlMetadataExtracterTest; import org.alfresco.repo.content.metadata.HtmlMetadataExtracterTest;
import org.alfresco.repo.content.metadata.MappingMetadataExtracterTest; import org.alfresco.repo.content.metadata.MappingMetadataExtracterTest;
import org.alfresco.repo.content.metadata.OfficeMetadataExtracterTest; import org.alfresco.repo.content.metadata.OfficeMetadataExtracterTest;
@@ -63,6 +64,7 @@ public class ContentTestSuite extends TestSuite
suite.addTestSuite(ContentStoreCleanerTest.class); suite.addTestSuite(ContentStoreCleanerTest.class);
suite.addTestSuite(FileContentStoreTest.class); suite.addTestSuite(FileContentStoreTest.class);
suite.addTestSuite(NoRandomAccessFileContentStoreTest.class); suite.addTestSuite(NoRandomAccessFileContentStoreTest.class);
suite.addTestSuite(ReadOnlyFileContentStoreTest.class);
suite.addTestSuite(MappingMetadataExtracterTest.class); suite.addTestSuite(MappingMetadataExtracterTest.class);
suite.addTestSuite(HtmlMetadataExtracterTest.class); suite.addTestSuite(HtmlMetadataExtracterTest.class);
suite.addTestSuite(OfficeMetadataExtracterTest.class); suite.addTestSuite(OfficeMetadataExtracterTest.class);

View File

@@ -272,7 +272,16 @@ public class RoutingContentService implements ContentService
/** {@inheritDoc} */ /** {@inheritDoc} */
public ContentReader getRawReader(String contentUrl) public ContentReader getRawReader(String contentUrl)
{ {
ContentReader reader = store.getReader(contentUrl); ContentReader reader = null;
try
{
reader = store.getReader(contentUrl);
}
catch (UnsupportedContentUrlException e)
{
// The URL is not supported, so we spoof it
reader = new EmptyContentReader(contentUrl);
}
if (reader == null) if (reader == null)
{ {
throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders");

View File

@@ -271,7 +271,7 @@ public class RoutingContentServiceTest extends TestCase
public void testGetRawReader() throws Exception public void testGetRawReader() throws Exception
{ {
ContentReader reader = contentService.getRawReader("blah"); ContentReader reader = contentService.getRawReader("test://non-existence");
assertNotNull("A reader is expected with content URL referencing no content", reader); assertNotNull("A reader is expected with content URL referencing no content", reader);
assertFalse("Reader should not have any content", reader.exists()); assertFalse("Reader should not have any content", reader.exists());
// Now write something // Now write something

View File

@@ -26,10 +26,9 @@ package org.alfresco.repo.content;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random;
import junit.framework.TestCase;
import net.sf.ehcache.Cache; import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager; import net.sf.ehcache.CacheManager;
@@ -40,22 +39,27 @@ import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider; import org.alfresco.util.TempFileProvider;
/** /**
* Ensures that the routing of URLs based on context is working. * Ensures that the routing of URLs based on context is working. A combination
* of fully featured and incompletely featured stores is used to ensure that
* all routing scenarios are handled.
* *
* @see AbstractRoutingContentStore * @see AbstractRoutingContentStore
* @since 2.1 * @since 2.1
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
public class RoutingContentStoreTest extends TestCase public class RoutingContentStoreTest extends AbstractWritableContentStoreTest
{ {
private ContentStore storeA; private ContentStore storeA;
private ContentStore storeB; private ContentStore storeB;
private ContentStore storeC;
private ContentStore storeD;
private ContentStore routingStore; private ContentStore routingStore;
@Override @Override
protected void setUp() throws Exception public void setUp() throws Exception
{ {
super.setUp();
File tempDir = TempFileProvider.getTempDir(); File tempDir = TempFileProvider.getTempDir();
// Create a subdirectory for A // Create a subdirectory for A
File storeADir = new File(tempDir, "A"); File storeADir = new File(tempDir, "A");
@@ -63,14 +67,23 @@ public class RoutingContentStoreTest extends TestCase
// Create a subdirectory for B // Create a subdirectory for B
File storeBDir = new File(tempDir, "B"); File storeBDir = new File(tempDir, "B");
storeB = new FileContentStore(storeBDir); storeB = new FileContentStore(storeBDir);
// Create a subdirectory for C
File storeCDir = new File(tempDir, "C");
storeC = new DumbReadOnlyFileStore(new FileContentStore(storeCDir));
// No subdirectory for D
storeD = new SupportsNoUrlFormatStore();
// Create the routing store // Create the routing store
routingStore = new RandomRoutingContentStore(storeA, storeB); routingStore = new RandomRoutingContentStore(storeA, storeB, storeC, storeD);
}
@Override
protected ContentStore getStore()
{
return routingStore;
} }
public void testSetUp() throws Exception public void testSetUp() throws Exception
{ {
assertNotNull(storeA);
assertNotNull(storeB);
assertNotNull(routingStore); assertNotNull(routingStore);
} }
@@ -96,7 +109,9 @@ public class RoutingContentStoreTest extends TestCase
*/ */
public void testMissingUrl() public void testMissingUrl()
{ {
ContentReader reader = routingStore.getReader("blah"); String missingContentUrl = FileContentStore.createNewFileStoreUrl();
ContentReader reader = routingStore.getReader(missingContentUrl);
assertNotNull("Missing URL should not return null", reader); assertNotNull("Missing URL should not return null", reader);
assertFalse("Empty reader should say content doesn't exist.", reader.exists()); assertFalse("Empty reader should say content doesn't exist.", reader.exists());
try try
@@ -110,7 +125,7 @@ public class RoutingContentStoreTest extends TestCase
} }
} }
public void testHandlingInCache() public void testGeneralUse()
{ {
for (int i = 0 ; i < 20; i++) for (int i = 0 ; i < 20; i++)
{ {
@@ -129,14 +144,6 @@ public class RoutingContentStoreTest extends TestCase
} }
} }
/**
* 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. * 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. * Matching of content URLs back to the stores is handled by the base class.
@@ -146,11 +153,9 @@ public class RoutingContentStoreTest extends TestCase
private static class RandomRoutingContentStore extends AbstractRoutingContentStore private static class RandomRoutingContentStore extends AbstractRoutingContentStore
{ {
private List<ContentStore> stores; private List<ContentStore> stores;
private Random random;
public RandomRoutingContentStore(ContentStore ... stores) public RandomRoutingContentStore(ContentStore ... stores)
{ {
this.random = new Random();
this.stores = new ArrayList<ContentStore>(5); this.stores = new ArrayList<ContentStore>(5);
for (ContentStore store : stores) for (ContentStore store : stores)
{ {
@@ -173,9 +178,66 @@ public class RoutingContentStoreTest extends TestCase
@Override @Override
protected ContentStore selectWriteStore(ContentContext ctx) protected ContentStore selectWriteStore(ContentContext ctx)
{ {
int size = stores.size(); // Shuffle the list of writable stores
int index = (int) Math.floor(random.nextDouble() * (double) size); List<ContentStore> shuffled = new ArrayList<ContentStore>(stores);
return stores.get(index); Collections.shuffle(shuffled);
// Pick the first writable store
for (ContentStore store : shuffled)
{
if (store.isWriteSupported())
{
return store;
}
}
// Nothing found
fail("A request came for a writer when there is no writable store to choose from");
return null;
}
}
/**
* The simplest possible store.
*
* @author Derek Hulley
*/
private static class DumbReadOnlyFileStore extends AbstractContentStore
{
FileContentStore fileStore;
public DumbReadOnlyFileStore(FileContentStore fileStore)
{
this.fileStore = fileStore;
}
public boolean isWriteSupported()
{
return false;
}
public ContentReader getReader(String contentUrl)
{
return fileStore.getReader(contentUrl);
}
}
/**
* This store supports nothing. It is designed to catch the routing code out.
*
* @author Derek Hulley
*/
private static class SupportsNoUrlFormatStore extends AbstractContentStore
{
public SupportsNoUrlFormatStore()
{
}
public boolean isWriteSupported()
{
return false;
}
public ContentReader getReader(String contentUrl)
{
throw new UnsupportedContentUrlException(this, contentUrl);
} }
} }
} }

View File

@@ -0,0 +1,76 @@
/*
* 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 org.alfresco.error.AlfrescoRuntimeException;
/**
* Exception produced when a content URL is not supported by a particular
* {@link ContentStore} implementation.
*
* @see ContentStore#getWriter(ContentContext)
* @since 2.1
* @author Derek Hulley
*/
public class UnsupportedContentUrlException extends AlfrescoRuntimeException
{
private static final long serialVersionUID = 1349903839801739376L;
private ContentStore contentStore;
private String contentUrl;
/**
* @param contentStore the originating content store
* @param contentUrl the offending content URL
*/
public UnsupportedContentUrlException(ContentStore contentStore, String contentUrl)
{
this(contentStore, contentUrl,
"The content URL is not supported by the content store: \n" +
" Store: " + contentStore.getClass().getName() + "\n" +
" Content URL: " + contentUrl);
}
/**
* @param contentStore the originating content store
* @param contentUrl the offending content URL
*/
public UnsupportedContentUrlException(ContentStore contentStore, String contentUrl, String msg)
{
super(msg);
this.contentStore = contentStore;
this.contentUrl = contentUrl;
}
public ContentStore getContentStore()
{
return contentStore;
}
public String getContentUrl()
{
return contentUrl;
}
}

View File

@@ -40,6 +40,7 @@ import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
@@ -220,7 +221,16 @@ public class ContentStoreCleaner
// now clean each store in turn // now clean each store in turn
for (ContentStore store : stores) for (ContentStore store : stores)
{ {
clean(validUrls, store); try
{
clean(validUrls, store);
}
catch (UnsupportedOperationException e)
{
throw new ContentIOException(
"Unable to clean store as the necessary operations are not supported: " + store,
e);
}
} }
} }

View File

@@ -88,7 +88,7 @@ public class ContentStoreCleanerTest extends TestCase
{ {
cleaner.setProtectDays(0); cleaner.setProtectDays(0);
// add some content to the store // add some content to the store
ContentWriter writer = store.getWriter(null, null); ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("ABC"); writer.putContent("ABC");
String contentUrl = writer.getContentUrl(); String contentUrl = writer.getContentUrl();
@@ -104,7 +104,7 @@ public class ContentStoreCleanerTest extends TestCase
{ {
cleaner.setProtectDays(1); cleaner.setProtectDays(1);
// add some content to the store // add some content to the store
ContentWriter writer = store.getWriter(null, null); ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("ABC"); writer.putContent("ABC");
String contentUrl = writer.getContentUrl(); String contentUrl = writer.getContentUrl();

View File

@@ -34,6 +34,7 @@ import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat; import java.text.MessageFormat;
import org.alfresco.repo.content.AbstractContentReader; import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
@@ -119,7 +120,9 @@ public class FileContentReader extends AbstractContentReader
*/ */
public FileContentReader(File file) public FileContentReader(File file)
{ {
this(file, FileContentStore.STORE_PROTOCOL + file.getAbsolutePath()); this(
file,
FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + file.getAbsolutePath());
} }
/** /**

View File

@@ -26,16 +26,23 @@ package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentStore; import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.EmptyContentReader;
import org.alfresco.repo.content.UnsupportedContentUrlException;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -50,11 +57,18 @@ import org.apache.commons.logging.LogFactory;
*/ */
public class FileContentStore extends AbstractContentStore public class FileContentStore extends AbstractContentStore
{ {
/**
* <b>store</b> is the new prefix for file content URLs
* @see ContentStore#PROTOCOL_DELIMITER
*/
public static final String STORE_PROTOCOL = "store";
private static final Log logger = LogFactory.getLog(FileContentStore.class); private static final Log logger = LogFactory.getLog(FileContentStore.class);
private File rootDirectory; private File rootDirectory;
private String rootAbsolutePath; private String rootAbsolutePath;
private boolean allowRandomAccess; private boolean allowRandomAccess;
private boolean readOnly;
/** /**
* @param rootDirectoryStr the root under which files will be stored. * @param rootDirectoryStr the root under which files will be stored.
@@ -83,6 +97,7 @@ public class FileContentStore extends AbstractContentStore
this.rootDirectory = rootDirectory.getAbsoluteFile(); this.rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath(); rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true; allowRandomAccess = true;
readOnly = false;
} }
public String toString() public String toString()
@@ -90,6 +105,8 @@ public class FileContentStore extends AbstractContentStore
StringBuilder sb = new StringBuilder(36); StringBuilder sb = new StringBuilder(36);
sb.append("FileContentStore") sb.append("FileContentStore")
.append("[ root=").append(rootDirectory) .append("[ root=").append(rootDirectory)
.append(", allowRandomAccess=").append(allowRandomAccess)
.append(", readOnly=").append(readOnly)
.append("]"); .append("]");
return sb.toString(); return sb.toString();
} }
@@ -110,6 +127,18 @@ public class FileContentStore extends AbstractContentStore
this.allowRandomAccess = allowRandomAccess; this.allowRandomAccess = allowRandomAccess;
} }
/**
* File stores may optionally be declared read-only. This is useful when configuring
* a store, possibly temporarily, to act as a source of data but to preserve it against
* any writes.
*
* @param readOnly <tt>true</tt> to force the store to only allow reads.
*/
public void setReadOnly(boolean readOnly)
{
this.readOnly = readOnly;
}
/** /**
* Generates a new URL and file appropriate to it. * Generates a new URL and file appropriate to it.
* *
@@ -118,7 +147,7 @@ public class FileContentStore extends AbstractContentStore
*/ */
private File createNewFile() throws IOException private File createNewFile() throws IOException
{ {
String contentUrl = createNewUrl(); String contentUrl = FileContentStore.createNewFileStoreUrl();
return createNewFile(contentUrl); return createNewFile(contentUrl);
} }
@@ -131,11 +160,20 @@ public class FileContentStore extends AbstractContentStore
* *
* @param newContentUrl the specific URL to use, which may not be in use * @param newContentUrl the specific URL to use, which may not be in use
* @return Returns a new and unique file * @return Returns a new and unique file
* @throws IOException if the file or parent directories couldn't be created or * @throws IOException
* if the URL is already in use. * if the file or parent directories couldn't be created or if the URL is already in use.
* @throws UnsupportedOperationException
* if the store is read-only
*
* @see #setReadOnly(boolean)
*/ */
public File createNewFile(String newContentUrl) throws IOException public File createNewFile(String newContentUrl) throws IOException
{ {
if (readOnly)
{
throw new UnsupportedOperationException("This store is currently read-only: " + this);
}
File file = makeFile(newContentUrl); File file = makeFile(newContentUrl);
// create the directory, if it doesn't exist // create the directory, if it doesn't exist
@@ -185,7 +223,7 @@ public class FileContentStore extends AbstractContentStore
index++; index++;
} }
// strip off the root path and adds the protocol prefix // strip off the root path and adds the protocol prefix
String url = AbstractContentStore.STORE_PROTOCOL + path.substring(index); String url = FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + path.substring(index);
// replace '\' with '/' so that URLs are consistent across all filesystems // replace '\' with '/' so that URLs are consistent across all filesystems
url = url.replace('\\', '/'); url = url.replace('\\', '/');
// done // done
@@ -195,34 +233,45 @@ public class FileContentStore extends AbstractContentStore
/** /**
* Creates a file from the given relative URL. * Creates a file from the given relative URL.
* *
* @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
* exist * exist
* @throws UnsupportedContentUrlException
* if the URL is invalid and doesn't support the
* {@link FileContentStore#STORE_PROTOCOL correct protocol}
* *
* @see #checkUrl(String) * @see #checkUrl(String)
*/ */
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 = FileContentStore.getRelativePart(contentUrl); Pair<String, String> urlParts = super.getContentUrlParts(contentUrl);
if (relativeUrl == null) String protocol = urlParts.getFirst();
String relativePath = urlParts.getSecond();
// Check the protocol
if (!protocol.equals(FileContentStore.STORE_PROTOCOL))
{ {
throw new ContentIOException( throw new UnsupportedContentUrlException(this, contentUrl);
"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, relativePath);
// done // done
return file; return file;
} }
/**
* @return Returns <tt>true</tt> always
*/
public boolean isWriteSupported()
{
return true;
}
/** /**
* Performs a direct check against the file for its existence. * Performs a direct check against the file for its existence.
*/ */
@Override @Override
public boolean exists(String contentUrl) throws ContentIOException public boolean exists(String contentUrl)
{ {
File file = makeFile(contentUrl); File file = makeFile(contentUrl);
return file.exists(); return file.exists();
@@ -237,8 +286,17 @@ public class FileContentStore extends AbstractContentStore
try try
{ {
File file = makeFile(contentUrl); File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl); ContentReader reader = null;
reader.setAllowRandomAccess(allowRandomAccess); if (file.exists())
{
FileContentReader fileContentReader = new FileContentReader(file, contentUrl);
fileContentReader.setAllowRandomAccess(allowRandomAccess);
reader = fileContentReader;
}
else
{
reader = new EmptyContentReader(contentUrl);
}
// done // done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
@@ -250,6 +308,11 @@ public class FileContentStore extends AbstractContentStore
} }
return reader; return reader;
} }
catch (UnsupportedContentUrlException e)
{
// This can go out directly
throw e;
}
catch (Throwable e) catch (Throwable e)
{ {
throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e); throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e);
@@ -259,10 +322,8 @@ public class FileContentStore extends AbstractContentStore
/** /**
* @return Returns a writer onto a location based on the date * @return Returns a writer onto a location based on the date
*/ */
public ContentWriter getWriter(ContentContext ctx) public ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl)
{ {
ContentReader existingContentReader = ctx.getExistingContentReader();
String newContentUrl = ctx.getContentUrl();
try try
{ {
File file = null; File file = null;
@@ -358,9 +419,17 @@ public class FileContentStore extends AbstractContentStore
/** /**
* Attempts to delete the content. The actual deletion is optional on the interface * Attempts to delete the content. The actual deletion is optional on the interface
* so it just returns the success or failure of the underlying delete. * so it just returns the success or failure of the underlying delete.
*
* @throws UnsupportedOperationException if the store is read-only
*
* @see #setReadOnly(boolean)
*/ */
public boolean delete(String contentUrl) throws ContentIOException public boolean delete(String contentUrl)
{ {
if (readOnly)
{
throw new UnsupportedOperationException("This store is currently read-only: " + this);
}
// ignore files that don't exist // ignore files that don't exist
File file = makeFile(contentUrl); File file = makeFile(contentUrl);
boolean deleted = false; boolean deleted = false;
@@ -384,45 +453,31 @@ public class FileContentStore extends AbstractContentStore
} }
/** /**
* This method can be used to ensure that URLs conform to the required format. * Creates a new content URL. This must be supported by all
* If subclasses have to parse the URL, then a call to this may not be required - * stores that are compatible with Alfresco.
* 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 a new and unique content URL
* @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) public static String createNewFileStoreUrl()
{ {
int index = 0; Calendar calendar = new GregorianCalendar();
if (contentUrl.startsWith(STORE_PROTOCOL)) int year = calendar.get(Calendar.YEAR);
{ int month = calendar.get(Calendar.MONTH) + 1; // 0-based
index = 8; int day = calendar.get(Calendar.DAY_OF_MONTH);
} int hour = calendar.get(Calendar.HOUR_OF_DAY);
else if (contentUrl.startsWith("file://")) int minute = calendar.get(Calendar.MINUTE);
{ // create the URL
index = 7; StringBuilder sb = new StringBuilder(20);
} sb.append(FileContentStore.STORE_PROTOCOL)
else .append(ContentStore.PROTOCOL_DELIMITER)
{ .append(year).append('/')
if (contentUrl.length() == 0) .append(month).append('/')
{ .append(day).append('/')
throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl); .append(hour).append('/')
} .append(minute).append('/')
return contentUrl; .append(GUID.generate()).append(".bin");
} String newContentUrl = sb.toString();
// done
// extract the relative part of the URL return newContentUrl;
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

@@ -25,11 +25,13 @@
package org.alfresco.repo.content.filestore; package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import java.nio.ByteBuffer;
import org.alfresco.repo.content.AbstractContentReadWriteTest; import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentExistsException;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.util.TempFileProvider; import org.alfresco.util.TempFileProvider;
/** /**
@@ -39,7 +41,7 @@ import org.alfresco.util.TempFileProvider;
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
public class FileContentStoreTest extends AbstractContentReadWriteTest public class FileContentStoreTest extends AbstractWritableContentStoreTest
{ {
private FileContentStore store; private FileContentStore store;
@@ -62,40 +64,28 @@ public class FileContentStoreTest extends AbstractContentReadWriteTest
return store; return store;
} }
public void testGetSafeContentReader() throws Exception /**
* Checks that the store disallows concurrent writers to be issued to the same URL.
*/
@SuppressWarnings("unused")
public void testConcurrentWriteDetection() throws Exception
{ {
String template = "ABC {0}{1}"; ByteBuffer buffer = ByteBuffer.wrap("Something".getBytes());
String arg0 = "DEF"; ContentStore store = getStore();
String arg1 = "123";
String fakeContent = "ABC DEF123";
// get a good reader ContentContext firstContentCtx = ContentStore.NEW_CONTENT_CONTEXT;
ContentReader reader = getReader(); ContentWriter firstWriter = store.getWriter(firstContentCtx);
assertFalse("No content has been written to the URL yet", reader.exists()); String contentUrl = firstWriter.getContentUrl();
// now create a file for it ContentContext secondContentCtx = new ContentContext(null, contentUrl);
File file = store.createNewFile(reader.getContentUrl()); try
assertTrue("File store did not connect new file", file.exists()); {
assertTrue("Reader did not detect creation of the underlying file", reader.exists()); ContentWriter secondWriter = store.getWriter(secondContentCtx);
fail("Store must disallow more than one writer onto the same content URL: " + store);
// remove the underlying content }
file.delete(); catch (ContentExistsException e)
assertFalse("File not missing", file.exists()); {
assertFalse("Reader doesn't show missing content", reader.exists()); // expected
}
// make a safe reader
ContentReader safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1);
// check it
assertTrue("Fake content doesn't exist", safeReader.exists());
assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString());
assertEquals("Fake mimetype incorrect", MimetypeMap.MIMETYPE_TEXT_PLAIN, safeReader.getMimetype());
assertEquals("Fake encoding incorrect", "UTF-8", safeReader.getEncoding());
// now repeat with a null reader
reader = null;
safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1);
// check it
assertTrue("Fake content doesn't exist", safeReader.exists());
assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString());
} }
} }

View File

@@ -33,6 +33,7 @@ import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import org.alfresco.repo.content.AbstractContentWriter; import org.alfresco.repo.content.AbstractContentWriter;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@@ -60,10 +61,7 @@ public class FileContentWriter extends AbstractContentWriter
*/ */
public FileContentWriter(File file) public FileContentWriter(File file)
{ {
this( this(file, null);
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
null);
} }
/** /**
@@ -77,7 +75,7 @@ public class FileContentWriter extends AbstractContentWriter
{ {
this( this(
file, file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(), FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + file.getAbsolutePath(),
existingContentReader); existingContentReader);
} }

View File

@@ -26,7 +26,7 @@ package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import org.alfresco.repo.content.AbstractContentReadWriteTest; import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.TempFileProvider; import org.alfresco.util.TempFileProvider;
@@ -35,9 +35,10 @@ import org.alfresco.util.TempFileProvider;
* *
* @see org.alfresco.repo.content.filestore.FileContentStore * @see org.alfresco.repo.content.filestore.FileContentStore
* *
* @since 2.1
* @author Derek Hulley * @author Derek Hulley
*/ */
public class NoRandomAccessFileContentStoreTest extends AbstractContentReadWriteTest public class NoRandomAccessFileContentStoreTest extends AbstractWritableContentStoreTest
{ {
private FileContentStore store; private FileContentStore store;

View File

@@ -0,0 +1,66 @@
/*
* 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.filestore;
import java.io.File;
import org.alfresco.repo.content.AbstractReadOnlyContentStoreTest;
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.TempFileProvider;
/**
* Tests the file-based store when in read-only mode.
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @since 2.1
* @author Derek Hulley
*/
public class ReadOnlyFileContentStoreTest extends AbstractReadOnlyContentStoreTest
{
private FileContentStore store;
@Override
public void setUp() throws Exception
{
super.setUp();
// create a store that uses a subdirectory of the temp directory
File tempDir = TempFileProvider.getTempDir();
store = new FileContentStore(
tempDir.getAbsolutePath() +
File.separatorChar +
getName());
// disallow random access
store.setReadOnly(true);
}
@Override
protected ContentStore getStore()
{
return store;
}
}

View File

@@ -30,6 +30,7 @@ import java.util.Set;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.alfresco.repo.content.AbstractContentStore; import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
@@ -76,7 +77,7 @@ public class ContentStoreReplicatorTest extends TestCase
*/ */
public void testSinglePassReplication() throws Exception public void testSinglePassReplication() throws Exception
{ {
ContentWriter writer = sourceStore.getWriter(null, null); ContentWriter writer = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("123"); writer.putContent("123");
// replicate // replicate
@@ -92,7 +93,7 @@ public class ContentStoreReplicatorTest extends TestCase
targetStore.exists(writer.getContentUrl())); targetStore.exists(writer.getContentUrl()));
// this was a single pass, so now more replication should be done // this was a single pass, so now more replication should be done
writer = sourceStore.getWriter(null, null); writer = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("456"); writer.putContent("456");
// wait a second // wait a second
@@ -119,21 +120,22 @@ public class ContentStoreReplicatorTest extends TestCase
{ {
replicator.start(); replicator.start();
String duplicateUrl = AbstractContentStore.createNewUrl(); String duplicateUrl = null;
// start the replicator - it won't wait between iterations // start the replicator - it won't wait between iterations
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
// put some content into both the target and source // put some content into both the target and source
duplicateUrl = AbstractContentStore.createNewUrl(); ContentWriter duplicateSourceWriter = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
ContentWriter duplicateTargetWriter = targetStore.getWriter(null, duplicateUrl); duplicateUrl = duplicateSourceWriter.getContentUrl();
ContentWriter duplicateSourceWriter = sourceStore.getWriter(null, duplicateUrl); ContentContext targetContentCtx = new ContentContext(null, duplicateUrl);
ContentWriter duplicateTargetWriter = targetStore.getWriter(targetContentCtx);
duplicateTargetWriter.putContent("Duplicate Target Content: " + i); duplicateTargetWriter.putContent("Duplicate Target Content: " + i);
duplicateSourceWriter.putContent(duplicateTargetWriter.getReader()); duplicateSourceWriter.putContent(duplicateTargetWriter.getReader());
for (int j = 0; j < 100; j++) for (int j = 0; j < 100; j++)
{ {
// write content // write content
ContentWriter writer = sourceStore.getWriter(null, null); ContentWriter writer = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("Repeated put: " + j); writer.putContent("Repeated put: " + j);
} }
} }

View File

@@ -202,6 +202,23 @@ public class ReplicatingContentStore extends AbstractContentStore
this.outboundThreadPoolExecutor = outboundThreadPoolExecutor; this.outboundThreadPoolExecutor = outboundThreadPoolExecutor;
} }
/**
* @return Returns <tt>true</tt> if the primary store supports writing
*/
public boolean isWriteSupported()
{
return primaryStore.isWriteSupported();
}
/**
* @return Returns <tt>true</tt> if the primary store supports the URL
*/
@Override
public boolean isContentUrlSupported(String contentUrl)
{
return primaryStore.isContentUrlSupported(contentUrl);
}
/** /**
* Forwards the call directly to the first store in the list of stores. * Forwards the call directly to the first store in the list of stores.
*/ */
@@ -444,6 +461,13 @@ public class ReplicatingContentStore extends AbstractContentStore
" to store: " + store); " to store: " + store);
} }
} }
catch (UnsupportedOperationException e)
{
throw new ContentIOException(
"Unable to replicate content. The target store doesn't support replication: \n" +
" Content: " + writer.getContentUrl() + "\n" +
" To Store: " + store);
}
catch (Throwable e) catch (Throwable e)
{ {
throw new ContentIOException("Content replication failed: \n" + throw new ContentIOException("Content replication failed: \n" +

View File

@@ -32,7 +32,7 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.alfresco.repo.content.AbstractContentReadWriteTest; import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.FileContentStore;
@@ -51,7 +51,7 @@ import org.alfresco.util.TempFileProvider;
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
public class ReplicatingContentStoreTest extends AbstractContentReadWriteTest public class ReplicatingContentStoreTest extends AbstractWritableContentStoreTest
{ {
private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency"; private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency";

View File

@@ -27,9 +27,9 @@ package org.alfresco.repo.node.index;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.node.db.NodeDaoService;
import org.alfresco.repo.search.Indexer; import org.alfresco.repo.search.Indexer;
import org.alfresco.repo.search.impl.lucene.AbstractLuceneIndexerImpl; import org.alfresco.repo.search.impl.lucene.AbstractLuceneIndexerImpl;
@@ -49,6 +49,7 @@ import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
/** /**
@@ -119,7 +120,8 @@ public class MissingContentReindexComponentTest extends TestCase
public synchronized void testReindex() throws Exception public synchronized void testReindex() throws Exception
{ {
// create a node with missing content // create a node with missing content
String contentUrl = AbstractContentStore.createNewUrl(); String contentUrl = FileContentStore.STORE_PROTOCOL + FileContentStore.PROTOCOL_DELIMITER +
"x/y/" + GUID.generate() + ".bin";
ContentData contentData = new ContentData(contentUrl, "text/plain", 0L, "UTF8"); ContentData contentData = new ContentData(contentUrl, "text/plain", 0L, "UTF8");
// create the file node // create the file node

View File

@@ -29,6 +29,7 @@ import java.util.Locale;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import org.alfresco.i18n.I18NUtil; import org.alfresco.i18n.I18NUtil;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.util.EqualsHelper; import org.alfresco.util.EqualsHelper;
@@ -239,6 +240,14 @@ public class ContentData implements Serializable
} }
} }
} }
// Check that the protocol separator is present
if (contentUrl != null && !AbstractContentStore.isValidContentUrl(contentUrl))
{
throw new IllegalArgumentException(
"The content URL must be of the form 'protocol://identifier': \n" +
" Content URL: " + contentUrl);
}
// check that mimetype is present if URL is present // check that mimetype is present if URL is present
if (mimetype == null) if (mimetype == null)
{ {