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

@@ -930,7 +930,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface
// Create the network file // Create the network file
NetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, cifsHelper, nodeRef, params); NetworkFile netFile = ContentNetworkFile.createFile(transactionService, nodeService, contentService, cifsHelper, nodeRef, params);
// Create a file state for the open file // Create a file state for the open file
@@ -1039,7 +1039,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface
NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true); NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true);
// create the network file // create the network file
NetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, cifsHelper, nodeRef, params); NetworkFile netFile = ContentNetworkFile.createFile(transactionService, nodeService, contentService, cifsHelper, nodeRef, params);
// Add a file state for the new file/folder // Add a file state for the new file/folder

View File

@@ -29,14 +29,17 @@ import org.alfresco.filesys.server.filesys.FileOpenParams;
import org.alfresco.filesys.server.filesys.NetworkFile; import org.alfresco.filesys.server.filesys.NetworkFile;
import org.alfresco.i18n.I18NUtil; import org.alfresco.i18n.I18NUtil;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.RandomAccessContent;
import org.alfresco.repo.content.filestore.FileContentReader; import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.transaction.TransactionUtil;
import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
import org.alfresco.service.cmr.repository.ContentAccessor; import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -52,6 +55,7 @@ public class ContentNetworkFile extends NetworkFile
{ {
private static final Log logger = LogFactory.getLog(ContentNetworkFile.class); private static final Log logger = LogFactory.getLog(ContentNetworkFile.class);
private TransactionService transactionService;
private NodeService nodeService; private NodeService nodeService;
private ContentService contentService; private ContentService contentService;
private NodeRef nodeRef; private NodeRef nodeRef;
@@ -68,14 +72,9 @@ public class ContentNetworkFile extends NetworkFile
/** /**
* Helper method to create a {@link NetworkFile network file} given a node reference. * Helper method to create a {@link NetworkFile network file} given a node reference.
*
* @param serviceRegistry
* @param filePathCache used to speed up repeated searches
* @param nodeRef the node representing the file or directory
* @param params the parameters dictating the path and other attributes with which the file is being accessed
* @return Returns a new instance of the network file
*/ */
public static ContentNetworkFile createFile( public static ContentNetworkFile createFile(
TransactionService transactionService,
NodeService nodeService, NodeService nodeService,
ContentService contentService, ContentService contentService,
CifsHelper cifsHelper, CifsHelper cifsHelper,
@@ -88,7 +87,7 @@ public class ContentNetworkFile extends NetworkFile
// TODO: Check access writes and compare to write requirements // TODO: Check access writes and compare to write requirements
// create the file // create the file
ContentNetworkFile netFile = new ContentNetworkFile(nodeService, contentService, nodeRef, path); ContentNetworkFile netFile = new ContentNetworkFile(transactionService, nodeService, contentService, nodeRef, path);
// set relevant parameters // set relevant parameters
if (params.isReadOnlyAccess()) if (params.isReadOnlyAccess())
{ {
@@ -151,10 +150,16 @@ public class ContentNetworkFile extends NetworkFile
return netFile; return netFile;
} }
private ContentNetworkFile(NodeService nodeService, ContentService contentService, NodeRef nodeRef, String name) private ContentNetworkFile(
TransactionService transactionService,
NodeService nodeService,
ContentService contentService,
NodeRef nodeRef,
String name)
{ {
super(name); super(name);
setFullName(name); setFullName(name);
this.transactionService = transactionService;
this.nodeService = nodeService; this.nodeService = nodeService;
this.contentService = contentService; this.contentService = contentService;
this.nodeRef = nodeRef; this.nodeRef = nodeRef;
@@ -259,6 +264,10 @@ public class ContentNetworkFile extends NetworkFile
// Indicate that we have a writable channel to the file // Indicate that we have a writable channel to the file
writableChannel = true; writableChannel = true;
// get the writable channel
// TODO: Decide on truncation strategy
channel = ((ContentWriter) content).getFileChannel(false);
} }
else else
{ {
@@ -272,17 +281,10 @@ public class ContentNetworkFile extends NetworkFile
// Indicate that we only have a read-only channel to the data // Indicate that we only have a read-only channel to the data
writableChannel = false; writableChannel = false;
// get the read-only channel
channel = ((ContentReader) content).getFileChannel();
} }
// wrap the channel accessor, if required
if (!(content instanceof RandomAccessContent))
{
// TODO: create a temp, random access file and put a FileContentWriter on it
// barf for now
throw new AlfrescoRuntimeException("Can only use a store that supplies randomly accessible channel");
}
RandomAccessContent randAccessContent = (RandomAccessContent) content;
// get the channel - we can only make this call once
channel = randAccessContent.getChannel();
} }
@Override @Override
@@ -296,20 +298,38 @@ public class ContentNetworkFile extends NetworkFile
{ {
return; return;
} }
else if (modified) // file was modified
if (modified) // file was modified
{ {
// close it // execute the close (with possible replication listeners, etc) and the
channel.close(); // node update in the same transaction. A transaction will be started
channel = null; // by the nodeService anyway, so it is merely widening the transaction
// write properties // boundaries.
ContentData contentData = content.getContentData(); TransactionWork<Object> closeWork = new TransactionWork<Object>()
nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData); {
public Object doWork() throws Exception
{
// close the channel
// close it
channel.close();
channel = null;
// update node properties
ContentData contentData = content.getContentData();
nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData);
// done
return null;
}
};
TransactionUtil.executeInUserTransaction(transactionService, closeWork);
} }
else else
{ {
// close it - it was not modified // close it - it was not modified
channel.close(); channel.close();
channel = null; channel = null;
// no transaction used here. Any listener operations against this (now unused) content
// are irrelevant.
} }
} }

