AR-383: Optimized overwrite behaviour for ContentWriter. Content will only be duplicated for the writes if explicitly requesting a random-access file without the truncate option

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2254 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2006-01-31 05:40:51 +00:00
parent cdede309e5
commit 9bac8270f4
14 changed files with 571 additions and 242 deletions

View File

@@ -26,10 +26,10 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.transaction.TransactionUtil;
import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
@@ -139,17 +139,36 @@ public abstract class AbstractContentAccessor implements ContentAccessor
this.encoding = encoding;
}
/**
* Generate a callback instance of the {@link FileChannel FileChannel}.
*
* @param directChannel the delegate that to perform the actual operations
* @param listeners the listeners to call
* @return Returns a new channel that functions just like the original, except
* that it issues callbacks to the listeners
* @throws ContentIOException
*/
protected FileChannel getCallbackFileChannel(
FileChannel directChannel,
List<ContentStreamListener> listeners)
throws ContentIOException
{
FileChannel ret = new CallbackFileChannel(directChannel, listeners);
// done
return ret;
}
/**
* Advise that listens for the completion of specific methods on the
* {@link java.nio.channels.ByteChannel} interface.
*
* @author Derek Hulley
*/
protected class ByteChannelCallbackAdvise implements AfterReturningAdvice
protected class ChannelCloseCallbackAdvise implements AfterReturningAdvice
{
private List<ContentStreamListener> listeners;
public ByteChannelCallbackAdvise(List<ContentStreamListener> listeners)
public ChannelCloseCallbackAdvise(List<ContentStreamListener> listeners)
{
this.listeners = listeners;
}
@@ -173,11 +192,6 @@ public abstract class AbstractContentAccessor implements ContentAccessor
// nothing to do
return;
}
// ensure that we are in a transaction
if (transactionService == null)
{
throw new AlfrescoRuntimeException("A transaction service is required when there are listeners present");
}
TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{
public Object doWork()
@@ -190,11 +204,26 @@ public abstract class AbstractContentAccessor implements ContentAccessor
return null;
}
};
TransactionUtil.executeInUserTransaction(transactionService, work);
if (transactionService != null)
{
// just create a transaction
TransactionUtil.executeInUserTransaction(transactionService, work);
}
else
{
try
{
work.doWork();
}
catch (Exception e)
{
throw new ContentIOException("Failed to executed channel close callbacks", e);
}
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Content listeners called: close");
logger.debug("" + listeners.size() + " content listeners called: close");
}
}
}
@@ -202,6 +231,10 @@ public abstract class AbstractContentAccessor implements ContentAccessor
/**
* Wraps a <code>FileChannel</code> to provide callbacks to listeners when the
* channel is {@link java.nio.channels.Channel#close() closed}.
* <p>
* This class is unfortunately necessary as the {@link FileChannel} doesn't have
* an single interface defining its methods, making it difficult to put an
* advice around the methods that require overriding.
*
* @author Derek Hulley
*/
@@ -253,7 +286,6 @@ public abstract class AbstractContentAccessor implements ContentAccessor
// nothing to do
return;
}
// create the work to update the listeners
TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{
public Object doWork()
@@ -266,14 +298,29 @@ public abstract class AbstractContentAccessor implements ContentAccessor
return null;
}
};
TransactionUtil.executeInUserTransaction(transactionService, work);
if (transactionService != null)
{
// just create a transaction
TransactionUtil.executeInUserTransaction(transactionService, work);
}
else
{
try
{
work.doWork();
}
catch (Exception e)
{
throw new ContentIOException("Failed to executed channel close callbacks", e);
}
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Content listeners called: close");
logger.debug("" + listeners.size() + " content listeners called: close");
}
}
@Override
public void force(boolean metaData) throws IOException
{

View File

@@ -28,6 +28,8 @@ 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.repo.transaction.DummyTransactionService;
@@ -35,6 +37,9 @@ 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;
/**
* Abstract base class that provides a set of tests for implementations
@@ -47,7 +52,11 @@ import org.alfresco.service.cmr.repository.ContentWriter;
*/
public abstract class AbstractContentReadWriteTest extends TestCase
{
private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
protected TransactionService transactionService;
private String contentUrl;
private UserTransaction txn;
public AbstractContentReadWriteTest()
{
@@ -58,6 +67,14 @@ public abstract class AbstractContentReadWriteTest extends TestCase
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();
}
/**
@@ -539,16 +556,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase
public void testRandomAccessWrite() throws Exception
{
ContentWriter writer = getWriter();
if (!(writer instanceof RandomAccessContent))
{
// not much to do here
return;
}
RandomAccessContent randomWriter = (RandomAccessContent) writer;
// check that we are allowed to write
assertTrue("Expected random access writing", randomWriter.canWrite());
FileChannel fileChannel = randomWriter.getChannel();
FileChannel fileChannel = writer.getFileChannel(true);
assertNotNull("No channel given", fileChannel);
// check that no other content access is allowed
@@ -584,6 +593,22 @@ public abstract class AbstractContentReadWriteTest extends TestCase
{
assertEquals("Content doesn't match", content[i], buffer.get(i));
}
// get a new writer from the store, using the existing content and perform a truncation check
ContentWriter writerTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl());
assertEquals("Content size incorrect", 0, writerTruncate.getSize());
// get the channel with truncation
FileChannel fcTruncate = writerTruncate.getFileChannel(true);
fcTruncate.close();
assertEquals("Content not truncated", 0, writerTruncate.getSize());
// get a new writer from the store, using the existing content and perform a non-truncation check
ContentWriter writerNoTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl());
assertEquals("Content size incorrect", 0, writerNoTruncate.getSize());
// get the channel without truncation
FileChannel fcNoTruncate = writerNoTruncate.getFileChannel(false);
fcNoTruncate.close();
assertEquals("Content was truncated", writer.getSize(), writerNoTruncate.getSize());
}
/**
@@ -599,16 +624,8 @@ public abstract class AbstractContentReadWriteTest extends TestCase
byte[] bytes = content.getBytes();
writer.putContent(content);
ContentReader reader = writer.getReader();
if (!(reader instanceof RandomAccessContent))
{
// not much to do here
return;
}
RandomAccessContent randomReader = (RandomAccessContent) reader;
// check that we are NOT allowed to write
assertFalse("Expected read-only random access", randomReader.canWrite());
FileChannel fileChannel = randomReader.getChannel();
FileChannel fileChannel = reader.getFileChannel();
assertNotNull("No channel given", fileChannel);
// check that no other content access is allowed
@@ -631,5 +648,6 @@ public abstract class AbstractContentReadWriteTest extends TestCase
buffer.get(bytes);
String checkContent = new String(bytes);
assertEquals("Content read failure", content, checkContent);
fileChannel.close();
}
}

View File

@@ -26,15 +26,18 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
@@ -152,24 +155,38 @@ public abstract class AbstractContentReader extends AbstractContentAccessor impl
protected abstract ReadableByteChannel getDirectReadableChannel() throws ContentIOException;
/**
* Optionally override to supply an alternate callback channel.
* Create a channel that performs callbacks to the given listeners.
*
* @param directChannel the result of {@link #getDirectReadableChannel()}
* @param listeners the listeners to call
* @return Returns a channel
* @throws ContentIOException
*/
protected ReadableByteChannel getCallbackReadableChannel(
private ReadableByteChannel getCallbackReadableChannel(
ReadableByteChannel directChannel,
List<ContentStreamListener> listeners)
throws ContentIOException
{
// introduce an advistor to handle the callbacks to the listeners
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
ReadableByteChannel callbackChannel = (ReadableByteChannel) proxyFactory.getProxy();
ReadableByteChannel callbackChannel = null;
if (directChannel instanceof FileChannel)
{
callbackChannel = getCallbackFileChannel((FileChannel) directChannel, listeners);
}
else
{
// introduce an advistor to handle the callbacks to the listeners
ChannelCloseCallbackAdvise advise = new ChannelCloseCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
callbackChannel = (ReadableByteChannel) proxyFactory.getProxy();
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Created callback byte channel: \n" +
" original: " + directChannel + "\n" +
" new: " + callbackChannel);
}
return callbackChannel;
}
@@ -195,6 +212,90 @@ public abstract class AbstractContentReader extends AbstractContentAccessor impl
return channel;
}
/**
* @inheritDoc
*/
public FileChannel getFileChannel() throws ContentIOException
{
/*
* Where the underlying support is not present for this method, a temporary
* file will be used as a substitute. When the write is complete, the
* results are copied directly to the underlying channel.
*/
// get the underlying implementation's best readable channel
channel = getReadableChannel();
// now use this channel if it can provide the random access, otherwise spoof it
FileChannel clientFileChannel = null;
if (channel instanceof FileChannel)
{
// all the support is provided by the underlying implementation
clientFileChannel = (FileChannel) channel;
// debug
if (logger.isDebugEnabled())
{
logger.debug("Content reader provided direct support for FileChannel: \n" +
" reader: " + this);
}
}
else
{
// No random access support is provided by the implementation.
// Spoof it by providing a 2-stage read from a temp file
File tempFile = TempFileProvider.createTempFile("random_read_spoof_", ".bin");
FileContentWriter spoofWriter = new FileContentWriter(tempFile);
// pull the content in from the underlying channel
FileChannel spoofWriterChannel = spoofWriter.getFileChannel(false);
try
{
long spoofFileSize = this.getSize();
spoofWriterChannel.transferFrom(channel, 0, spoofFileSize);
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy from permanent channel to spoofed temporary channel: \n" +
" reader: " + this + "\n" +
" temp: " + spoofWriter,
e);
}
finally
{
try { spoofWriterChannel.close(); } catch (IOException e) {}
}
// get a reader onto the spoofed content
final ContentReader spoofReader = spoofWriter.getReader();
// Attach a listener
// - ensure that the close call gets propogated to the underlying channel
ContentStreamListener spoofListener = new ContentStreamListener()
{
public void contentStreamClosed() throws ContentIOException
{
try
{
channel.close();
}
catch (IOException e)
{
throw new ContentIOException("Failed to close underlying channel", e);
}
}
};
spoofReader.addListener(spoofListener);
// we now have the spoofed up channel that the client can work with
clientFileChannel = spoofReader.getFileChannel();
// debug
if (logger.isDebugEnabled())
{
logger.debug("Content writer provided indirect support for FileChannel: \n" +
" writer: " + this + "\n" +
" temp writer: " + spoofWriter);
}
}
// the file is now available for random access
return clientFileChannel;
}
/**
* @see Channels#newInputStream(java.nio.channels.ReadableByteChannel)
*/

