From f30ccf8d6cac2838677186482bb1f99dcd49a54b Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Sat, 9 Jun 2007 00:43:02 +0000 Subject: [PATCH] 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 --- .../audit/hibernate/HibernateAuditDAO.java | 9 +- .../repo/content/AbstractContentAccessor.java | 2 +- .../repo/content/AbstractContentStore.java | 260 +++++-- .../repo/content/AbstractContentWriter.java | 7 +- .../AbstractReadOnlyContentStoreTest.java | 286 +++++++ .../content/AbstractRoutingContentStore.java | 224 ++++-- ... => AbstractWritableContentStoreTest.java} | 695 ++++++++++-------- .../repo/content/ContentDataTest.java | 21 +- .../repo/content/ContentExistsException.java | 76 ++ .../alfresco/repo/content/ContentStore.java | 136 +++- .../repo/content/ContentTestSuite.java | 2 + .../repo/content/RoutingContentService.java | 11 +- .../content/RoutingContentServiceTest.java | 2 +- .../repo/content/RoutingContentStoreTest.java | 108 ++- .../UnsupportedContentUrlException.java | 76 ++ .../content/cleanup/ContentStoreCleaner.java | 12 +- .../cleanup/ContentStoreCleanerTest.java | 4 +- .../content/filestore/FileContentReader.java | 5 +- .../content/filestore/FileContentStore.java | 173 +++-- .../filestore/FileContentStoreTest.java | 66 +- .../content/filestore/FileContentWriter.java | 8 +- .../NoRandomAccessFileContentStoreTest.java | 5 +- .../ReadOnlyFileContentStoreTest.java | 66 ++ .../ContentStoreReplicatorTest.java | 16 +- .../replication/ReplicatingContentStore.java | 24 + .../ReplicatingContentStoreTest.java | 4 +- .../MissingContentReindexComponentTest.java | 6 +- .../service/cmr/repository/ContentData.java | 9 + 28 files changed, 1685 insertions(+), 628 deletions(-) create mode 100644 source/java/org/alfresco/repo/content/AbstractReadOnlyContentStoreTest.java rename source/java/org/alfresco/repo/content/{AbstractContentReadWriteTest.java => AbstractWritableContentStoreTest.java} (66%) create mode 100644 source/java/org/alfresco/repo/content/ContentExistsException.java create mode 100644 source/java/org/alfresco/repo/content/UnsupportedContentUrlException.java create mode 100644 source/java/org/alfresco/repo/content/filestore/ReadOnlyFileContentStoreTest.java diff --git a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java index 8a58523947..f4fc64b7eb 100644 --- a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java +++ b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java @@ -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; } diff --git a/source/java/org/alfresco/repo/content/AbstractContentAccessor.java b/source/java/org/alfresco/repo/content/AbstractContentAccessor.java index 94597ef6f6..0e049a4ae1 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentAccessor.java +++ b/source/java/org/alfresco/repo/content/AbstractContentAccessor.java @@ -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; diff --git a/source/java/org/alfresco/repo/content/AbstractContentStore.java b/source/java/org/alfresco/repo/content/AbstractContentStore.java index e1d2d4ec18..dedc7539d9 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentStore.java +++ b/source/java/org/alfresco/repo/content/AbstractContentStore.java @@ -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. + *

+ * Implementations must override either of the getWriter 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 protocol://identifier + * 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 true 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. - *

- * 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 null + * @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 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(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 getUrls() throws ContentIOException + public final Set getUrls() { return getUrls(null, null); } + /** + * Override if the derived class supports the operation. + * + * @throws UnsupportedOperationException always + * + * @since 2.1 + */ + public Set 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); diff --git a/source/java/org/alfresco/repo/content/AbstractContentWriter.java b/source/java/org/alfresco/repo/content/AbstractContentWriter.java index 22e2e68749..18e9222695 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentWriter.java +++ b/source/java/org/alfresco/repo/content/AbstractContentWriter.java @@ -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); diff --git a/source/java/org/alfresco/repo/content/AbstractReadOnlyContentStoreTest.java b/source/java/org/alfresco/repo/content/AbstractReadOnlyContentStoreTest.java new file mode 100644 index 0000000000..edcb605f38 --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractReadOnlyContentStoreTest.java @@ -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 setUp to initialise resources. + *