View File

@@ -26,10 +26,10 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.util.List; import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.transaction.TransactionUtil; import org.alfresco.repo.transaction.TransactionUtil;
import org.alfresco.service.cmr.repository.ContentAccessor; import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@@ -139,17 +139,36 @@ public abstract class AbstractContentAccessor implements ContentAccessor
this.encoding = encoding; 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 * Advise that listens for the completion of specific methods on the
* {@link java.nio.channels.ByteChannel} interface. * {@link java.nio.channels.ByteChannel} interface.
* *
* @author Derek Hulley * @author Derek Hulley
*/ */
protected class ByteChannelCallbackAdvise implements AfterReturningAdvice protected class ChannelCloseCallbackAdvise implements AfterReturningAdvice
{ {
private List<ContentStreamListener> listeners; private List<ContentStreamListener> listeners;
public ByteChannelCallbackAdvise(List<ContentStreamListener> listeners) public ChannelCloseCallbackAdvise(List<ContentStreamListener> listeners)
{ {
this.listeners = listeners; this.listeners = listeners;
} }
@@ -173,11 +192,6 @@ public abstract class AbstractContentAccessor implements ContentAccessor
// nothing to do // nothing to do
return; 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>() TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{ {
public Object doWork() public Object doWork()
@@ -190,11 +204,26 @@ public abstract class AbstractContentAccessor implements ContentAccessor
return null; 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 // done
if (logger.isDebugEnabled()) 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 * Wraps a <code>FileChannel</code> to provide callbacks to listeners when the
* channel is {@link java.nio.channels.Channel#close() closed}. * 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 * @author Derek Hulley
*/ */
@@ -253,7 +286,6 @@ public abstract class AbstractContentAccessor implements ContentAccessor
// nothing to do // nothing to do
return; return;
} }
// create the work to update the listeners
TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>() TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{ {
public Object doWork() public Object doWork()
@@ -266,11 +298,26 @@ public abstract class AbstractContentAccessor implements ContentAccessor
return null; 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 // done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("Content listeners called: close"); logger.debug("" + listeners.size() + " content listeners called: close");
} }
} }

View File

@@ -28,6 +28,8 @@ import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.util.Set; import java.util.Set;
import javax.transaction.UserTransaction;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.alfresco.repo.transaction.DummyTransactionService; 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.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.springframework.context.ApplicationContext;
/** /**
* Abstract base class that provides a set of tests for implementations * Abstract base class that provides a set of tests for implementations
@@ -47,7 +52,11 @@ import org.alfresco.service.cmr.repository.ContentWriter;
*/ */
public abstract class AbstractContentReadWriteTest extends TestCase public abstract class AbstractContentReadWriteTest extends TestCase
{ {
private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
protected TransactionService transactionService;
private String contentUrl; private String contentUrl;
private UserTransaction txn;
public AbstractContentReadWriteTest() public AbstractContentReadWriteTest()
{ {
@@ -58,6 +67,14 @@ public abstract class AbstractContentReadWriteTest extends TestCase
public void setUp() throws Exception public void setUp() throws Exception
{ {
contentUrl = AbstractContentStore.createNewUrl(); 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 public void testRandomAccessWrite() throws Exception
{ {
ContentWriter writer = getWriter(); 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); assertNotNull("No channel given", fileChannel);
// check that no other content access is allowed // 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)); 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(); byte[] bytes = content.getBytes();
writer.putContent(content); writer.putContent(content);
ContentReader reader = writer.getReader(); 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); assertNotNull("No channel given", fileChannel);
// check that no other content access is allowed // check that no other content access is allowed
@@ -631,5 +648,6 @@ public abstract class AbstractContentReadWriteTest extends TestCase
buffer.get(bytes); buffer.get(bytes);
String checkContent = new String(bytes); String checkContent = new String(bytes);
assertEquals("Content read failure", content, checkContent); 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.OutputStream;
import java.io.Reader; import java.io.Reader;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException; 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.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ProxyFactory;
@@ -152,24 +155,38 @@ public abstract class AbstractContentReader extends AbstractContentAccessor impl
protected abstract ReadableByteChannel getDirectReadableChannel() throws ContentIOException; 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 directChannel the result of {@link #getDirectReadableChannel()}
* @param listeners the listeners to call * @param listeners the listeners to call
* @return Returns a channel * @return Returns a channel
* @throws ContentIOException * @throws ContentIOException
*/ */
protected ReadableByteChannel getCallbackReadableChannel( private ReadableByteChannel getCallbackReadableChannel(
ReadableByteChannel directChannel, ReadableByteChannel directChannel,
List<ContentStreamListener> listeners) List<ContentStreamListener> listeners)
throws ContentIOException throws ContentIOException
{ {
// introduce an advistor to handle the callbacks to the listeners ReadableByteChannel callbackChannel = null;
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners); if (directChannel instanceof FileChannel)
ProxyFactory proxyFactory = new ProxyFactory(directChannel); {
proxyFactory.addAdvice(advise); callbackChannel = getCallbackFileChannel((FileChannel) directChannel, listeners);
ReadableByteChannel callbackChannel = (ReadableByteChannel) proxyFactory.getProxy(); }
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 // done
if (logger.isDebugEnabled())
{
logger.debug("Created callback byte channel: \n" +
" original: " + directChannel + "\n" +
" new: " + callbackChannel);
}
return callbackChannel; return callbackChannel;
} }
@@ -195,6 +212,90 @@ public abstract class AbstractContentReader extends AbstractContentAccessor impl
return channel; 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) * @see Channels#newInputStream(java.nio.channels.ReadableByteChannel)
*/ */

View File

@@ -24,16 +24,20 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException; 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.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ProxyFactory;
@@ -165,24 +169,38 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
protected abstract WritableByteChannel getDirectWritableChannel() throws ContentIOException; 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 directChannel the result of {@link #getDirectWritableChannel()}
* @param listeners the listeners to call * @param listeners the listeners to call
* @return Returns a callback channel * @return Returns a channel that executes callbacks
* @throws ContentIOException * @throws ContentIOException
*/ */
protected WritableByteChannel getCallbackWritableChannel( private WritableByteChannel getCallbackWritableChannel(
WritableByteChannel directChannel, WritableByteChannel directChannel,
List<ContentStreamListener> listeners) List<ContentStreamListener> listeners)
throws ContentIOException throws ContentIOException
{ {
// proxy to add an advise WritableByteChannel callbackChannel = null;
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners); if (directChannel instanceof FileChannel)
ProxyFactory proxyFactory = new ProxyFactory(directChannel); {
proxyFactory.addAdvice(advise); callbackChannel = getCallbackFileChannel((FileChannel) directChannel, listeners);
WritableByteChannel callbackChannel = (WritableByteChannel) proxyFactory.getProxy(); }
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 // done
if (logger.isDebugEnabled())
{
logger.debug("Created callback byte channel: \n" +
" original: " + directChannel + "\n" +
" new: " + callbackChannel);
}
return callbackChannel; return callbackChannel;
} }
@@ -210,6 +228,126 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl
return channel; 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) * @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; package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.channels.FileChannel; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentReader; import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.MimetypeMap; 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.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider; import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@@ -43,11 +41,12 @@ import org.apache.commons.logging.LogFactory;
* *
* @author Derek Hulley * @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 static final Log logger = LogFactory.getLog(FileContentReader.class);
private File file; private File file;
private boolean allowRandomAccess;
/** /**
* Checks the existing reader provided and replaces it with a reader onto some * 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); super(url);
this.file = file; 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 @Override
protected ContentReader createReader() throws ContentIOException 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 @Override
@@ -184,12 +191,23 @@ public class FileContentReader extends AbstractContentReader implements RandomAc
throw new IOException("File does not exist"); throw new IOException("File does not exist");
} }
// create the channel // create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it ReadableByteChannel channel = null;
FileChannel channel = randomAccessFile.getChannel(); 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 // done
if (logger.isDebugEnabled()) 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; 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 * @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 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 File rootDirectory;
private String rootAbsolutePath; private String rootAbsolutePath;
private boolean allowRandomAccess;
/** /**
* @param rootDirectory the root under which files will be stored. The * @param rootDirectory the root under which files will be stored. The
@@ -60,6 +61,7 @@ public class FileContentStore extends AbstractContentStore
} }
rootDirectory = rootDirectory.getAbsoluteFile(); rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath(); rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true;
} }
public String toString() public String toString()
@@ -71,6 +73,22 @@ public class FileContentStore extends AbstractContentStore
return sb.toString(); 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. * Generates a new URL and file appropriate to it.
* *
@@ -193,6 +211,7 @@ public class FileContentStore extends AbstractContentStore
{ {
File file = makeFile(contentUrl); File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl); FileContentReader reader = new FileContentReader(file, contentUrl);
reader.setAllowRandomAccess(allowRandomAccess);
// done // done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
@@ -233,6 +252,7 @@ public class FileContentStore extends AbstractContentStore
} }
// create the writer // create the writer
FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader); FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader);
writer.setAllowRandomAccess(allowRandomAccess);
// done // done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())

View File

@@ -17,19 +17,16 @@
package org.alfresco.repo.content.filestore; package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.channels.FileChannel; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel; 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.AbstractContentWriter;
import org.alfresco.repo.content.RandomAccessContent;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -40,11 +37,12 @@ import org.apache.commons.logging.LogFactory;
* *
* @author Derek Hulley * @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 static final Log logger = LogFactory.getLog(FileContentWriter.class);
private File file; private File file;
private boolean allowRandomAccess;
/** /**
* Constructor that builds a URL based on the absolute path of the file. * 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); super(url, existingContentReader);
this.file = file; 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 @Override
protected ContentReader createReader() throws ContentIOException 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 @Override
@@ -132,12 +138,23 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
throw new IOException("File exists - overwriting not allowed"); throw new IOException("File exists - overwriting not allowed");
} }
// create the channel // create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it WritableByteChannel channel = null;
FileChannel channel = randomAccessFile.getChannel(); 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 // done
if (logger.isDebugEnabled()) 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; 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 * @return Returns true always
*/ */
@@ -173,51 +171,4 @@ public class FileContentWriter extends AbstractContentWriter implements RandomAc
{ {
return true; // this is a writer 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;
}
}

View File

@@ -45,6 +45,10 @@ public interface ContentAccessor
/** /**
* Set the transaction provider that will be used when stream listeners are called. * Set the transaction provider that will be used when stream listeners are called.
* No transactions are started unless there are listeners present to be executed.
* For consistency, the execution of listeners <b>will not</b> be allowed to proceed
* unless this property has been set OR the channel close operations are executed
* within the context of a live transaction.
* *
* @param transactionService a transaction provider * @param transactionService a transaction provider
*/ */

View File

@@ -19,6 +19,7 @@ package org.alfresco.service.cmr.repository;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
/** /**
@@ -86,6 +87,19 @@ public interface ContentReader extends ContentAccessor
*/ */
public ReadableByteChannel getReadableChannel() throws ContentIOException; public ReadableByteChannel getReadableChannel() throws ContentIOException;
/**
* Provides read-only, random-access to the underlying content. In general, this method
* should be considered more expensive than the sequential-access method,
* {@link #getReadableChannel()}.
*
* @return Returns a random-access channel onto the content
* @throws ContentIOException
*
* @see #getReadableChannel()
* @see java.io.RandomAccessFile#getChannel()
*/
public FileChannel getFileChannel() throws ContentIOException;
/** /**
* Get a stream to read from the underlying channel * Get a stream to read from the underlying channel
* *

View File

@@ -19,6 +19,7 @@ package org.alfresco.service.cmr.repository;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
@@ -77,6 +78,23 @@ public interface ContentWriter extends ContentAccessor
*/ */
public WritableByteChannel getWritableChannel() throws ContentIOException; public WritableByteChannel getWritableChannel() throws ContentIOException;
/**
* Provides read-write, random-access to the underlying content. In general, this method
* should be considered more expensive than the sequential-access method,
* {@link #getWritableChannel()}.
* <p>
* Underlying implementations use the <code>truncate</code> parameter to determine the
* most effective means of providing access to the content.
*
* @param truncate true to start with zero length content
* @return Returns a random-access channel onto the content
* @throws ContentIOException
*
* @see #getWritableChannel()
* @see java.io.RandomAccessFile#getChannel()
*/
public FileChannel getFileChannel(boolean truncate) throws ContentIOException;
/** /**
* Get a stream to write to the underlying channel. * Get a stream to write to the underlying channel.
* *