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.repo.audit.AuditConfiguration;
import org.alfresco.repo.audit.AuditDAO;
import org.alfresco.repo.audit.AuditException;
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.MimetypeMap;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
@@ -383,12 +382,12 @@ public class HibernateAuditDAO extends HibernateDaoSupport implements AuditDAO,
{
AuditConfigImpl auditConfig = new AuditConfigImpl();
InputStream is = new BufferedInputStream(auditInfo.getAuditConfiguration().getInputStream());
String url = AbstractContentStore.createNewUrl();
ContentWriter writer = contentStore.getWriter(null, url);
ContentWriter writer = contentStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.setMimetype(MimetypeMap.MIMETYPE_XML);
writer.setEncoding("UTF-8");
writer.putContent(is);
auditConfig.setConfigURL(url);
String contentUrl = writer.getContentUrl();
auditConfig.setConfigURL(contentUrl);
getSession().save(auditConfig);
return auditConfig;
}

View File

@@ -81,7 +81,7 @@ public abstract class AbstractContentAccessor implements ContentAccessor
{
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;

View File

@@ -24,15 +24,15 @@
*/
package org.alfresco.repo.content;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Date;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
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.
@@ -41,94 +41,112 @@ import org.alfresco.util.GUID;
* reasons of replication and backup, the most important functionality
* provided is the generation of new content URLs and the checking of
* 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
*/
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
* stores that are compatible with Alfresco.
* Checks that the content conforms to the format <b>protocol://identifier</b>
* 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();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
// create the URL
StringBuilder sb = new StringBuilder(20);
sb.append(STORE_PROTOCOL)
.append(year).append('/')
.append(month).append('/')
.append(day).append('/')
.append(hour).append('/')
.append(minute).append('/')
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
// done
return newContentUrl;
if (contentUrl == null)
{
return false;
}
int index = contentUrl.indexOf(ContentStore.PROTOCOL_DELIMITER);
if (index <= 0)
{
return false;
}
if (contentUrl.length() <= (index + PROTOCOL_DELIMETER_LENGTH))
{
return false;
}
return true;
}
/**
* This method can be used to ensure that URLs conform to the
* required format. If subclasses have to parse the URL,
* then a call to this may not be required - provided that
* the format is checked.
* <p>
* The protocol part of the URL (including legacy protocols)
* is stripped out and just the relative path is returned.
* Splits the content URL into its component parts as separated by
* {@link ContentStore#PROTOCOL_DELIMITER protocol delimiter}.
*
* @param contentUrl a URL of the content to check
* @return Returns the relative part of the URL
* @throws RuntimeException if the URL is not correct
* @param contentUrl the content URL to split
* @return Returns the protocol and identifier portions of the content URL,
* 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
* really specific to the FileContentStore and has been moved into
* it.
* @since 2.1
*/
public static String getRelativePart(String contentUrl) throws RuntimeException
protected Pair<String, String> getContentUrlParts(String contentUrl)
{
int index = 0;
if (contentUrl.startsWith(STORE_PROTOCOL))
if (contentUrl == null)
{
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(
"All content URLs must start with " + STORE_PROTOCOL + ": \n" +
" the invalid url is: " + contentUrl);
throw new UnsupportedContentUrlException(this, contentUrl);
}
// extract the relative part of the URL
String path = contentUrl.substring(index);
// more extensive checks can be added in, but it seems overkill
if (path.length() < 8)
{
throw new AlfrescoRuntimeException(
"The content URL is invalid: \n" +
" content url: " + contentUrl);
}
return path;
return new Pair<String, String>(protocol, identifier);
}
/**
* Simple implementation that uses the
* {@link ContentReader#exists() reader's exists} method as its implementation.
* Override this method to supply a efficient and direct check of the URL supplied.
* 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);
return reader.exists();
try
{
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)
*/
public final Set<String> getUrls() throws ContentIOException
public final Set<String> getUrls()
{
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 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);
return getWriter(ctx);

View File