+ * Usually tests will construct a static instance of the store to use throughout all the + * tests. + * + * @return Returns the same instance 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 null if the store is empty. + */ + protected String getExistingContentUrl() + { + ContentStore store = getStore(); + try + { + Set 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 + * getReader() and exists() 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 inappropriate 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 + *

+ * 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 + } + } +} diff --git a/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java b/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java index 6c4578c960..b919633f74 100644 --- a/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java +++ b/source/java/org/alfresco/repo/content/AbstractRoutingContentStore.java @@ -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 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 true 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 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 true if write is supported by any of the stores. + */ + public boolean isWriteSupported() + { + List 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 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; } } diff --git a/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java b/source/java/org/alfresco/repo/content/AbstractWritableContentStoreTest.java similarity index 66% rename from source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java rename to source/java/org/alfresco/repo/content/AbstractWritableContentStoreTest.java index 367902b56d..dc814c1137 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java +++ b/source/java/org/alfresco/repo/content/AbstractWritableContentStoreTest.java @@ -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 setUp to initialise resources. + * @inheritDoc *

- * Usually tests will construct a static instance of the store to use throughout all the - * tests. - * - * @return Returns the same instance 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 getWriter 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 inappropriate 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 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 *

* 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 *

@@ -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 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 diff --git a/source/java/org/alfresco/repo/content/ContentDataTest.java b/source/java/org/alfresco/repo/content/ContentDataTest.java index da430700e3..f473f9f1a5 100644 --- a/source/java/org/alfresco/repo/content/ContentDataTest.java +++ b/source/java/org/alfresco/repo/content/ContentDataTest.java @@ -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); + } } diff --git a/source/java/org/alfresco/repo/content/ContentExistsException.java b/source/java/org/alfresco/repo/content/ContentExistsException.java new file mode 100644 index 0000000000..6d3e7e2072 --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentExistsException.java @@ -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; + } +} diff --git a/source/java/org/alfresco/repo/content/ContentStore.java b/source/java/org/alfresco/repo/content/ContentStore.java index 45658c25d3..216bfe26cd 100644 --- a/source/java/org/alfresco/repo/content/ContentStore.java +++ b/source/java/org/alfresco/repo/content/ContentStore.java @@ -42,7 +42,9 @@ import org.alfresco.service.cmr.repository.ContentWriter; * providing persistence and retrieval of the content against a * content URL. *

- * The URL format is store://year/month/day/GUID.bin
+ * 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 store://year/month/day/GUID.bin
*

- * The old file:// prefix must still be supported - and functionality - * around this can be found in the {@link org.alfresco.repo.content.AbstractContentStore} - * implementation. + *

+ * 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. + *