View File

@@ -24,16 +24,20 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentAccessor;
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.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
@@ -151,7 +155,7 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
return false;
}
}
/**
* Provides low-level access to write content to the repository.
* <p>
@@ -165,24 +169,38 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
protected abstract WritableByteChannel getDirectWritableChannel() throws ContentIOException;
/**
* Optionally override to supply an alternate callback channel.
*
* Create a channel that performs callbacks to the given listeners.
*
* @param directChannel the result of {@link #getDirectWritableChannel()}
* @param listeners the listeners to call
* @return Returns a callback channel
* @return Returns a channel that executes callbacks
* @throws ContentIOException
*/
protected WritableByteChannel getCallbackWritableChannel(
private WritableByteChannel getCallbackWritableChannel(
WritableByteChannel directChannel,
List<ContentStreamListener> listeners)
throws ContentIOException
{
// proxy to add an advise
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
WritableByteChannel callbackChannel = (WritableByteChannel) proxyFactory.getProxy();
WritableByteChannel callbackChannel = null;
if (directChannel instanceof FileChannel)
{
callbackChannel = getCallbackFileChannel((FileChannel) directChannel, listeners);
}
else
{
// introduce an advistor to handle the callbacks to the listeners
ChannelCloseCallbackAdvise advise = new ChannelCloseCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
callbackChannel = (WritableByteChannel) proxyFactory.getProxy();
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Created callback byte channel: \n" +
" original: " + directChannel + "\n" +
" new: " + callbackChannel);
}
return callbackChannel;
}
@@ -210,6 +228,126 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
return channel;
}
/**
* @inheritDoc
*/
public FileChannel getFileChannel(boolean truncate) throws ContentIOException
{
/*
* By calling this method, clients indicate that they wish to make random
* changes to the file. It is possible that the client might only want
* to update a tiny proportion of the file (truncate == false) or
* start afresh (truncate == true).
*
* Where the underlying support is not present for this method, a temporary
* file will be used as a substitute. When the write is complete, the
* results are copied directly to the underlying channel.
*/
// get the underlying implementation's best writable channel
channel = getWritableChannel();
// now use this channel if it can provide the random access, otherwise spoof it
FileChannel clientFileChannel = null;
if (channel instanceof FileChannel)
{
// all the support is provided by the underlying implementation
clientFileChannel = (FileChannel) channel;
// copy over the existing content, if required
if (!truncate && existingContentReader != null)
{
ReadableByteChannel existingContentChannel = existingContentReader.getReadableChannel();
long existingContentLength = existingContentReader.getSize();
// copy the existing content
try
{
clientFileChannel.transferFrom(existingContentChannel, 0, existingContentLength);
// copy complete
if (logger.isDebugEnabled())
{
logger.debug("Copied content for random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader);
}
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy from existing content to enable random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader,
e);
}
finally
{
try { existingContentChannel.close(); } catch (IOException e) {}
}
}
// debug
if (logger.isDebugEnabled())
{
logger.debug("Content writer provided direct support for FileChannel: \n" +
" writer: " + this);
}
}
else
{
// No random access support is provided by the implementation.
// Spoof it by providing a 2-stage write via a temp file
File tempFile = TempFileProvider.createTempFile("random_write_spoof_", ".bin");
final FileContentWriter spoofWriter = new FileContentWriter(
tempFile, // the file to write to
getExistingContentReader()); // this ensures that the existing content is pulled in
// Attach a listener
// - to ensure that the content gets loaded from the temp file once writing has finished
// - to ensure that the close call gets passed on to the underlying channel
ContentStreamListener spoofListener = new ContentStreamListener()
{
public void contentStreamClosed() throws ContentIOException
{
// the spoofed temp channel has been closed, so get a new reader for it
ContentReader spoofReader = spoofWriter.getReader();
FileChannel spoofChannel = spoofReader.getFileChannel();
// upload all the temp content to the real underlying channel
try
{
long spoofFileSize = spoofChannel.size();
spoofChannel.transferTo(0, spoofFileSize, channel);
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy from spoofed temporary channel to permanent channel: \n" +
" writer: " + this + "\n" +
" temp: " + spoofReader,
e);
}
finally
{
try { spoofChannel.close(); } catch (Throwable e) {}
try
{
channel.close();
}
catch (IOException e)
{
throw new ContentIOException("Failed to close underlying channel", e);
}
}
}
};
spoofWriter.addListener(spoofListener);
// we now have the spoofed up channel that the client can work with
clientFileChannel = spoofWriter.getFileChannel(truncate);
// debug
if (logger.isDebugEnabled())
{
logger.debug("Content writer provided indirect support for FileChannel: \n" +
" writer: " + this + "\n" +
" temp writer: " + spoofWriter);
}
}
// the file is now available for random access
return clientFileChannel;
}
/**
* @see Channels#newOutputStream(java.nio.channels.WritableByteChannel)
*/

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2005 Alfresco, Inc.
*
* Licensed under the Mozilla Public License version 1.1
* with a permitted attribution clause. You may obtain a
* copy of the License at
*
* http://www.alfresco.org/legal/license.txt
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the
* License.
*/
package org.alfresco.repo.content;
import java.nio.channels.FileChannel;
import org.alfresco.service.cmr.repository.ContentIOException;
/**
* Supplementary interface for content readers and writers that allow random-access to
* the underlying content.
* <p>
* The use of this interface by a client <b>may</b> preclude the use of any other
* access to the underlying content - this depends on the underlying implementation.
*
* @author Derek Hulley
*/
public interface RandomAccessContent
{
/**
* @return Returns true if the content can be written to
*/
public boolean canWrite();
/**
* Get a channel to access the content. The channel's behaviour is similar to that
* when a <tt>FileChannel</tt> is retrieved using {@link java.io.RandomAccessFile#getChannel()}.
*
* @return Returns a channel to access the content
* @throws ContentIOException
*
* @see #canWrite()
* @see java.io.RandomAccessFile#getChannel()
*/
public FileChannel getChannel() throws ContentIOException;
}