@@ -121,9 +121,10 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
*/
public final ContentReader getReader() throws ContentIOException
{
String contentUrl = getContentUrl();
if (!isClosed())
{
return null;
return new EmptyContentReader(contentUrl);
}
ContentReader reader = createReader();
if (reader == null)
@@ -131,7 +132,7 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" +
" 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" +
" writer: " + this + "\n" +
@@ -235,7 +236,7 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
// this is a use-once object
if (channel != null)
{
throw new RuntimeException("A channel has already been opened");
throw new ContentIOException("A channel has already been opened");
}
WritableByteChannel directChannel = getDirectWritableChannel();
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.WriteLock;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
@@ -70,7 +71,7 @@ public abstract class AbstractRoutingContentStore implements ContentStore
{
this.storesByContentUrl = storesCache;
}
/**
* @return Returns a list of all possible stores available for reading or writing
*/
@@ -101,10 +102,28 @@ public abstract class AbstractRoutingContentStore implements ContentStore
{
// Check if the store is in the cache
ContentStore store = storesByContentUrl.get(contentUrl);
if (store != null && store.exists(contentUrl))
if (store != null)
{
// We found a store and can use it
return store;
// We found a store that was previously used
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
@@ -129,23 +148,30 @@ public abstract class AbstractRoutingContentStore implements ContentStore
}
return store;
}
else
{
store = null;
}
// It isn't, so search all the stores
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)
{
boolean exists = false;
try
{
exists = storeInList.exists(contentUrl);
if (!exists)
{
// It is not in the store
continue;
}
// At least the content URL was supported
contentUrlSupported = true;
}
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;
}
// We found one
@@ -154,6 +180,11 @@ public abstract class AbstractRoutingContentStore implements ContentStore
storesByContentUrl.put(contentUrl, store);
break;
}
// Check if the content URL was supported
if (!contentUrlSupported)
{
throw new UnsupportedContentUrlException(this, contentUrl);
}
// Done
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
* {@link ContentStore#exists(String)} contract.
* @return Returns <tt>true</tt> if the URL is supported by any of the stores.
*/
public boolean delete(String contentUrl) throws ContentIOException
public boolean isContentUrlSupported(String contentUrl)
{
boolean deleted = true;
List<ContentStore> stores = getAllStores();
boolean supported = false;
for (ContentStore store : stores)
{
deleted &= store.delete(contentUrl);
if (store.isContentUrlSupported(contentUrl))
{
supported = true;
break;
}
}
// Done
if (logger.isDebugEnabled())
{
logger.debug("Deleted content URL from stores: \n" +
" Stores: " + stores.size() + "\n" +
" Deleted: " + deleted);
logger.debug("The url " + (supported ? "is" : "is not") + " supported by at least one store.");
}
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.
*/
@@ -267,47 +403,27 @@ public abstract class AbstractRoutingContentStore implements ContentStore
}
/**
* Selects a store for the given context and caches store that was used.
*
* @see #selectWriteStore(ContentContext)
* This operation has to be performed on all the stores in order to maintain the
* {@link ContentStore#exists(String)} contract.
*/
public ContentWriter getWriter(ContentContext context) throws ContentIOException
public boolean delete(String contentUrl) throws ContentIOException
{
// Select the store for writing
ContentStore store = selectWriteStore(context);
if (store == null)
boolean deleted = true;
List<ContentStore> stores = getAllStores();
for (ContentStore store : stores)
{
throw new NullPointerException("Unable to find a writer. 'selectWriteStore' may not return null.");
}
ContentWriter writer = store.getWriter(context);
// Cache the store against the URL
storesCacheWriteLock.lock();
try
{
String contentUrl = writer.getContentUrl();
storesByContentUrl.put(contentUrl, store);
}
finally
{
storesCacheWriteLock.unlock();
if (store.isWriteSupported())
{
deleted &= store.delete(contentUrl);
}
}
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"Got writer and cache URL from store: \n" +
" Context: " + context + "\n" +
" Writer: " + writer + "\n" +
" Store: " + store);
logger.debug("Deleted content URL from stores: \n" +
" Stores: " + stores.size() + "\n" +
" Deleted: " + deleted);
}
return writer;
}
/**
* @see
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException
{
return getWriter(new ContentContext(existingContentReader, newContentUrl));
return deleted;
}
}

View File

@@ -38,113 +38,224 @@ import java.util.Date;
import java.util.Locale;
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.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.springframework.context.ApplicationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 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.ContentWriter
*
* @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;
private String contentUrl;
private UserTransaction txn;
public AbstractContentReadWriteTest()
public AbstractWritableContentStoreTest()
{
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
* therefore safe to use <code>setUp</code> to initialise resources.
* @inheritDoc
* <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.
* This implementation creates some content in the store and returns the new content URL.
*/
protected abstract ContentStore getStore();
/**
* @see #getStore()
*/
protected final ContentWriter getWriter()
protected String getExistingContentUrl()
{
ContentContext contentCtx = new ContentContext(null, contentUrl);
return getStore().getWriter(contentCtx);
ContentWriter writer = getWriter();
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
{
assertNotNull("setUp() not executed: no content URL present");
// 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());
}
public void testContentUrl() throws Exception
public void testWritable() throws Exception
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
// 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));
ContentStore store = getStore();
assertTrue("The store cannot be read-only", store.isWriteSupported());
}
/**
* 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
}
}
public void testMimetypAbdEncodingAndLocale() throws Exception
/**
* 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();
// set mimetype and encoding
@@ -172,104 +283,25 @@ public abstract class AbstractContentReadWriteTest extends TestCase
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
{
ContentReader reader = getReader();
ContentWriter writer = getWriter();
ContentReader readerBeforeWrite = writer.getReader();
// 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());
// 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
writer.putContent("ABC");
// check that the write has been closed
assertTrue("Writer stream should be closed", writer.isClosed());
// check that we can get a reader from the writer
writerGivenReader = writer.getReader();
assertNotNull("No reader given by closed writer", writerGivenReader);
assertFalse("Readers should still be closed", reader.isClosed());
assertFalse("Readers should still be closed", writerGivenReader.isClosed());
ContentReader readerAfterWrite = writer.getReader();
assertNotNull("No reader given by closed writer", readerAfterWrite);
assertFalse("Before-content reader should not be affected by content updates", readerBeforeWrite.isClosed());
assertFalse("After content reader should not be closed", readerAfterWrite.isClosed());
// check that the instance is new each time
ContentReader newReaderA = writer.getReader();
@@ -278,180 +310,61 @@ public abstract class AbstractContentReadWriteTest extends TestCase
// check that the readers refer to the same URL
assertEquals("Readers should refer to same URL",
reader.getContentUrl(), writerGivenReader.getContentUrl());
readerBeforeWrite.getContentUrl(), readerAfterWrite.getContentUrl());
// read their content
String contentCheck = reader.getContentString();
assertEquals("Incorrect content", "ABC", contentCheck);
contentCheck = writerGivenReader.getContentString();
try
{
readerBeforeWrite.getContentString();
}
catch (Throwable e)
{
// The content doesn't exist for this reader
}
String contentCheck = readerAfterWrite.getContentString();
assertEquals("Incorrect content", "ABC", contentCheck);
// check closed state of readers
assertTrue("Reader should be closed", reader.isClosed());
assertTrue("Reader should be closed", writerGivenReader.isClosed());
assertFalse("Before-content reader stream should not be closed", readerBeforeWrite.isClosed());
assertTrue("After-content reader should be closed after reading", readerAfterWrite.isClosed());
}
/**
* Checks that the store disallows concurrent writers to be issued to the same URL.
*/
@SuppressWarnings("unused")
public void testConcurrentWriteDetection() throws Exception
public void testGetUrls()
{
String contentUrl = AbstractContentStore.createNewUrl();
ContentStore store = getStore();
ContentContext contentCtx = new ContentContext(null, contentUrl);
ContentWriter firstWriter = store.getWriter(contentCtx);
try
{
ContentWriter secondWriter = store.getWriter(contentCtx);
fail("Store issued two writers for the same URL: " + store);
store.getUrls();
}
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));
}
/**
* Checks that the writer can have a listener attached
*/
public void testWriteStreamListener() throws Exception
public void testDeleteSimple() throws Exception
{
ContentStore store = getStore();
ContentWriter writer = getWriter();
final boolean[] streamClosed = new boolean[] {false}; // has to be final
ContentStreamListener listener = new ContentStreamListener()
writer.putContent("Content for " + getName());
String contentUrl = writer.getContentUrl();
assertTrue("Content must now exist", store.exists(contentUrl));
try
{
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
{
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);
store.delete(contentUrl);
}
catch (UnsupportedOperationException e)
{
logger.warn("Store test " + getName() + " not possible on " + store.getClass().getName());
return;
}
assertFalse("Content must now be removed", store.exists(contentUrl));
}
/**
@@ -459,19 +372,18 @@ public abstract class AbstractContentReadWriteTest extends TestCase
* <p>
* Only applies when {@link #getStore()} returns a value.
*/
public void testDelete() throws Exception
public void testDeleteReaderStates() throws Exception
{
ContentStore store = getStore();
ContentWriter writer = getWriter();
String content = "ABC";
String content = "Content for " + getName();
String contentUrl = writer.getContentUrl();
// write some bytes, but don't close the stream
OutputStream os = writer.getContentOutputStream();
os.write(content.getBytes());
os.flush(); // make sure that the bytes get persisted
// close the stream
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
* <p>
@@ -547,7 +600,17 @@ public abstract class AbstractContentReadWriteTest extends TestCase
public void testListUrls() throws Exception
{
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();
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
contentUrls = store.getUrls(null, yesterday);
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
ContentContext writerTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl());
ContentContext writerTruncateCtx = new ContentContext(writer.getReader(), null);
ContentWriter writerTruncate = getStore().getWriter(writerTruncateCtx);
assertEquals("Content size incorrect", 0, writerTruncate.getSize());
// get the channel with truncation
@@ -632,7 +687,7 @@ public abstract class AbstractContentReadWriteTest extends TestCase
assertEquals("Content not truncated", 0, writerTruncate.getSize());
// get a new writer from the store, using the existing content and perform a non-truncation check
ContentContext writerNoTruncateCtx = new ContentContext(writer.getReader(), AbstractContentStore.createNewUrl());
ContentContext writerNoTruncateCtx = new ContentContext(writer.getReader(), null);
ContentWriter writerNoTruncate = getStore().getWriter(writerNoTruncateCtx);
assertEquals("Content size incorrect", 0, writerNoTruncate.getSize());
// get the channel without truncation

View File

@@ -61,15 +61,32 @@ public class ContentDataTest extends TestCase
ContentData checkProperty = ContentData.createContentProperty(propertyStr);
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
propertyStr = property.toString();
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
checkProperty = ContentData.createContentProperty(propertyStr);
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
* <code>content URL</code>.
* <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>
* <li> <b>store://</b>: prefix identifying an Alfresco content stores
* 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>GUID</b>: A unique identifier </li>
* </ul>
* The old <b>file://</b> prefix must still be supported - and functionality
* around this can be found in the {@link org.alfresco.repo.content.AbstractContentStore}
* implementation.
* <p>
* Where the store cannot handle a particular content URL request, the
* {@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
*/
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.
@@ -71,30 +110,37 @@ public interface ContentStore
* reader to {@link ContentReader#exists() check for existence}, although
* that check should also be performed.
*
* @param contentUrl the path to the content
* @return Returns true if the content exists, otherwise
* false if the content doesn't exist or if the URL
* is not applicable to this store.
* @param contentUrl
* the path to the content
* @return
* 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
* if an IO error occurs
*
* @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.
* The reader is <b>stateful</b> and can <b>only be used once</b>.
*
* @param contentUrl the path to where the content is located
* @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.
* @param contentUrl the path to where the content is located
* @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.
* @throws UnsupportedContentUrlException
* if the content URL supplied is not supported by the store
* @throws ContentIOException
* if an IO error occurs
*
* @see #exists(String)
* @see ContentReader#exists()
* @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
@@ -110,15 +156,24 @@ public interface ContentStore
* can enable this by copying the existing content into the new location
* before supplying a writer onto the new content.
*
* @param context the context of content.
* @return Returns a write-only content accessor
* @throws ContentIOException if completely new content storage could not be created
* @param context
* the context of content.
* @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#getContentUrl()
*/
public ContentWriter getWriter(ContentContext context) throws ContentIOException;
public ContentWriter getWriter(ContentContext context);
/**
* Shortcut method to {@link #getWriter(ContentContext)}.
@@ -127,26 +182,37 @@ public interface ContentStore
*
* @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.
* @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)
*/
public Set<String> getUrls() throws ContentIOException;
public Set<String> getUrls();
/**
* Get a set of all content URLs in the store. This indicates all content
* available for reads.
*
* @param createdAfter all URLs returned must have been created after this date. May be null.
* @param createdBefore 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
* @param createdAfter
* all URLs returned must have been created after this date. May be null.
* @param createdBefore
* 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
* 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.
@@ -154,11 +220,17 @@ public interface ContentStore
* A delete cannot be forced since it is much better to have the
* file remain longer than desired rather than deleted prematurely.
*
* @param contentUrl the URL of the content to delete
* @return Return true 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.
* @param contentUrl
* the URL of the content to delete
* @return
* 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
* 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.filestore.FileContentStoreTest;
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.MappingMetadataExtracterTest;
import org.alfresco.repo.content.metadata.OfficeMetadataExtracterTest;
@@ -63,6 +64,7 @@ public class ContentTestSuite extends TestSuite
suite.addTestSuite(ContentStoreCleanerTest.class);
suite.addTestSuite(FileContentStoreTest.class);
suite.addTestSuite(NoRandomAccessFileContentStoreTest.class);
suite.addTestSuite(ReadOnlyFileContentStoreTest.class);
suite.addTestSuite(MappingMetadataExtracterTest.class);
suite.addTestSuite(HtmlMetadataExtracterTest.class);
suite.addTestSuite(OfficeMetadataExtracterTest.class);

View File

@@ -272,7 +272,16 @@ public class RoutingContentService implements ContentService
/** {@inheritDoc} */
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)
{
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
{
ContentReader reader = contentService.getRawReader("blah");
ContentReader reader = contentService.getRawReader("test://non-existence");
assertNotNull("A reader is expected with content URL referencing no content", reader);
assertFalse("Reader should not have any content", reader.exists());
// Now write something

View File

@@ -26,10 +26,9 @@ package org.alfresco.repo.content;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import junit.framework.TestCase;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
@@ -40,22 +39,27 @@ import org.alfresco.service.cmr.repository.ContentWriter;
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
* @since 2.1
*
* @author Derek Hulley
*/
public class RoutingContentStoreTest extends TestCase
public class RoutingContentStoreTest extends AbstractWritableContentStoreTest
{
private ContentStore storeA;
private ContentStore storeB;
private ContentStore storeC;
private ContentStore storeD;
private ContentStore routingStore;
@Override
protected void setUp() throws Exception
public void setUp() throws Exception
{
super.setUp();
File tempDir = TempFileProvider.getTempDir();
// Create a subdirectory for A
File storeADir = new File(tempDir, "A");
@@ -63,14 +67,23 @@ public class RoutingContentStoreTest extends TestCase
// Create a subdirectory for B
File storeBDir = new File(tempDir, "B");
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
routingStore = new RandomRoutingContentStore(storeA, storeB);
routingStore = new RandomRoutingContentStore(storeA, storeB, storeC, storeD);
}
@Override
protected ContentStore getStore()
{
return routingStore;
}
public void testSetUp() throws Exception
{
assertNotNull(storeA);
assertNotNull(storeB);
assertNotNull(routingStore);
}
@@ -96,7 +109,9 @@ public class RoutingContentStoreTest extends TestCase
*/
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);
assertFalse("Empty reader should say content doesn't exist.", reader.exists());
try
@@ -110,7 +125,7 @@ public class RoutingContentStoreTest extends TestCase
}
}
public void testHandlingInCache()
public void testGeneralUse()
{
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.
* 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 List<ContentStore> stores;
private Random random;
public RandomRoutingContentStore(ContentStore ... stores)
{
this.random = new Random();
this.stores = new ArrayList<ContentStore>(5);
for (ContentStore store : stores)
{
@@ -173,9 +178,66 @@ public class RoutingContentStoreTest extends TestCase
@Override
protected ContentStore selectWriteStore(ContentContext ctx)
{
int size = stores.size();
int index = (int) Math.floor(random.nextDouble() * (double) size);
return stores.get(index);
// Shuffle the list of writable stores
List<ContentStore> shuffled = new ArrayList<ContentStore>(stores);
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.DictionaryService;
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.transaction.TransactionService;
import org.alfresco.util.PropertyCheck;
@@ -220,7 +221,16 @@ public class ContentStoreCleaner
// now clean each store in turn
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);
// add some content to the store
ContentWriter writer = store.getWriter(null, null);
ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("ABC");
String contentUrl = writer.getContentUrl();
@@ -104,7 +104,7 @@ public class ContentStoreCleanerTest extends TestCase
{
cleaner.setProtectDays(1);
// add some content to the store
ContentWriter writer = store.getWriter(null, null);
ContentWriter writer = store.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("ABC");
String contentUrl = writer.getContentUrl();

View File

@@ -34,6 +34,7 @@ import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat;
import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
@@ -119,7 +120,9 @@ public class FileContentReader extends AbstractContentReader
*/
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.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentStore;
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.ContentReader;
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;
@@ -50,11 +57,18 @@ import org.apache.commons.logging.LogFactory;
*/
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 File rootDirectory;
private String rootAbsolutePath;
private boolean allowRandomAccess;
private boolean readOnly;
/**
* @param rootDirectoryStr the root under which files will be stored.
@@ -83,6 +97,7 @@ public class FileContentStore extends AbstractContentStore
this.rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true;
readOnly = false;
}
public String toString()
@@ -90,6 +105,8 @@ public class FileContentStore extends AbstractContentStore
StringBuilder sb = new StringBuilder(36);
sb.append("FileContentStore")
.append("[ root=").append(rootDirectory)
.append(", allowRandomAccess=").append(allowRandomAccess)
.append(", readOnly=").append(readOnly)
.append("]");
return sb.toString();
}
@@ -110,6 +127,18 @@ public class FileContentStore extends AbstractContentStore
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.
*
@@ -118,7 +147,7 @@ public class FileContentStore extends AbstractContentStore
*/
private File createNewFile() throws IOException
{
String contentUrl = createNewUrl();
String contentUrl = FileContentStore.createNewFileStoreUrl();
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
* @return Returns a new and unique file
* @throws IOException if the file or parent directories couldn't be created or
* if the URL is already in use.
* @throws IOException
* 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
{
if (readOnly)
{
throw new UnsupportedOperationException("This store is currently read-only: " + this);
}
File file = makeFile(newContentUrl);
// create the directory, if it doesn't exist
@@ -185,7 +223,7 @@ public class FileContentStore extends AbstractContentStore
index++;
}
// 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
url = url.replace('\\', '/');
// done
@@ -195,34 +233,45 @@ public class FileContentStore extends AbstractContentStore
/**
* Creates a file from the given relative URL.
*
* @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not
* exist
* @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not
* exist
* @throws UnsupportedContentUrlException
* if the URL is invalid and doesn't support the
* {@link FileContentStore#STORE_PROTOCOL correct protocol}
*
* @see #checkUrl(String)
*/
private File makeFile(String contentUrl)
{
// take just the part after the protocol
String relativeUrl = FileContentStore.getRelativePart(contentUrl);
if (relativeUrl == null)
Pair<String, String> urlParts = super.getContentUrlParts(contentUrl);
String protocol = urlParts.getFirst();
String relativePath = urlParts.getSecond();
// Check the protocol
if (!protocol.equals(FileContentStore.STORE_PROTOCOL))
{
throw new ContentIOException(
"The content URL is not valid for this store: \n" +
" Store: " + this + "\n" +
" Content URL: " + contentUrl);
throw new UnsupportedContentUrlException(this, contentUrl);
}
// get the file
File file = new File(rootDirectory, relativeUrl);
File file = new File(rootDirectory, relativePath);
// done
return file;
}
/**
* @return Returns <tt>true</tt> always
*/
public boolean isWriteSupported()
{
return true;
}
/**
* Performs a direct check against the file for its existence.
*/
@Override
public boolean exists(String contentUrl) throws ContentIOException
public boolean exists(String contentUrl)
{
File file = makeFile(contentUrl);
return file.exists();
@@ -237,8 +286,17 @@ public class FileContentStore extends AbstractContentStore
try
{
File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl);
reader.setAllowRandomAccess(allowRandomAccess);
ContentReader reader = null;
if (file.exists())
{
FileContentReader fileContentReader = new FileContentReader(file, contentUrl);
fileContentReader.setAllowRandomAccess(allowRandomAccess);
reader = fileContentReader;
}
else
{
reader = new EmptyContentReader(contentUrl);
}
// done
if (logger.isDebugEnabled())
@@ -250,6 +308,11 @@ public class FileContentStore extends AbstractContentStore
}
return reader;
}
catch (UnsupportedContentUrlException e)
{
// This can go out directly
throw e;
}
catch (Throwable 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
*/
public ContentWriter getWriter(ContentContext ctx)
public ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl)
{
ContentReader existingContentReader = ctx.getExistingContentReader();
String newContentUrl = ctx.getContentUrl();
try
{
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
* 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
File file = makeFile(contentUrl);
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.
* If subclasses have to parse the URL, then a call to this may not be required -
* provided that the format is checked.
* <p>
* The protocol part of the URL (including legacy protocols)
* is stripped out and just the relative path is returned. If no known prefix is
* found, or if the relative part is empty, then <tt>null</tt> is returned.
* Creates a new content URL. This must be supported by all
* stores that are compatible with Alfresco.
*
* @param contentUrl a URL of the content to check
* @return Returns the relative part of the URL. If there is no
* prefix, then the URL is assumed to be the relative part.
* @return Returns a new and unique content URL
*/
public static String getRelativePart(String contentUrl)
public static String createNewFileStoreUrl()
{
int index = 0;
if (contentUrl.startsWith(STORE_PROTOCOL))
{
index = 8;
}
else if (contentUrl.startsWith("file://"))
{
index = 7;
}
else
{
if (contentUrl.length() == 0)
{
throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl);
}
return contentUrl;
}
// extract the relative part of the URL
String path = contentUrl.substring(index);
// more extensive checks can be added in, but it seems overkill
if (path.length() == 0)
{
throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl);
}
return path;
Calendar calendar = new GregorianCalendar();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
// create the URL
StringBuilder sb = new StringBuilder(20);
sb.append(FileContentStore.STORE_PROTOCOL)
.append(ContentStore.PROTOCOL_DELIMITER)
.append(year).append('/')
.append(month).append('/')
.append(day).append('/')
.append(hour).append('/')
.append(minute).append('/')
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
// done
return newContentUrl;
}
}

View File

@@ -25,11 +25,13 @@
package org.alfresco.repo.content.filestore;
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.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
/**
@@ -39,7 +41,7 @@ import org.alfresco.util.TempFileProvider;
*
* @author Derek Hulley
*/
public class FileContentStoreTest extends AbstractContentReadWriteTest
public class FileContentStoreTest extends AbstractWritableContentStoreTest
{
private FileContentStore store;
@@ -55,47 +57,35 @@ public class FileContentStoreTest extends AbstractContentReadWriteTest
File.separatorChar +
getName());
}
@Override
protected ContentStore getStore()
{
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}";
String arg0 = "DEF";
String arg1 = "123";
String fakeContent = "ABC DEF123";
ByteBuffer buffer = ByteBuffer.wrap("Something".getBytes());
ContentStore store = getStore();
// get a good reader
ContentReader reader = getReader();
assertFalse("No content has been written to the URL yet", reader.exists());
// now create a file for it
File file = store.createNewFile(reader.getContentUrl());
assertTrue("File store did not connect new file", file.exists());
assertTrue("Reader did not detect creation of the underlying file", reader.exists());
// remove the underlying content
file.delete();
assertFalse("File not missing", file.exists());
assertFalse("Reader doesn't show missing content", reader.exists());
// 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());
ContentContext firstContentCtx = ContentStore.NEW_CONTENT_CONTEXT;
ContentWriter firstWriter = store.getWriter(firstContentCtx);
String contentUrl = firstWriter.getContentUrl();
ContentContext secondContentCtx = new ContentContext(null, contentUrl);
try
{
ContentWriter secondWriter = store.getWriter(secondContentCtx);
fail("Store must disallow more than one writer onto the same content URL: " + store);
}
catch (ContentExistsException e)
{
// expected
}
}
}

View File

@@ -33,6 +33,7 @@ import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
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.ContentReader;
import org.apache.commons.logging.Log;
@@ -60,10 +61,7 @@ public class FileContentWriter extends AbstractContentWriter
*/
public FileContentWriter(File file)
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
null);
this(file, null);
}
/**
@@ -77,7 +75,7 @@ public class FileContentWriter extends AbstractContentWriter
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + file.getAbsolutePath(),
existingContentReader);
}

View File

@@ -26,7 +26,7 @@ package org.alfresco.repo.content.filestore;
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.util.TempFileProvider;
@@ -35,9 +35,10 @@ import org.alfresco.util.TempFileProvider;
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @since 2.1
* @author Derek Hulley
*/
public class NoRandomAccessFileContentStoreTest extends AbstractContentReadWriteTest
public class NoRandomAccessFileContentStoreTest extends AbstractWritableContentStoreTest
{
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 org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.service.cmr.repository.ContentWriter;
@@ -76,7 +77,7 @@ public class ContentStoreReplicatorTest extends TestCase
*/
public void testSinglePassReplication() throws Exception
{
ContentWriter writer = sourceStore.getWriter(null, null);
ContentWriter writer = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("123");
// replicate
@@ -92,7 +93,7 @@ public class ContentStoreReplicatorTest extends TestCase
targetStore.exists(writer.getContentUrl()));
// 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");
// wait a second
@@ -119,21 +120,22 @@ public class ContentStoreReplicatorTest extends TestCase
{
replicator.start();
String duplicateUrl = AbstractContentStore.createNewUrl();
String duplicateUrl = null;
// start the replicator - it won't wait between iterations
for (int i = 0; i < 10; i++)
{
// put some content into both the target and source
duplicateUrl = AbstractContentStore.createNewUrl();
ContentWriter duplicateTargetWriter = targetStore.getWriter(null, duplicateUrl);
ContentWriter duplicateSourceWriter = sourceStore.getWriter(null, duplicateUrl);
ContentWriter duplicateSourceWriter = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
duplicateUrl = duplicateSourceWriter.getContentUrl();
ContentContext targetContentCtx = new ContentContext(null, duplicateUrl);
ContentWriter duplicateTargetWriter = targetStore.getWriter(targetContentCtx);
duplicateTargetWriter.putContent("Duplicate Target Content: " + i);
duplicateSourceWriter.putContent(duplicateTargetWriter.getReader());
for (int j = 0; j < 100; j++)
{
// write content
ContentWriter writer = sourceStore.getWriter(null, null);
ContentWriter writer = sourceStore.getWriter(ContentStore.NEW_CONTENT_CONTEXT);
writer.putContent("Repeated put: " + j);
}
}

View File

@@ -202,6 +202,23 @@ public class ReplicatingContentStore extends AbstractContentStore
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.
*/
@@ -444,6 +461,13 @@ public class ReplicatingContentStore extends AbstractContentStore
" 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)
{
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.TimeUnit;
import org.alfresco.repo.content.AbstractContentReadWriteTest;
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore;
@@ -51,7 +51,7 @@ import org.alfresco.util.TempFileProvider;
*
* @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";

View File

@@ -27,9 +27,9 @@ package org.alfresco.repo.node.index;
import junit.framework.TestCase;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.repo.node.db.NodeDaoService;
import org.alfresco.repo.search.Indexer;
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.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.springframework.context.ApplicationContext;
/**
@@ -119,7 +120,8 @@ public class MissingContentReindexComponentTest extends TestCase
public synchronized void testReindex() throws Exception
{
// 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");
// create the file node

View File

@@ -29,6 +29,7 @@ import java.util.Locale;
import java.util.StringTokenizer;
import org.alfresco.i18n.I18NUtil;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
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
if (mimetype == null)
{