mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
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:
@@ -930,7 +930,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1039,7 +1039,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface
|
||||
NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true);
|
||||
|
||||
// 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
|
||||
|
||||
|
@@ -29,14 +29,17 @@ import org.alfresco.filesys.server.filesys.FileOpenParams;
|
||||
import org.alfresco.filesys.server.filesys.NetworkFile;
|
||||
import org.alfresco.i18n.I18NUtil;
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.content.RandomAccessContent;
|
||||
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.ContentData;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
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.NodeService;
|
||||
import org.alfresco.service.transaction.TransactionService;
|
||||
import org.apache.commons.logging.Log;
|
||||
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 TransactionService transactionService;
|
||||
private NodeService nodeService;
|
||||
private ContentService contentService;
|
||||
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.
|
||||
*
|
||||
* @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(
|
||||
TransactionService transactionService,
|
||||
NodeService nodeService,
|
||||
ContentService contentService,
|
||||
CifsHelper cifsHelper,
|
||||
@@ -88,7 +87,7 @@ public class ContentNetworkFile extends NetworkFile
|
||||
// TODO: Check access writes and compare to write requirements
|
||||
|
||||
// create the file
|
||||
ContentNetworkFile netFile = new ContentNetworkFile(nodeService, contentService, nodeRef, path);
|
||||
ContentNetworkFile netFile = new ContentNetworkFile(transactionService, nodeService, contentService, nodeRef, path);
|
||||
// set relevant parameters
|
||||
if (params.isReadOnlyAccess())
|
||||
{
|
||||
@@ -151,10 +150,16 @@ public class ContentNetworkFile extends NetworkFile
|
||||
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);
|
||||
setFullName(name);
|
||||
this.transactionService = transactionService;
|
||||
this.nodeService = nodeService;
|
||||
this.contentService = contentService;
|
||||
this.nodeRef = nodeRef;
|
||||
@@ -259,6 +264,10 @@ public class ContentNetworkFile extends NetworkFile
|
||||
// Indicate that we have a writable channel to the file
|
||||
|
||||
writableChannel = true;
|
||||
|
||||
// get the writable channel
|
||||
// TODO: Decide on truncation strategy
|
||||
channel = ((ContentWriter) content).getFileChannel(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -272,17 +281,10 @@ public class ContentNetworkFile extends NetworkFile
|
||||
// Indicate that we only have a read-only channel to the data
|
||||
|
||||
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
|
||||
@@ -296,20 +298,38 @@ public class ContentNetworkFile extends NetworkFile
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (modified) // file was modified
|
||||
|
||||
|
||||
if (modified) // file was modified
|
||||
{
|
||||
// execute the close (with possible replication listeners, etc) and the
|
||||
// node update in the same transaction. A transaction will be started
|
||||
// by the nodeService anyway, so it is merely widening the transaction
|
||||
// boundaries.
|
||||
TransactionWork<Object> closeWork = new TransactionWork<Object>()
|
||||
{
|
||||
public Object doWork() throws Exception
|
||||
{
|
||||
// close the channel
|
||||
// close it
|
||||
channel.close();
|
||||
channel = null;
|
||||
// write properties
|
||||
// update node properties
|
||||
ContentData contentData = content.getContentData();
|
||||
nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData);
|
||||
// done
|
||||
return null;
|
||||
}
|
||||
};
|
||||
TransactionUtil.executeInUserTransaction(transactionService, closeWork);
|
||||
}
|
||||
else
|
||||
{
|
||||
// close it - it was not modified
|
||||
channel.close();
|
||||
channel = null;
|
||||
// no transaction used here. Any listener operations against this (now unused) content
|
||||
// are irrelevant.
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
};
|
||||
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,11 +298,26 @@ public abstract class AbstractContentAccessor implements ContentAccessor
|
||||
return null;
|
||||
}
|
||||
};
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
{
|
||||
ReadableByteChannel callbackChannel = null;
|
||||
if (directChannel instanceof FileChannel)
|
||||
{
|
||||
callbackChannel = getCallbackFileChannel((FileChannel) directChannel, listeners);
|
||||
}
|
||||
else
|
||||
{
|
||||
// introduce an advistor to handle the callbacks to the listeners
|
||||
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners);
|
||||
ChannelCloseCallbackAdvise advise = new ChannelCloseCallbackAdvise(listeners);
|
||||
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
|
||||
proxyFactory.addAdvice(advise);
|
||||
ReadableByteChannel callbackChannel = (ReadableByteChannel) proxyFactory.getProxy();
|
||||
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)
|
||||
*/
|
||||
|
@@ -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;
|
||||
@@ -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);
|
||||
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);
|
||||
WritableByteChannel callbackChannel = (WritableByteChannel) proxyFactory.getProxy();
|
||||
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)
|
||||
*/
|
||||
|
@@ -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;
|
||||
}
|
@@ -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
|
||||
ReadableByteChannel channel = null;
|
||||
if (allowRandomAccess)
|
||||
{
|
||||
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it
|
||||
FileChannel channel = randomAccessFile.getChannel();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
@@ -71,6 +73,22 @@ public class FileContentStore extends AbstractContentStore
|
||||
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())
|
||||
|
@@ -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
|
||||
WritableByteChannel channel = null;
|
||||
if (allowRandomAccess)
|
||||
{
|
||||
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it
|
||||
FileChannel channel = randomAccessFile.getChannel();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -45,6 +45,10 @@ public interface ContentAccessor
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@@ -19,6 +19,7 @@ package org.alfresco.service.cmr.repository;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
/**
|
||||
@@ -86,6 +87,19 @@ public interface ContentReader extends ContentAccessor
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
@@ -19,6 +19,7 @@ package org.alfresco.service.cmr.repository;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
|
||||
|
||||
@@ -77,6 +78,23 @@ public interface ContentWriter extends ContentAccessor
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
Reference in New Issue
Block a user