View File

@@ -17,20 +17,18 @@
package org.alfresco.repo.content.filestore;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.RandomAccessContent;
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.util.TempFileProvider;
import org.apache.commons.logging.Log;
@@ -43,11 +41,12 @@ import org.apache.commons.logging.LogFactory;
*
* @author Derek Hulley
*/
public class FileContentReader extends AbstractContentReader implements RandomAccessContent
public class FileContentReader extends AbstractContentReader
{
private static final Log logger = LogFactory.getLog(FileContentReader.class);
private File file;
private boolean allowRandomAccess;
/**
* Checks the existing reader provided and replaces it with a reader onto some
@@ -118,6 +117,12 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
super(url);
this.file = file;
allowRandomAccess = true;
}
/* package */ void setAllowRandomAccess(boolean allow)
{
this.allowRandomAccess = allow;
}
/**
@@ -170,7 +175,9 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
@Override
protected ContentReader createReader() throws ContentIOException
{
return new FileContentReader(this.file, getContentUrl());
FileContentReader reader = new FileContentReader(this.file, getContentUrl());
reader.setAllowRandomAccess(this.allowRandomAccess);
return reader;
}
@Override
@@ -184,12 +191,23 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
throw new IOException("File does not exist");
}
// create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it
FileChannel channel = randomAccessFile.getChannel();
ReadableByteChannel channel = null;
if (allowRandomAccess)
{
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it
channel = randomAccessFile.getChannel();
}
else
{
InputStream is = new FileInputStream(file);
channel = Channels.newChannel(is);
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel to file: " + file);
logger.debug("Opened write channel to file: \n" +
" file: " + file + "\n" +
" random-access: " + allowRandomAccess);
}
return channel;
}
@@ -199,25 +217,6 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
}
}
/**
* @param directChannel a file channel
*/
@Override
protected ReadableByteChannel getCallbackReadableChannel(
ReadableByteChannel directChannel,
List<ContentStreamListener> listeners) throws ContentIOException
{
if (!(directChannel instanceof FileChannel))
{
throw new AlfrescoRuntimeException("Expected read channel to be a file channel");
}
FileChannel fileChannel = (FileChannel) directChannel;
// wrap it
FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners);
// done
return callbackChannel;
}
/**
* @return Returns false as this is a reader
*/
@@ -225,11 +224,4 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
{
return false; // we only allow reading
}
public FileChannel getChannel() throws ContentIOException
{
// go through the super classes to ensure that all concurrency conditions
// and listeners are satisfied
return (FileChannel) super.getReadableChannel();
}
}

