mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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");
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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" +
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
{
|
||||
|
Reference in New Issue
Block a user