+ * Where a store cannot serve a particular request because the functionality + * is just not available, the UnsupportedOperationException should + * be thrown. Once again, there may be fallback handling provided for these + * situations. * + * @since 1.0 * @author Derek Hulley */ public interface ContentStore { - /** store:// 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 :// + */ + 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 true 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 if the URL is not applicable to this store. + * @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 stateful and can only be used once. * - * @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 getUrls() throws ContentIOException; + public Set 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 getUrls(Date createdAfter, Date createdBefore) throws ContentIOException; + public Set 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 true is returned. + * @param contentUrl + * the URL of the content to delete + * @return + * Returns true if the content was deleted (either by this or another operation), + * otherwise false. If the content no longer exists, then true 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); } diff --git a/source/java/org/alfresco/repo/content/ContentTestSuite.java b/source/java/org/alfresco/repo/content/ContentTestSuite.java index 80ffb0df94..f51c4710cf 100644 --- a/source/java/org/alfresco/repo/content/ContentTestSuite.java +++ b/source/java/org/alfresco/repo/content/ContentTestSuite.java @@ -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); diff --git a/source/java/org/alfresco/repo/content/RoutingContentService.java b/source/java/org/alfresco/repo/content/RoutingContentService.java index 30cc9c0c37..74b890d8af 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentService.java +++ b/source/java/org/alfresco/repo/content/RoutingContentService.java @@ -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"); diff --git a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java index 85c662a63a..82e4b19571 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java +++ b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java @@ -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 diff --git a/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java b/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java index e9d4466177..3fe59f5d39 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/RoutingContentStoreTest.java @@ -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 stores; - private Random random; public RandomRoutingContentStore(ContentStore ... stores) { - this.random = new Random(); this.stores = new ArrayList(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 shuffled = new ArrayList(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); } } } diff --git a/source/java/org/alfresco/repo/content/UnsupportedContentUrlException.java b/source/java/org/alfresco/repo/content/UnsupportedContentUrlException.java new file mode 100644 index 0000000000..eb9deb8e15 --- /dev/null +++ b/source/java/org/alfresco/repo/content/UnsupportedContentUrlException.java @@ -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; + } +} diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java index 59c430a5f8..ff63839f5b 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java @@ -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); + } } } diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java index 8ff49a4d1d..940dd305ed 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java @@ -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(); diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentReader.java b/source/java/org/alfresco/repo/content/filestore/FileContentReader.java index 28691b4d28..5b2e742b2a 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentReader.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentReader.java @@ -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()); } /** diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java index e2c3321445..32472a2476 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java @@ -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 { + /** + * store 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 true 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 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 true 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. - *

- * The protocol part of the URL (including legacy protocols) - * is stripped out and just the relative path is returned. If no known prefix is - * found, or if the relative part is empty, then null is returned. + * 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; } } diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java index bfeb9fb85b..8a9cbdfc64 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java @@ -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 + } } } diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java b/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java index 1a50c2ab3f..165dbe7bd9 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java @@ -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); } diff --git a/source/java/org/alfresco/repo/content/filestore/NoRandomAccessFileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/NoRandomAccessFileContentStoreTest.java index ee4c90a267..4a6a5a3c3a 100644 --- a/source/java/org/alfresco/repo/content/filestore/NoRandomAccessFileContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/filestore/NoRandomAccessFileContentStoreTest.java @@ -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; diff --git a/source/java/org/alfresco/repo/content/filestore/ReadOnlyFileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/ReadOnlyFileContentStoreTest.java new file mode 100644 index 0000000000..af0bda971e --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/ReadOnlyFileContentStoreTest.java @@ -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; + } +} diff --git a/source/java/org/alfresco/repo/content/replication/ContentStoreReplicatorTest.java b/source/java/org/alfresco/repo/content/replication/ContentStoreReplicatorTest.java index d1beb495dd..6537f47cc5 100644 --- a/source/java/org/alfresco/repo/content/replication/ContentStoreReplicatorTest.java +++ b/source/java/org/alfresco/repo/content/replication/ContentStoreReplicatorTest.java @@ -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); } } diff --git a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java index 6d1f7e0156..849da16858 100644 --- a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java +++ b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java @@ -202,6 +202,23 @@ public class ReplicatingContentStore extends AbstractContentStore this.outboundThreadPoolExecutor = outboundThreadPoolExecutor; } + /** + * @return Returns true if the primary store supports writing + */ + public boolean isWriteSupported() + { + return primaryStore.isWriteSupported(); + } + + /** + * @return Returns true 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" + diff --git a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStoreTest.java b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStoreTest.java index db9bc1ba49..5e4128a9bf 100644 --- a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStoreTest.java @@ -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"; diff --git a/source/java/org/alfresco/repo/node/index/MissingContentReindexComponentTest.java b/source/java/org/alfresco/repo/node/index/MissingContentReindexComponentTest.java index 0f31872497..f09bb697d4 100644 --- a/source/java/org/alfresco/repo/node/index/MissingContentReindexComponentTest.java +++ b/source/java/org/alfresco/repo/node/index/MissingContentReindexComponentTest.java @@ -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 diff --git a/source/java/org/alfresco/service/cmr/repository/ContentData.java b/source/java/org/alfresco/service/cmr/repository/ContentData.java index 72b7fd0136..9fc70573d1 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentData.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentData.java @@ -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) {