View File

@@ -43,6 +43,7 @@ public class FileContentStore extends AbstractContentStore
private File rootDirectory;
private String rootAbsolutePath;
private boolean allowRandomAccess;
/**
* @param rootDirectory the root under which files will be stored. The
@@ -60,6 +61,7 @@ public class FileContentStore extends AbstractContentStore
}
rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true;
}
public String toString()
@@ -70,7 +72,23 @@ public class FileContentStore extends AbstractContentStore
.append("]");
return sb.toString();
}
/**
* Stores may optionally produce readers and writers that support random access.
* Switch this off for this store by setting this to <tt>false</tt>.
* <p>
* This switch is primarily used during testing to ensure that the system has the
* ability to spoof random access in cases where the store is unable to produce
* readers and writers that allow random access. Typically, stream-based access
* would be an example.
*
* @param allowRandomAccess true to allow random access, false to have it faked
*/
public void setAllowRandomAccess(boolean allowRandomAccess)
{
this.allowRandomAccess = allowRandomAccess;
}
/**
* Generates a new URL and file appropriate to it.
*
@@ -193,6 +211,7 @@ public class FileContentStore extends AbstractContentStore
{
File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl);
reader.setAllowRandomAccess(allowRandomAccess);
// done
if (logger.isDebugEnabled())
@@ -233,6 +252,7 @@ public class FileContentStore extends AbstractContentStore
}
// create the writer
FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader);
writer.setAllowRandomAccess(allowRandomAccess);
// done
if (logger.isDebugEnabled())

View File

@@ -17,19 +17,16 @@
package org.alfresco.repo.content.filestore;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentWriter;
import org.alfresco.repo.content.RandomAccessContent;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -40,11 +37,12 @@ import org.apache.commons.logging.LogFactory;
*
* @author Derek Hulley
*/
public class FileContentWriter extends AbstractContentWriter implements RandomAccessContent
public class FileContentWriter extends AbstractContentWriter
{
private static final Log logger = LogFactory.getLog(FileContentWriter.class);
private File file;
private boolean allowRandomAccess;
/**
* Constructor that builds a URL based on the absolute path of the file.
@@ -88,6 +86,12 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
super(url, existingContentReader);
this.file = file;
allowRandomAccess = true;
}
/* package */ void setAllowRandomAccess(boolean allow)
{
this.allowRandomAccess = allow;
}
/**
@@ -118,7 +122,9 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
@Override
protected ContentReader createReader() throws ContentIOException
{
return new FileContentReader(this.file, getContentUrl());
FileContentReader reader = new FileContentReader(this.file, getContentUrl());
reader.setAllowRandomAccess(this.allowRandomAccess);
return reader;
}
@Override
@@ -132,12 +138,23 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
throw new IOException("File exists - overwriting not allowed");
}
// create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it
FileChannel channel = randomAccessFile.getChannel();
WritableByteChannel channel = null;
if (allowRandomAccess)
{
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it
channel = randomAccessFile.getChannel();
}
else
{
OutputStream os = new FileOutputStream(file);
channel = Channels.newChannel(os);
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel to file: " + file);
logger.debug("Opened write channel to file: \n" +
" file: " + file + "\n" +
" random-access: " + allowRandomAccess);
}
return channel;
}
@@ -147,25 +164,6 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
}
}
/**
* @param directChannel a file channel
*/
@Override
protected WritableByteChannel getCallbackWritableChannel(
WritableByteChannel directChannel,
List<ContentStreamListener> listeners) throws ContentIOException
{
if (!(directChannel instanceof FileChannel))
{
throw new AlfrescoRuntimeException("Expected write channel to be a file channel");
}
FileChannel fileChannel = (FileChannel) directChannel;
// wrap it
FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners);
// done
return callbackChannel;
}
/**
* @return Returns true always
*/
@@ -173,51 +171,4 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
{
return true; // this is a writer
}
public FileChannel getChannel() throws ContentIOException
{
/*
* By calling this method, clients indicate that they wish to make random
* changes to the file. It is possible that the client might only want
* to update a tiny proportion of the file - or even none of it. Either
* way, the file must be as whole and complete as before it was accessed.
*/
// go through the super classes to ensure that all concurrency conditions
// and listeners are satisfied
FileChannel channel = (FileChannel) super.getWritableChannel();
// random access means that the the new content's starting point must be
// that of the existing content
ContentReader existingContentReader = getExistingContentReader();
if (existingContentReader != null)
{
ReadableByteChannel existingContentChannel = existingContentReader.getReadableChannel();
long existingContentLength = existingContentReader.getSize();
// copy the existing content
try
{
channel.transferFrom(existingContentChannel, 0, existingContentLength);
// copy complete
if (logger.isDebugEnabled())
{
logger.debug("Copied content for random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader);
}
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy from existing content to enable random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader,
e);
}
finally
{
try { existingContentChannel.close(); } catch (IOException e) {}
}
}
// the file is now available for random access
return channel;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2005 Alfresco, Inc.
*
* Licensed under the Mozilla Public License version 1.1
* with a permitted attribution clause. You may obtain a
* copy of the License at
*
* http://www.alfresco.org/legal/license.txt
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the
* License.
*/
package org.alfresco.repo.content.filestore;
import java.io.File;
import org.alfresco.repo.content.AbstractContentReadWriteTest;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.TempFileProvider;
/**
* Tests the file-based store when random access is not allowed, i.e. has to be spoofed.
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @author Derek Hulley
*/
public class NoRandomAccessFileContentStoreTest extends AbstractContentReadWriteTest
{
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.setAllowRandomAccess(false);
}
@Override
protected ContentStore getStore()
{
return store;
}
}