diff --git a/source/java/org/alfresco/filesys/alfresco/AlfrescoDiskDriver.java b/source/java/org/alfresco/filesys/alfresco/AlfrescoDiskDriver.java index 023e4da668..1903e38420 100644 --- a/source/java/org/alfresco/filesys/alfresco/AlfrescoDiskDriver.java +++ b/source/java/org/alfresco/filesys/alfresco/AlfrescoDiskDriver.java @@ -18,6 +18,8 @@ package org.alfresco.filesys.alfresco; +import java.util.concurrent.Callable; + import javax.transaction.Status; import javax.transaction.UserTransaction; @@ -33,6 +35,7 @@ import org.alfresco.jlan.server.filesys.TreeConnection; import org.alfresco.jlan.smb.SMBException; import org.alfresco.jlan.smb.SMBStatus; import org.alfresco.jlan.util.DataBuffer; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log; @@ -59,6 +62,10 @@ public abstract class AlfrescoDiskDriver implements IOCtlInterface, Transactiona private TransactionService m_transactionService; + // Remember whether the current thread is already in a retrying transaction + + private ThreadLocal m_inRetryingTransaction = new ThreadLocal(); + /** * Return the service registry * @@ -138,7 +145,7 @@ public abstract class AlfrescoDiskDriver implements IOCtlInterface, Transactiona public void beginReadTransaction(SrvSession sess) { beginTransaction( sess, true); } - + /** * Begin a writeable transaction * @@ -148,6 +155,47 @@ public abstract class AlfrescoDiskDriver implements IOCtlInterface, Transactiona beginTransaction( sess, false); } + /** + * Perform a retryable operation in a write transaction + * + * @param sess + * the server session + * @param callback + * callback for the retryable operation + * @return the result of the operation + */ + public T doInWriteTransaction(SrvSession sess, final Callable callback) + { + Boolean wasInRetryingTransaction = m_inRetryingTransaction.get(); + try + { + boolean hadTransaction = sess.hasTransaction(); + if (hadTransaction) + { + sess.endTransaction(); + } + m_inRetryingTransaction.set(Boolean.TRUE); + T result = m_transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionHelper.RetryingTransactionCallback() + { + + public T execute() throws Throwable + { + return callback.call(); + } + }); + if (hadTransaction) + { + beginReadTransaction(sess); + } + return result; + } + finally + { + m_inRetryingTransaction.set(wasInRetryingTransaction); + } + } + /** * End an active transaction * @@ -227,6 +275,14 @@ public abstract class AlfrescoDiskDriver implements IOCtlInterface, Transactiona private final void beginTransaction( SrvSession sess, boolean readOnly) throws AlfrescoRuntimeException { + // Do nothing if we are already in a retrying transaction + Boolean inRetryingTransaction = m_inRetryingTransaction.get(); + + if (inRetryingTransaction != null && inRetryingTransaction) + { + return; + } + // Initialize the per session thread local that holds the transaction sess.initializeTransactionObject(); diff --git a/source/java/org/alfresco/filesys/avm/AVMDiskDriver.java b/source/java/org/alfresco/filesys/avm/AVMDiskDriver.java index dbb7b04fa6..ed76eff069 100644 --- a/source/java/org/alfresco/filesys/avm/AVMDiskDriver.java +++ b/source/java/org/alfresco/filesys/avm/AVMDiskDriver.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.StringTokenizer; +import java.util.concurrent.Callable; import javax.transaction.UserTransaction; @@ -817,34 +818,35 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * If an error occurs. */ - public void closeFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws java.io.IOException + public void closeFile(final SrvSession sess, final TreeConnection tree, final NetworkFile file) throws java.io.IOException { // DEBUG if ( logger.isDebugEnabled()) logger.debug("Close file " + file.getFullName()); - // Start a transaction if the file has been updated - - if ( file.getWriteCount() > 0) - beginWriteTransaction( sess); - - // Close the file - - file.closeFile(); + doInWriteTransaction(sess, new Callable(){ - // Check if the file/directory is marked for delete + public Void call() throws Exception + { + // Close the file + + file.closeFile(); - if (file.hasDeleteOnClose()) - { + // Check if the file/directory is marked for delete - // Check for a file or directory + if (file.hasDeleteOnClose()) + { - if (file.isDirectory()) - deleteDirectory(sess, tree, file.getFullName()); - else - deleteFile(sess, tree, file.getFullName()); - } + // Check for a file or directory + + if (file.isDirectory()) + deleteDirectory(sess, tree, file.getFullName()); + else + deleteFile(sess, tree, file.getFullName()); + } + return null; + }}); } /** @@ -869,11 +871,11 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface // Split the path to get the new folder name and relative path - String[] paths = FileName.splitPath(params.getPath()); + final String[] paths = FileName.splitPath(params.getPath()); // Convert the relative path to a store path - AVMPath storePath = buildStorePath(ctx, paths[0], sess); + final AVMPath storePath = buildStorePath(ctx, paths[0], sess); // DEBUG @@ -892,13 +894,18 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface // Create a new file - beginWriteTransaction( sess); - try { - // Create the new file entry + doInWriteTransaction(sess, new Callable(){ - m_avmService.createDirectory(storePath.getAVMPath(), paths[1]); + public Void call() throws Exception + { + // Create the new file entry + + m_avmService.createDirectory(storePath.getAVMPath(), paths[1]); + + return null; + }}); } catch (AVMExistsException ex) { @@ -939,20 +946,20 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * If an error occurs. */ - public NetworkFile createFile(SrvSession sess, TreeConnection tree, FileOpenParams params) + public NetworkFile createFile(final SrvSession sess, TreeConnection tree, final FileOpenParams params) throws java.io.IOException { // Check if the filesystem is writable - AVMContext ctx = (AVMContext) tree.getContext(); + final AVMContext ctx = (AVMContext) tree.getContext(); // Split the path to get the file name and relative path - String[] paths = FileName.splitPath(params.getPath()); + final String[] paths = FileName.splitPath(params.getPath()); // Convert the relative path to a store path - AVMPath storePath = buildStorePath(ctx, paths[0], sess); + final AVMPath storePath = buildStorePath(ctx, paths[0], sess); // DEBUG @@ -973,38 +980,41 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface throw new AccessDeniedException("Cannot create " + params.getPath() + ", filesys not writable"); } - // Create a new file - - beginWriteTransaction( sess); - - AVMNetworkFile netFile = null; try { - // Create the new file entry + // Create a new file + return doInWriteTransaction(sess, new Callable(){ - m_avmService.createFile(storePath.getAVMPath(), paths[1]).close(); + public NetworkFile call() throws Exception + { + // Create the new file entry - // Get the new file details + m_avmService.createFile(storePath.getAVMPath(), paths[1]).close(); - AVMPath fileStorePath = buildStorePath(ctx, params.getPath(), sess); - AVMNodeDescriptor nodeDesc = m_avmService.lookup(fileStorePath.getVersion(), fileStorePath.getAVMPath()); + // Get the new file details - if (nodeDesc != null) - { - // Create the network file object for the new file + AVMPath fileStorePath = buildStorePath(ctx, params.getPath(), sess); + AVMNodeDescriptor nodeDesc = m_avmService.lookup(fileStorePath.getVersion(), fileStorePath.getAVMPath()); - netFile = new AVMNetworkFile(nodeDesc, fileStorePath.getAVMPath(), fileStorePath.getVersion(), - m_avmService); - netFile.setGrantedAccess(NetworkFile.READWRITE); - netFile.setFullName(params.getPath()); + if (nodeDesc != null) + { + // Create the network file object for the new file - netFile.setFileId(fileStorePath.generateFileId()); + AVMNetworkFile netFile = new AVMNetworkFile(nodeDesc, fileStorePath.getAVMPath(), fileStorePath.getVersion(), + m_nodeService, m_avmService); + netFile.setGrantedAccess(NetworkFile.READWRITE); + netFile.setFullName(params.getPath()); - // Set the mime-type for the new file + netFile.setFileId(fileStorePath.generateFileId()); - netFile.setMimeType(m_mimetypeService.guessMimetype(paths[1])); - } + // Set the mime-type for the new file + + netFile.setMimeType(m_mimetypeService.guessMimetype(paths[1])); + return netFile; + } + return null; + }}); } catch (AVMExistsException ex) { @@ -1030,10 +1040,6 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface { throw new AccessDeniedException(params.getPath()); } - - // Return the file - - return netFile; } /** @@ -1048,12 +1054,12 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * The exception description. */ - public void deleteDirectory(SrvSession sess, TreeConnection tree, String dir) throws java.io.IOException + public void deleteDirectory(SrvSession sess, TreeConnection tree, final String dir) throws java.io.IOException { // Convert the relative path to a store path AVMContext ctx = (AVMContext) tree.getContext(); - AVMPath storePath = buildStorePath(ctx, dir, sess); + final AVMPath storePath = buildStorePath(ctx, dir, sess); // DEBUG @@ -1072,30 +1078,34 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface // Make sure the path is to a folder before deleting it - beginWriteTransaction( sess); - try { - AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath()); - if (nodeDesc != null) - { - // Check that we are deleting a folder + doInWriteTransaction(sess, new Callable(){ - if (nodeDesc.isDirectory()) + public Void call() throws Exception { - // Make sure the directory is empty + AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath()); + if (nodeDesc != null) + { + // Check that we are deleting a folder - SortedMap fileList = m_avmService.getDirectoryListing(nodeDesc); - if (fileList != null && fileList.size() > 0) - throw new DirectoryNotEmptyException(dir); + if (nodeDesc.isDirectory()) + { + // Make sure the directory is empty - // Delete the folder + SortedMap fileList = m_avmService.getDirectoryListing(nodeDesc); + if (fileList != null && fileList.size() > 0) + throw new DirectoryNotEmptyException(dir); - m_avmService.removeNode(storePath.getAVMPath()); - } - else - throw new IOException("Delete directory path is not a directory, " + dir); - } + // Delete the folder + + m_avmService.removeNode(storePath.getAVMPath()); + } + else + throw new IOException("Delete directory path is not a directory, " + dir); + } + return null; + }}); } catch (AVMNotFoundException ex) { @@ -1123,12 +1133,12 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * The exception description. */ - public void deleteFile(SrvSession sess, TreeConnection tree, String name) throws java.io.IOException + public void deleteFile(SrvSession sess, TreeConnection tree, final String name) throws java.io.IOException { // Convert the relative path to a store path AVMContext ctx = (AVMContext) tree.getContext(); - AVMPath storePath = buildStorePath(ctx, name, sess); + final AVMPath storePath = buildStorePath(ctx, name, sess); // DEBUG @@ -1147,24 +1157,28 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface // Make sure the path is to a file before deleting it - beginWriteTransaction( sess); - try { - AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath()); - if (nodeDesc != null) - { - // Check that we are deleting a file + doInWriteTransaction(sess, new Callable(){ - if (nodeDesc.isFile()) + public Void call() throws Exception { - // Delete the file + AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath()); + if (nodeDesc != null) + { + // Check that we are deleting a file - m_avmService.removeNode(storePath.getAVMPath()); - } - else - throw new IOException("Delete file path is not a file, " + name); - } + if (nodeDesc.isFile()) + { + // Delete the file + + m_avmService.removeNode(storePath.getAVMPath()); + } + else + throw new IOException("Delete file path is not a file, " + name); + } + return null; + }}); } catch (AVMNotFoundException ex) { @@ -1545,7 +1559,7 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface // Create the network file object for the opened file/folder - netFile = new AVMNetworkFile(nodeDesc, storePath.getAVMPath(), storePath.getVersion(), m_avmService); + netFile = new AVMNetworkFile(nodeDesc, storePath.getAVMPath(), storePath.getVersion(), m_nodeService, m_avmService); if (params.isReadOnlyAccess() || storePath.getVersion() != AVMContext.VERSION_HEAD) netFile.setGrantedAccess(NetworkFile.READONLY); @@ -1651,13 +1665,13 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface AVMContext ctx = (AVMContext) tree.getContext(); - String[] oldPaths = FileName.splitPath(oldName); - String[] newPaths = FileName.splitPath(newName); + final String[] oldPaths = FileName.splitPath(oldName); + final String[] newPaths = FileName.splitPath(newName); // Convert the parent paths to store paths - AVMPath oldAVMPath = buildStorePath(ctx, oldPaths[0], sess); - AVMPath newAVMPath = buildStorePath(ctx, newPaths[0], sess); + final AVMPath oldAVMPath = buildStorePath(ctx, oldPaths[0], sess); + final AVMPath newAVMPath = buildStorePath(ctx, newPaths[0], sess); // DEBUG @@ -1679,15 +1693,17 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface throw new AccessDeniedException("Cannot rename folder to read-only folder, " + newName); } - // Start a transaction for the rename - - beginWriteTransaction( sess); - try { - // Rename the file/folder + doInWriteTransaction(sess, new Callable(){ - m_avmService.rename(oldAVMPath.getAVMPath(), oldPaths[1], newAVMPath.getAVMPath(), newPaths[1]); + public Void call() throws Exception + { + // Rename the file/folder + + m_avmService.rename(oldAVMPath.getAVMPath(), oldPaths[1], newAVMPath.getAVMPath(), newPaths[1]); + return null; + }}); } catch (AVMNotFoundException ex) { @@ -1973,7 +1989,7 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * The exception description. */ - public void truncateFile(SrvSession sess, TreeConnection tree, NetworkFile file, long siz) + public void truncateFile(SrvSession sess, TreeConnection tree, final NetworkFile file, final long siz) throws java.io.IOException { // Check if the file is a directory, or only has read access @@ -1985,13 +2001,25 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface AVMNetworkFile avmFile = (AVMNetworkFile) file; - if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false) - beginWriteTransaction( sess); - // Truncate or extend the file + if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false) + { + doInWriteTransaction(sess, new Callable(){ + + public Void call() throws Exception + { + file.truncateFile(siz); + file.flushFile(); + return null; + }}); + } + else + { + file.truncateFile(siz); + file.flushFile(); + } + - file.truncateFile(siz); - file.flushFile(); } /** @@ -2015,8 +2043,8 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface * @exception java.io.IOException * The exception description. */ - public int writeFile(SrvSession sess, TreeConnection tree, NetworkFile file, byte[] buf, int bufoff, int siz, - long fileoff) throws java.io.IOException + public int writeFile(SrvSession sess, TreeConnection tree, final NetworkFile file, final byte[] buf, final int bufoff, final int siz, + final long fileoff) throws java.io.IOException { // Check if the file is a directory, or only has read access @@ -2027,12 +2055,22 @@ public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface AVMNetworkFile avmFile = (AVMNetworkFile) file; - if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false) - beginWriteTransaction( sess); - // Write the data to the file + if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false) + { + doInWriteTransaction(sess, new Callable(){ + + public Void call() throws Exception + { + file.writeFile(buf, siz, bufoff, fileoff); + return null; + }}); + } + else + { + file.writeFile(buf, siz, bufoff, fileoff); + } - file.writeFile(buf, siz, bufoff, fileoff); // Return the actual write length diff --git a/source/java/org/alfresco/filesys/avm/AVMNetworkFile.java b/source/java/org/alfresco/filesys/avm/AVMNetworkFile.java index d3153ebd3c..288437c76f 100644 --- a/source/java/org/alfresco/filesys/avm/AVMNetworkFile.java +++ b/source/java/org/alfresco/filesys/avm/AVMNetworkFile.java @@ -25,13 +25,23 @@ import java.nio.channels.FileChannel; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.filesys.alfresco.AlfrescoNetworkFile; import org.alfresco.jlan.server.filesys.AccessDeniedException; +import org.alfresco.jlan.server.filesys.DiskFullException; import org.alfresco.jlan.server.filesys.FileAttribute; import org.alfresco.jlan.server.filesys.NetworkFile; import org.alfresco.jlan.smb.SeekType; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.avm.AVMNodeConverter; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.cmr.avm.AVMNodeDescriptor; import org.alfresco.service.cmr.avm.AVMService; +import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentReader; 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.cmr.usage.ContentQuotaException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -48,6 +58,10 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { private static final Log logger = LogFactory.getLog(AVMNetworkFile.class); + // Node Service + + private NodeService m_nodeService; + // AVM service private AVMService m_avmService; @@ -64,6 +78,8 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { // Access to the file data, flag to indicate if the file channel is writable private FileChannel m_channel; + private ContentWriter m_content; + private boolean m_writable; // Mime type, if a writer is opened @@ -76,14 +92,16 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { * @param details AVMNodeDescriptor * @param avmPath String * @param avmVersion int + * @param nodeService NodeService * @param avmService AVMService */ - public AVMNetworkFile( AVMNodeDescriptor details, String avmPath, int avmVersion, AVMService avmService) + public AVMNetworkFile( AVMNodeDescriptor details, String avmPath, int avmVersion, NodeService nodeService, AVMService avmService) { super( details.getName()); // Save the service, apth and version + m_nodeService = nodeService; m_avmService = avmService; m_avmPath = avmPath; m_avmVersion = avmVersion; @@ -337,20 +355,59 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { { // If the file is a directory or the file channel has not been opened then there is nothing to do - if ( isDirectory() || m_channel == null) + if ( isDirectory() || m_channel == null && m_content == null) return; - // Close the file channel - - try - { - m_channel.close(); - m_channel = null; - } - catch ( IOException ex) - { - logger.error("Failed to close file channel for " + getName(), ex); - } + // We may be in a retry block, in which case this section will already have executed and channel will be null + if (m_channel != null) + { + // Close the file channel + + try + { + m_channel.close(); + m_channel = null; + } + catch ( IOException ex) + { + if (RetryingTransactionHelper.extractRetryCause(ex) != null) + { + throw ex; + } + logger.error("Failed to close file channel for " + getName(), ex); + } + + } + + if (m_content != null) + { + // Retrieve the content data and stop the content URL from being 'eagerly deleted', in case we need to + // retry the transaction + + final ContentData contentData = m_content.getContentData(); + contentData.reference(); + + try + { + NodeRef nodeRef = AVMNodeConverter.ToNodeRef(-1, m_avmPath); + m_nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData); + } + catch (ContentQuotaException qe) + { + throw new DiskFullException(qe.getMessage()); + } + + // Tidy up after ourselves after a successful commit. Otherwise leave things to allow a + AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() + { + @Override + public void afterCommit() + { + m_content = null; + contentData.deReference(); + } + }); + } } /** @@ -412,21 +469,21 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { // Access the content data and get a file channel to the data - if ( write) + if ( write ) { // Access the content data for write - ContentWriter cWriter = null; + m_content = null; try { // Create a writer to access the file data - cWriter = m_avmService.getContentWriter( m_avmPath); + m_content = m_avmService.getContentWriter(m_avmPath, false); // Set the mime-type - cWriter.setMimetype( getMimeType()); + m_content.setMimetype( getMimeType()); } catch (Exception ex) { logger.debug( ex); @@ -439,7 +496,7 @@ public class AVMNetworkFile extends AlfrescoNetworkFile { // Get the writable channel, do not copy existing content data if the file is to be truncated - m_channel = cWriter.getFileChannel( trunc); + m_channel = m_content.getFileChannel( trunc); // Reset the file position to match the read-only file channel position, unless we truncated the file diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 3f3a1e0f18..c37ab187eb 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -24,8 +24,10 @@ import java.io.Serializable; import java.net.InetAddress; import java.util.Date; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import javax.transaction.UserTransaction; @@ -73,14 +75,14 @@ import org.alfresco.repo.admin.SysAdminParams; import org.alfresco.repo.node.archive.NodeArchiveService; import org.alfresco.repo.security.authentication.AuthenticationContext; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.lock.LockType; import org.alfresco.service.cmr.lock.NodeLockedException; import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentIOException; -import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -91,6 +93,7 @@ import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.extensions.config.ConfigElement; @@ -1842,63 +1845,74 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @return NetworkFile * @exception java.io.IOException If an error occurs. */ - public NetworkFile createFile(SrvSession sess, TreeConnection tree, FileOpenParams params) throws IOException + public NetworkFile createFile(SrvSession sess, final TreeConnection tree, final FileOpenParams params) throws IOException { - // Create the transaction - - beginWriteTransaction( sess); - ContentContext ctx = (ContentContext) tree.getContext(); + final ContentContext ctx = (ContentContext) tree.getContext(); try { - // Get the device root - NodeRef deviceRootNodeRef = ctx.getRootNode(); - - String path = params.getPath(); - FileState parentState = null; - - // If the state table is available then try to find the parent folder node for the new file - // to save having to walk the path - - if ( ctx.hasStateCache()) - { - // See if the parent folder has a file state, we can avoid having to walk the path - - String[] paths = FileName.splitPath(path); - if ( paths[0] != null && paths[0].length() > 1) + // Access the repository in a retryable write transaction + Pair result = doInWriteTransaction(sess, new Callable>(){ + public Pair call() throws Exception { - // Find the node ref for the folder being searched + // Get the device root + + NodeRef deviceRootNodeRef = ctx.getRootNode(); - NodeRef nodeRef = getNodeForPath(tree, paths[0]); - - if ( nodeRef != null) + String path = params.getPath(); + String parentPath = null; + + // If the state table is available then try to find the parent folder node for the new file + // to save having to walk the path + + if (ctx.hasStateCache()) { - deviceRootNodeRef = nodeRef; - path = paths[1]; + // See if the parent folder has a file state, we can avoid having to walk the path - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Create file using cached noderef for path " + paths[0]); + String[] paths = FileName.splitPath(path); + if ( paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + NodeRef nodeRef = getNodeForPath(tree, paths[0]); + + if ( nodeRef != null) + { + deviceRootNodeRef = nodeRef; + path = paths[1]; + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.debug("Create file using cached noderef for path " + paths[0]); + } + + parentPath = paths[0]; + } } - // Get the file state for the parent folder + // Create it - the path will be created, if necessary + + NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); - parentState = getStateForPath(tree, paths[0]); - if ( parentState == null && ctx.hasStateCache()) - parentState = ctx.getStateCache().findFileState( paths[0], true); - } + return new Pair(parentPath, nodeRef); + }}); + + // Get or create the file state for the parent folder + FileState parentState = null; + String parentPath = result.getFirst(); + if (parentPath != null) + { + parentState = getStateForPath(tree, parentPath); + if (parentState == null && ctx.hasStateCache()) + parentState = ctx.getStateCache().findFileState(parentPath, true); } - // Create it - the path will be created, if necessary - - NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true); - nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); - // Create the network file - ContentNetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, mimetypeService, cifsHelper, nodeRef, params); + ContentNetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, mimetypeService, cifsHelper, result.getSecond(), params); // Always allow write access to a newly created file @@ -1933,7 +1947,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa fstate.setFileStatus( FileExists); fstate.incrementOpenCount(); - fstate.setFilesystemObject(nodeRef); + fstate.setFilesystemObject(result.getSecond()); // Store the file state with the file @@ -1954,7 +1968,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Debug if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Created file: path=" + path + " file open parameters=" + params + " node=" + nodeRef + " network file=" + netFile); + logger.debug("Created file: path=" + params.getPath() + " file open parameters=" + params + " node=" + result.getSecond() + " network file=" + netFile); // Return the new network file @@ -2004,59 +2018,72 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param params Directory create parameters * @exception java.io.IOException If an error occurs. */ - public void createDirectory(SrvSession sess, TreeConnection tree, FileOpenParams params) throws IOException + public void createDirectory(SrvSession sess, final TreeConnection tree, final FileOpenParams params) throws IOException { - // Create the transaction - - beginWriteTransaction( sess); - ContentContext ctx = (ContentContext) tree.getContext(); + final ContentContext ctx = (ContentContext) tree.getContext(); try { - // get the device root - - NodeRef deviceRootNodeRef = ctx.getRootNode(); - - String path = params.getPath(); - FileState parentState = null; - - // If the state table is available then try to find the parent folder node for the new folder - // to save having to walk the path - - if ( ctx.hasStateCache()) + // Access the repository in a retryable write transaction + Pair result = doInWriteTransaction(sess, new Callable>() { - // See if the parent folder has a file state, we can avoid having to walk the path - - String[] paths = FileName.splitPath(path); - if ( paths[0] != null && paths[0].length() > 1) + + public Pair call() throws Exception { - // Find the node ref for the folder being searched - - NodeRef nodeRef = getNodeForPath(tree, paths[0]); - - if ( nodeRef != null) + // get the device root + + NodeRef deviceRootNodeRef = ctx.getRootNode(); + + String path = params.getPath(); + String parentPath = null; + + // If the state table is available then try to find the parent folder node for the new folder + // to save having to walk the path + + if ( ctx.hasStateCache()) { - deviceRootNodeRef = nodeRef; - path = paths[1]; - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Create file using cached noderef for path " + paths[0]); + // See if the parent folder has a file state, we can avoid having to walk the path + + String[] paths = FileName.splitPath(path); + if (paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + NodeRef nodeRef = getNodeForPath(tree, paths[0]); + + if (nodeRef != null) + { + deviceRootNodeRef = nodeRef; + path = paths[1]; + + // DEBUG + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.debug("Create file using cached noderef for path " + paths[0]); + } + + parentPath = paths[0]; + } } + + // Create it - the path will be created, if necessary + + NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, false); - // Get the file state for the parent folder - - parentState = getStateForPath(tree, paths[0]); - if ( parentState == null && ctx.hasStateCache()) - parentState = ctx.getStateCache().findFileState( paths[0], true); + return new Pair(parentPath, nodeRef); } + }); + + // Get or create the file state for the parent folder + FileState parentState = null; + String parentPath = result.getFirst(); + if (parentPath != null) + { + parentState = getStateForPath(tree, parentPath); + if (parentState == null && ctx.hasStateCache()) + parentState = ctx.getStateCache().findFileState(parentPath, true); } - // Create it - the path will be created, if necessary - - NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, false); - // Add a file state for the new folder if ( ctx.hasStateCache()) @@ -2067,7 +2094,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Indicate that the file is open fstate.setFileStatus( DirectoryExists); - fstate.setFilesystemObject(nodeRef); + fstate.setFilesystemObject(result.getSecond()); // DEBUG @@ -2084,7 +2111,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Debug if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Created directory: path=" + path + " file open params=" + params + " node=" + nodeRef); + logger.debug("Created directory: path=" + params.getPath() + " file open params=" + params + " node=" + result.getSecond()); } catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) { @@ -2118,59 +2145,61 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param dir Directory name. * @exception java.io.IOException The exception description. */ - public void deleteDirectory(SrvSession sess, TreeConnection tree, String dir) throws IOException + public void deleteDirectory(SrvSession sess, TreeConnection tree, final String dir) throws IOException { - // Create the transaction - - beginWriteTransaction( sess); - // get the device root ContentContext ctx = (ContentContext) tree.getContext(); - NodeRef deviceRootNodeRef = ctx.getRootNode(); + final NodeRef deviceRootNodeRef = ctx.getRootNode(); try { - // Get the node for the folder + NodeRef nodeRef = doInWriteTransaction(sess, new Callable(){ + + public NodeRef call() throws Exception + { + // Get the node for the folder - NodeRef nodeRef = cifsHelper.getNodeRef(deviceRootNodeRef, dir); - if (fileFolderService.exists(nodeRef)) - { - // Check if the folder is empty + NodeRef nodeRef = cifsHelper.getNodeRef(deviceRootNodeRef, dir); + if (fileFolderService.exists(nodeRef)) + { + // Check if the folder is empty - if ( cifsHelper.isFolderEmpty( nodeRef) == true) { - - // Delete the folder node + if ( cifsHelper.isFolderEmpty( nodeRef)) + { + // Delete the folder node - fileFolderService.delete(nodeRef); - - // Remove the file state - - if ( ctx.hasStateCache()) - { - // Remove the file state - - ctx.getStateCache().removeFileState(dir); - - // Update, or create, a parent folder file state - - String[] paths = FileName.splitPath(dir); - if ( paths[0] != null && paths[0].length() > 1) - { - // Get the file state for the parent folder - - FileState parentState = getStateForPath(tree, paths[0]); + fileFolderService.delete(nodeRef); + return nodeRef; + } + else + { + throw new DirectoryNotEmptyException( dir); + } + } + return null; + }}); + if (nodeRef != null && ctx.hasStateCache()) + { + // Remove the file state + + ctx.getStateCache().removeFileState(dir); + + // Update, or create, a parent folder file state + + String[] paths = FileName.splitPath(dir); + if ( paths[0] != null && paths[0].length() > 1) + { + // Get the file state for the parent folder + + FileState parentState = getStateForPath(tree, paths[0]); if ( parentState == null && ctx.hasStateCache()) parentState = ctx.getStateCache().findFileState( paths[0], true); - - // Update the modification timestamp - - parentState.updateModifyDateTime(); - } - } - } - else - throw new DirectoryNotEmptyException( dir); + + // Update the modification timestamp + + parentState.updateModifyDateTime(); + } } // Debug @@ -2239,15 +2268,12 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param param Network file context. * @exception java.io.IOException If an error occurs. */ - public void closeFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws IOException - { - // Create the transaction - - beginWriteTransaction( sess); - + public void closeFile(SrvSession sess, TreeConnection tree, final NetworkFile file) throws IOException + { // Get the associated file state - ContentContext ctx = (ContentContext) tree.getContext(); + final ContentContext ctx = (ContentContext) tree.getContext(); + FileState toUpdate = null; if ( file instanceof ContentNetworkFile) { @@ -2266,31 +2292,24 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa return; } - + if ( ctx.hasStateCache()) { FileState fstate = ctx.getStateCache().findFileState(file.getFullName()); if ( fstate != null) { // If the file open count is now zero then reset the stored sharing mode - + if ( fstate.decrementOpenCount() == 0) - fstate.setSharedAccess( SharingMode.READWRITE + SharingMode.DELETE); - + fstate.setSharedAccess( SharingMode.READWRITE + SharingMode.DELETE); + // Check if there is a cached modification timestamp to be written out if ( file.hasDeleteOnClose() == false && fstate.hasModifyDateTime() && fstate.hasFilesystemObject()) { - // Update the modification date on the file/folder node - - Date modifyDate = new Date( fstate.getModifyDateTime()); - nodeService.setProperty((NodeRef) fstate.getFilesystemObject(), ContentModel.PROP_MODIFIED, modifyDate); - - // Debug - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Updated modifcation timestamp, " + file.getFullName() + ", modTime=" + modifyDate); - } + // Update the modification date on the file/folder node + toUpdate = fstate; + } } } } @@ -2314,99 +2333,130 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa } } - // Defer to the network file to close the stream and remove the content - - file.closeFile(); - - // Remove the node if marked for delete - - if (file.hasDeleteOnClose()) + // Perform repository updates in a retryable write transaction + final FileState finalFileState = toUpdate; + Pair result = doInWriteTransaction(sess, new Callable>() { - // Check if the file is a noderef based file - - if ( file instanceof NodeRefNetworkFile) + public Pair call() throws Exception { - NodeRefNetworkFile nodeNetFile = (NodeRefNetworkFile) file; - NodeRef nodeRef = nodeNetFile.getNodeRef(); - - // We don't know how long the network file has had the reference, so check for existence - - if (fileFolderService.exists(nodeRef)) + // Update the modification date on the file/folder node + if (finalFileState != null) { - try - { - boolean isVersionable = nodeService.hasAspect( nodeRef, ContentModel.ASPECT_VERSIONABLE); - - try - { - // Delete the file - - fileFolderService.delete(nodeRef); - - // Check if there is a quota manager enabled, release space back to the user quota - - if ( ctx.hasQuotaManager()) - ctx.getQuotaManager().releaseSpace(sess, tree, file.getFileId(), file.getFullName(), fileSize); - } - catch ( Exception ex) - { - if ( logger.isWarnEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.warn("Error during delete on close, " + file.getFullName(), ex); - } - - // Set the file state to indicate a delete on close - - if ( ctx.hasStateCache()) - { - if ( isVersionable == true) { - - // Get, or create, the file state - - FileState fState = ctx.getStateCache().findFileState(file.getFullName(), true); - - // Indicate that the file was deleted via a delete on close request - - fState.setFileStatus( DeleteOnClose); + + Date modifyDate = new Date(finalFileState.getModifyDateTime()); + nodeService.setProperty((NodeRef) finalFileState.getFilesystemObject(), ContentModel.PROP_MODIFIED, modifyDate); - // Make sure the file state is cached for a short while, save the noderef details - - fState.setExpiryTime(System.currentTimeMillis() + FileState.RenameTimeout); - fState.setFilesystemObject(nodeRef); - } - else { - - // Remove the file state - - ctx.getStateCache().removeFileState( file.getFullName()); - } + // Debug + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.debug("Updated modifcation timestamp, " + file.getFullName() + ", modTime=" + modifyDate); + } + + // Defer to the network file to close the stream and remove the content + + file.closeFile(); + + // Remove the node if marked for delete + + if (file.hasDeleteOnClose()) + { + // Check if the file is a noderef based file + + if ( file instanceof NodeRefNetworkFile) + { + NodeRefNetworkFile nodeNetFile = (NodeRefNetworkFile) file; + NodeRef nodeRef = nodeNetFile.getNodeRef(); + + // We don't know how long the network file has had the reference, so check for existence + + if (fileFolderService.exists(nodeRef)) + { + try + { + boolean isVersionable = nodeService.hasAspect( nodeRef, ContentModel.ASPECT_VERSIONABLE); + + try + { + // Delete the file + + fileFolderService.delete(nodeRef); + + } + catch ( Exception ex) + { + if (RetryingTransactionHelper.extractRetryCause(ex) != null) + { + throw ex; + } + if ( logger.isWarnEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.warn("Error during delete on close, " + file.getFullName(), ex); + } + + // Return a node ref to update in the state table + return new Pair(nodeRef, isVersionable); + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.debug("Delete on close - access denied, " + file.getFullName()); + + // Convert to a filesystem access denied exception + + throw new AccessDeniedException("Delete on close " + file.getFullName()); + } } } - catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) - { - // Debug - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Delete on close - access denied, " + file.getFullName()); - - // Convert to a filesystem access denied exception - - throw new AccessDeniedException("Delete on close " + file.getFullName()); - } } - } - else if ( file instanceof PseudoNetworkFile || - file instanceof MemoryNetworkFile) + + return null; + }}); + + if (result != null) + { + // Check if there is a quota manager enabled, release space back to the user quota + + if ( ctx.hasQuotaManager()) + ctx.getQuotaManager().releaseSpace(sess, tree, file.getFileId(), file.getFullName(), fileSize); + + // Set the file state to indicate a delete on close + + if (ctx.hasStateCache()) { - // Delete the pseudo file - - if ( hasPseudoFileInterface(ctx)) + if (result.getSecond()) { - // Delete the pseudo file - - getPseudoFileInterface(ctx).deletePseudoFile( sess, tree, file.getFullName()); + + // Get, or create, the file state + + FileState fState = ctx.getStateCache().findFileState(file.getFullName(), true); + + // Indicate that the file was deleted via a delete on close request + + fState.setFileStatus(DeleteOnClose); + + // Make sure the file state is cached for a short while, save the noderef details + + fState.setExpiryTime(System.currentTimeMillis() + FileState.RenameTimeout); + fState.setFilesystemObject(result.getFirst()); + } + else + { + + // Remove the file state + + ctx.getStateCache().removeFileState(file.getFullName()); } } } + else if (file.hasDeleteOnClose() && (file instanceof PseudoNetworkFile || file instanceof MemoryNetworkFile) + && hasPseudoFileInterface(ctx)) + { + // Delete the pseudo file + + getPseudoFileInterface(ctx).deletePseudoFile(sess, tree, file.getFullName()); + + } // DEBUG @@ -2422,92 +2472,109 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param file NetworkFile * @exception java.io.IOException The exception description. */ - public void deleteFile(SrvSession sess, TreeConnection tree, String name) throws IOException + public void deleteFile(final SrvSession sess, final TreeConnection tree, final String name) throws IOException { - // Create the transaction - - beginWriteTransaction( sess); - // Get the device context - ContentContext ctx = (ContentContext) tree.getContext(); + final ContentContext ctx = (ContentContext) tree.getContext(); try { // Check if there is a quota manager enabled, if so then we need to save the current file size - QuotaManager quotaMgr = ctx.getQuotaManager(); - FileInfo fInfo = null; - - if ( quotaMgr != null) { - - // Get the size of the file being deleted - - fInfo = getFileInformation( sess, tree, name); - } - - // Get the node - - NodeRef nodeRef = getNodeForPath(tree, name); - if (fileFolderService.exists(nodeRef)) - { - // Check if the node is versionable - - boolean isVersionable = nodeService.hasAspect( nodeRef, ContentModel.ASPECT_VERSIONABLE); - - fileFolderService.delete(nodeRef); - - // Remove the file state - - if ( ctx.hasStateCache()) - { - // Check if the node is versionable, cache the node details for a short while - - if ( isVersionable == true) { - - // Make sure the file state is cached for a short while, a new file may be renamed to the same name - // in which case we can connect the file to the previous version history - - FileState delState = ctx.getStateCache().findFileState( name, true); - - delState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); - delState.setFileStatus( DeleteOnClose); - delState.setFilesystemObject( nodeRef); - } - else { - - // Remove the file state - - ctx.getStateCache().removeFileState( name); - } - - // Update, or create, a parent folder file state - - String[] paths = FileName.splitPath(name); - if ( paths[0] != null && paths[0].length() > 1) - { - // Get the file state for the parent folder - - FileState parentState = getStateForPath(tree, paths[0]); - if ( parentState == null && ctx.hasStateCache()) - parentState = ctx.getStateCache().findFileState( paths[0], true); + final QuotaManager quotaMgr = ctx.getQuotaManager(); - // Update the modification timestamp - - parentState.updateModifyDateTime(); + // Perform repository updates in a retryable write transaction + Callable postTxn = doInWriteTransaction(sess, new Callable>() + { + public Callable call() throws Exception + { + // Get the size of the file being deleted + final FileInfo fInfo = quotaMgr == null ? null : getFileInformation(sess, tree, name); + + // Get the node and delete it + final NodeRef nodeRef = getNodeForPath(tree, name); + + Callable result = null; + if (fileFolderService.exists(nodeRef)) + { + // Check if the node is versionable + + final boolean isVersionable = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE); + + fileFolderService.delete(nodeRef); + + // Return the operations to perform when the transaction succeeds + result = new Callable() + { + + public Void call() throws Exception + { + // Remove the file state + + if (ctx.hasStateCache()) + { + // Check if the node is versionable, cache the node details for a short while + + if (isVersionable == true) + { + + // Make sure the file state is cached for a short while, a new file may be + // renamed to the same name + // in which case we can connect the file to the previous version history + + FileState delState = ctx.getStateCache().findFileState(name, true); + + delState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); + delState.setFileStatus(DeleteOnClose); + delState.setFilesystemObject(nodeRef); + } + else + { + + // Remove the file state + + ctx.getStateCache().removeFileState(name); + } + + // Update, or create, a parent folder file state + + String[] paths = FileName.splitPath(name); + if (paths[0] != null && paths[0].length() > 1) + { + // Get the file state for the parent folder + + FileState parentState = getStateForPath(tree, paths[0]); + if (parentState == null && ctx.hasStateCache()) + parentState = ctx.getStateCache().findFileState(paths[0], true); + + // Update the modification timestamp + + parentState.updateModifyDateTime(); + } + } + + // Release the space back to the users quota + + if (quotaMgr != null) + quotaMgr.releaseSpace(sess, tree, fInfo.getFileId(), name, fInfo.getSize()); + + return null; + } + }; } + + // Debug + + if (logger.isDebugEnabled() && (ctx.hasDebug(AlfrescoContext.DBG_FILE) || ctx.hasDebug(AlfrescoContext.DBG_RENAME))) + logger.debug("Deleted file: " + name + ", node=" + nodeRef); + + return result; } - - // Release the space back to the users quota - - if ( quotaMgr != null) - quotaMgr.releaseSpace( sess, tree, fInfo.getFileId(), name, fInfo.getSize()); - } + }); - // Debug - - if (logger.isDebugEnabled() && (ctx.hasDebug(AlfrescoContext.DBG_FILE) || ctx.hasDebug(AlfrescoContext.DBG_RENAME))) - logger.debug("Deleted file: " + name + ", node=" + nodeRef); + // Perform state updates after the transaction succeeds + postTxn.call(); } catch (NodeLockedException ex) { @@ -2531,7 +2598,12 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa throw new AccessDeniedException("Delete " + name); } - catch (AlfrescoRuntimeException ex) + catch (IOException ex) + { + // Allow I/O Exceptions to pass through + throw ex; + } + catch (Exception ex) { // Debug @@ -2540,7 +2612,9 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Convert to a general I/O exception - throw new IOException("Delete file " + name); + IOException ioe = new IOException("Delete file " + name); + ioe.initCause(ex); + throw ioe; } } @@ -2553,299 +2627,339 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param newName java.lang.String * @exception java.io.IOException The exception description. */ - public void renameFile(SrvSession sess, TreeConnection tree, String oldName, String newName) throws IOException + public void renameFile(final SrvSession sess, final TreeConnection tree, final String oldName, final String newName) + throws IOException { - // Create the transaction + // Create the transaction (initially read-only) + + beginReadTransaction(sess); - beginWriteTransaction( sess); - // Get the device context - - ContentContext ctx = (ContentContext) tree.getContext(); - + + final ContentContext ctx = (ContentContext) tree.getContext(); + // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) logger.debug("Rename oldName=" + oldName + ", newName=" + newName); - + try { // Get the file/folder to move - - NodeRef nodeToMoveRef = getNodeForPath(tree, oldName); - + + final NodeRef nodeToMoveRef = getNodeForPath(tree, oldName); + // Check if the node is a link node if ( nodeToMoveRef != null && nodeService.getProperty(nodeToMoveRef, ContentModel.PROP_LINK_DESTINATION) != null) throw new AccessDeniedException("Cannot rename link nodes"); - + // Get the new target folder - it must be a folder - + String[] splitPaths = FileName.splitPath(newName); - NodeRef targetFolderRef = getNodeForPath(tree, splitPaths[0]); - String name = splitPaths[1]; + final NodeRef targetFolderRef = getNodeForPath(tree, splitPaths[0]); + final String name = splitPaths[1]; // Check if this is a rename within the same folder - - String[] oldPaths = FileName.splitPath( oldName); - - boolean sameFolder = false; - if ( splitPaths[0].equalsIgnoreCase( oldPaths[0])) - sameFolder = true; - + + String[] oldPaths = FileName.splitPath(oldName); + + final boolean sameFolder = splitPaths[0].equalsIgnoreCase(oldPaths[0]); + // Get the file state for the old file, if available - - FileState oldState = ctx.getStateCache().findFileState( oldName); - + + final FileState oldState = ctx.getStateCache().findFileState(oldName); + // Check if we are renaming a folder, or the rename is to a different folder - - boolean isFolder = cifsHelper.isDirectory( nodeToMoveRef); - + + boolean isFolder = cifsHelper.isDirectory(nodeToMoveRef); + if ( isFolder == true || sameFolder == false) { // Update the old file state - - if ( oldState != null) + + if (oldState != null) { // Update the file state index to use the new name - + ctx.getStateCache().renameFileState(newName, oldState, true); } - + // Rename or move the file/folder - - if ( sameFolder == true) - cifsHelper.rename(nodeToMoveRef, name); - else - cifsHelper.move(nodeToMoveRef, targetFolderRef, name); - + + doInWriteTransaction(sess, new Callable() + { + + public Void call() throws Exception + { + if (sameFolder == true) + cifsHelper.rename(nodeToMoveRef, name); + else + cifsHelper.move(nodeToMoveRef, targetFolderRef, name); + return null; + } + }); + // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Renamed " + (isFolder ? "folder" : "file") + " using " + (sameFolder ? "rename" : "move")); + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Renamed " + (isFolder ? "folder" : "file") + " using " + + (sameFolder ? "rename" : "move")); } else { // Rename a file within the same folder // // Check if the target file already exists - - int newExists = fileExists( sess, tree, newName); - FileState newState = ctx.getStateCache().findFileState( newName, true); - - NodeRef targetNodeRef = null; - - boolean isFromVersionable = nodeService.hasAspect( nodeToMoveRef, ContentModel.ASPECT_VERSIONABLE); - - if ( newExists == FileStatus.FileExists) { - - // Use the existing file as the target node - - targetNodeRef = getNodeForPath( tree, newName); - } - else { - - // Check if the target has a renamed or delete-on-close state - - if ( newState.getFileStatus() == FileRenamed) { - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Using renamed node, " + newState); - - // Use the renamed node to clone aspects/state - - cloneNodeAspects( name, (NodeRef) newState.getFilesystemObject(), nodeToMoveRef, ctx); - } - else if ( newState.getFileStatus() == DeleteOnClose) { - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Restoring delete-on-close node, " + newState); - - // Restore the deleted node so we can relink the new version to the old history/properties - - NodeRef archivedNode = getNodeArchiveService().getArchivedNode((NodeRef) newState.getFilesystemObject()); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Found archived node " + archivedNode); - - if ( archivedNode != null && getNodeService().exists( archivedNode)) - { - // Restore the node - - targetNodeRef = getNodeService().restoreNode( archivedNode, null, null, null); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Restored node " + targetNodeRef); - - // Check if the deleted file had a linked node, due to a rename - - NodeRef linkNode = (NodeRef) newState.findAttribute( AttrLinkNode); - - if ( linkNode != null && nodeService.exists( linkNode)) { - - // Clone aspects from the linked node onto the restored node - - cloneNodeAspects( name, linkNode, targetNodeRef, ctx); + final int newExists = fileExists(sess, tree, newName); + final FileState newState = ctx.getStateCache().findFileState(newName, true); + + List postTxn = doInWriteTransaction(sess, new Callable>() + { + + public List call() throws Exception + { + List postTxn = new LinkedList(); + + NodeRef targetNodeRef = null; + + boolean isFromVersionable = nodeService.hasAspect( nodeToMoveRef, ContentModel.ASPECT_VERSIONABLE); + + if ( newExists == FileStatus.FileExists) { + + // Use the existing file as the target node + + targetNodeRef = getNodeForPath( tree, newName); + } + else { + + // Check if the target has a renamed or delete-on-close state + + if ( newState.getFileStatus() == FileRenamed) { + // DEBUG - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) { - logger.debug(" Moved aspects from linked node " + linkNode); - - // Check if the node is a working copy + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Using renamed node, " + newState); + + // Use the renamed node to clone aspects/state + + cloneNodeAspects( name, (NodeRef) newState.getFilesystemObject(), nodeToMoveRef, ctx); + } + else if ( newState.getFileStatus() == DeleteOnClose) { + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Restoring delete-on-close node, " + newState); + + // Restore the deleted node so we can relink the new version to the old history/properties + + NodeRef archivedNode = getNodeArchiveService().getArchivedNode((NodeRef) newState.getFilesystemObject()); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Found archived node " + archivedNode); + + if ( archivedNode != null && getNodeService().exists( archivedNode)) + { + // Restore the node - if ( nodeService.hasAspect( targetNodeRef, ContentModel.ASPECT_WORKING_COPY)) { + targetNodeRef = getNodeService().restoreNode( archivedNode, null, null, null); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Restored node " + targetNodeRef); + + // Check if the deleted file had a linked node, due to a rename + + NodeRef linkNode = (NodeRef) newState.findAttribute( AttrLinkNode); + + if ( linkNode != null && nodeService.exists( linkNode)) { - // Check if the main document is still locked + // Clone aspects from the linked node onto the restored node - NodeRef mainNodeRef = (NodeRef) nodeService.getProperty( targetNodeRef, ContentModel.PROP_COPY_REFERENCE); - if ( mainNodeRef != null) { - LockType lockTyp = lockService.getLockType( mainNodeRef); - logger.debug(" Main node ref lock type = " + lockTyp); + cloneNodeAspects( name, linkNode, targetNodeRef, ctx); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) { + logger.debug(" Moved aspects from linked node " + linkNode); + + // Check if the node is a working copy + + if ( nodeService.hasAspect( targetNodeRef, ContentModel.ASPECT_WORKING_COPY)) { + + // Check if the main document is still locked + + NodeRef mainNodeRef = (NodeRef) nodeService.getProperty( targetNodeRef, ContentModel.PROP_COPY_REFERENCE); + if ( mainNodeRef != null) { + LockType lockTyp = lockService.getLockType( mainNodeRef); + logger.debug(" Main node ref lock type = " + lockTyp); + } + } } } } } + + // Check if the node being renamed is versionable + + else if ( isFromVersionable == true) { + + // Create a new node for the target + + targetNodeRef = cifsHelper.createNode(ctx.getRootNode(), newName, true); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Created new node for " + newName); + + // Copy aspects from the original file + + cloneNodeAspects( name, nodeToMoveRef, targetNodeRef, ctx); + } } - } - - // Check if the node being renamed is versionable - - else if ( isFromVersionable == true) { - - // Create a new node for the target - - targetNodeRef = cifsHelper.createNode(ctx.getRootNode(), newName, true); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Created new node for " + newName); - // Copy aspects from the original file - - cloneNodeAspects( name, nodeToMoveRef, targetNodeRef, ctx); + // If the original or target nodes are not versionable then just use a standard rename of the + // node + if ( isFromVersionable == false && + ( targetNodeRef == null || nodeService.hasAspect( targetNodeRef, ContentModel.ASPECT_VERSIONABLE) == false)) { + + // Rename the file/folder + + cifsHelper.rename(nodeToMoveRef, name); + + postTxn.add(new Runnable() + { + public void run() + { + // Mark the new file as existing + + newState.setFileStatus(FileExists); + newState.setFilesystemObject(nodeToMoveRef); + + // Make sure the old file state is cached for a short while, the file may not be open so the + // file state could be expired + + oldState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); + + // Indicate that this is a renamed file state, set the node ref of the file that was renamed + + oldState.setFileStatus(FileRenamed); + oldState.setFilesystemObject(nodeToMoveRef); + } + }); + + // DEBUG + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" Use standard rename for " + name + "(versionable=" + isFromVersionable + ", targetNodeRef=" + targetNodeRef + ")"); + } + else { + + // Make sure we have a valid target node + + if ( targetNodeRef == null) { + + // DEBUG + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + logger.debug(" No target node for rename"); + + // Throw an error + + throw new AccessDeniedException("No target node for file rename"); + } + + // Copy content data from the old file to the new file + + copyContentData(sess, tree, nodeToMoveRef, targetNodeRef); + + final NodeRef finalTargetNodeRef = targetNodeRef; + postTxn.add(new Runnable() + { + + public void run() + { + // Mark the new file as existing + + newState.setFileStatus(FileExists); + newState.setFilesystemObject(finalTargetNodeRef); + + // Make sure the old file state is cached for a short while, the file may not be open so the + // file state could be expired + + oldState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); + + // Indicate that this is a deleted file state, set the node ref of the file that was renamed + + oldState.setFileStatus( DeleteOnClose); + oldState.setFilesystemObject(nodeToMoveRef); + + // Link to the new node, a new file may be renamed into place, we need to transfer aspect/locks + + oldState.addAttribute( AttrLinkNode, finalTargetNodeRef); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug( AlfrescoContext.DBG_RENAME)) + logger.debug(" Cached delete state for " + oldName); + } + }); + + // Delete the old file + + nodeService.deleteNode(nodeToMoveRef); + + } + + return postTxn; } + }); + + // Run the required state-changing logic once the retrying transaction has completed successfully + for (Runnable runnable : postTxn) + { + runnable.run(); } - - // If the original or target nodes are not versionable then just use a standard rename of the node - - if ( isFromVersionable == false && - ( targetNodeRef == null || nodeService.hasAspect( targetNodeRef, ContentModel.ASPECT_VERSIONABLE) == false)) { - - // Rename the file/folder - - cifsHelper.rename(nodeToMoveRef, name); - - // Mark the new file as existing - - newState.setFileStatus( FileExists); - newState.setFilesystemObject( nodeToMoveRef); - - // Make sure the old file state is cached for a short while, the file may not be open so the - // file state could be expired - - oldState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); - - // Indicate that this is a renamed file state, set the node ref of the file that was renamed - - oldState.setFileStatus( FileRenamed); - oldState.setFilesystemObject(nodeToMoveRef); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" Use standard rename for " + name + "(versionable=" + isFromVersionable + ", targetNodeRef=" + targetNodeRef + ")"); - } - else { - - // Make sure we have a valid target node - - if ( targetNodeRef == null) { - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) - logger.debug(" No target node for rename"); - - // Throw an error - - throw new AccessDeniedException( "No target node for file rename"); - } - - // Copy content data from the old file to the new file - - copyContentData( sess, tree, nodeToMoveRef, targetNodeRef); - - // Mark the new file as existing - - newState.setFileStatus( FileExists); - newState.setFilesystemObject( targetNodeRef); - - // Delete the old file - - nodeService.deleteNode( nodeToMoveRef); - - // Make sure the old file state is cached for a short while, the file may not be open so the - // file state could be expired - - oldState.setExpiryTime(System.currentTimeMillis() + FileState.DeleteTimeout); - - // Indicate that this is a deleted file state, set the node ref of the file that was renamed - - oldState.setFileStatus( DeleteOnClose); - oldState.setFilesystemObject(nodeToMoveRef); - - // Link to the new node, a new file may be renamed into place, we need to transfer aspect/locks - - oldState.addAttribute( AttrLinkNode, targetNodeRef); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug( AlfrescoContext.DBG_RENAME)) - logger.debug(" Cached delete state for " + oldName); - } } } catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) { // Debug - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) logger.debug("Rename file - access denied, " + oldName); - + // Convert to a filesystem access denied status - + throw new AccessDeniedException("Rename file " + oldName); } catch (NodeLockedException ex) { // Debug - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) logger.debug("Rename file", ex); - + // Convert to an filesystem access denied exception - + throw new AccessDeniedException("Node locked " + oldName); } catch (AlfrescoRuntimeException ex) { // Debug - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) + + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_RENAME)) logger.debug("Rename file", ex); - + // Convert to a general I/O exception - + throw new AccessDeniedException("Rename file " + oldName); } } @@ -2859,11 +2973,11 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param info FileInfo * @exception IOException */ - public void setFileInformation(SrvSession sess, TreeConnection tree, String name, FileInfo info) throws IOException + public void setFileInformation(SrvSession sess, final TreeConnection tree, final String name, final FileInfo info) throws IOException { // Get the device context - ContentContext ctx = (ContentContext) tree.getContext(); + final ContentContext ctx = (ContentContext) tree.getContext(); try { @@ -2877,120 +2991,121 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa return; } - // Get the file/folder node - - NodeRef nodeRef = getNodeForPath(tree, name); - FileState fstate = getStateForPath(tree, name); - - // Check permissions on the file/folder node - - if ( permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.DENIED) - throw new AccessDeniedException("No write access to " + name); - - if ( permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.DENIED) - throw new AccessDeniedException("No delete access to " + name); - + final FileState fstate = getStateForPath(tree, name); + + doInWriteTransaction(sess, new Callable>(){ + + public Pair call() throws Exception + { + // Get the file/folder node + + NodeRef nodeRef = getNodeForPath(tree, name); + + // Check permissions on the file/folder node + + if ( permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.DENIED) + throw new AccessDeniedException("No write access to " + name); + + if ( permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.DENIED) + throw new AccessDeniedException("No delete access to " + name); + + // Check if the file is being marked for deletion, if so then check if the file is locked + + if ( info.hasSetFlag(FileInfo.SetDeleteOnClose) && info.hasDeleteOnClose()) + { + // Check if the node is locked + + if ( nodeService.hasAspect( nodeRef, ContentModel.ASPECT_LOCKABLE)) + { + // Get the lock type, if any + + String lockTypeStr = (String) nodeService.getProperty( nodeRef, ContentModel.PROP_LOCK_TYPE); + + if ( lockTypeStr != null) + throw new AccessDeniedException("Node locked, cannot mark for delete"); + } + + // Get the node for the folder + + if (fileFolderService.exists(nodeRef)) + { + // Check if it is a folder that is being deleted, make sure it is empty + + boolean isFolder = true; + + if ( fstate != null) + isFolder = fstate.isDirectory(); + else { + ContentFileInfo cInfo = cifsHelper.getFileInformation( nodeRef); + if ( cInfo != null && cInfo.isDirectory() == false) + isFolder = false; + } + + // Check if the folder is empty + + if ( isFolder == true && cifsHelper.isFolderEmpty( nodeRef) == false) + throw new DirectoryNotEmptyException( name); + } + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) + logger.debug("Set deleteOnClose=true file=" + name); + } + + // Set the creation date/time + + if ( info.hasSetFlag(FileInfo.SetCreationDate)) { + + // Set the creation date on the file/folder node + + Date createDate = new Date( info.getCreationDateTime()); + nodeService.setProperty( nodeRef, ContentModel.PROP_CREATED, createDate); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) + logger.debug("Set creationDate=" + createDate + " file=" + name); + } + + // Set the modification date/time + + if ( info.hasSetFlag(FileInfo.SetModifyDate)) { + + // Set the modification date on the file/folder node + + Date modifyDate = new Date( info.getModifyDateTime()); + nodeService.setProperty( nodeRef, ContentModel.PROP_MODIFIED, modifyDate); + + // DEBUG + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) + logger.debug("Set modifyDate=" + modifyDate + " file=" + name); + } + + return null; + }}); // Check if the file is being marked for deletion, if so then check if the file is locked - - if ( info.hasSetFlag(FileInfo.SetDeleteOnClose) && info.hasDeleteOnClose()) + + // Update the change date/time + if (fstate != null) { - // Start a transaction - - beginReadTransaction( sess); - - // Check if the node is locked - - if ( nodeService.hasAspect( nodeRef, ContentModel.ASPECT_LOCKABLE)) + if (info.hasSetFlag(FileInfo.SetDeleteOnClose) && info.hasDeleteOnClose() + || info.hasSetFlag(FileInfo.SetCreationDate)) { - // Get the lock type, if any - - String lockTypeStr = (String) nodeService.getProperty( nodeRef, ContentModel.PROP_LOCK_TYPE); - - if ( lockTypeStr != null) - throw new AccessDeniedException("Node locked, cannot mark for delete"); + + fstate.updateChangeDateTime(); } - - // Get the node for the folder - - if (fileFolderService.exists(nodeRef)) + + // Set the modification date/time + + if (info.hasSetFlag(FileInfo.SetModifyDate)) { - // Check if it is a folder that is being deleted, make sure it is empty - - boolean isFolder = true; - - if ( fstate != null) - isFolder = fstate.isDirectory(); - else { - ContentFileInfo cInfo = cifsHelper.getFileInformation( nodeRef); - if ( cInfo != null && cInfo.isDirectory() == false) - isFolder = false; - } - // Check if the folder is empty - - if ( isFolder == true && cifsHelper.isFolderEmpty( nodeRef) == false) - throw new DirectoryNotEmptyException( name); - } - - // Update the change date/time - - if ( fstate != null) - fstate.updateChangeDateTime(); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) - logger.debug("Set deleteOnClose=true file=" + name); - } - - // Set the creation date/time - - if ( info.hasSetFlag(FileInfo.SetCreationDate)) { - - // Create the transaction - - beginWriteTransaction( sess); - - // Set the creation date on the file/folder node - - Date createDate = new Date( info.getCreationDateTime()); - nodeService.setProperty( nodeRef, ContentModel.PROP_CREATED, createDate); - - // Update the change date/time - - if ( fstate != null) - fstate.updateChangeDateTime(); - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) - logger.debug("Set creationDate=" + createDate + " file=" + name); - } - - // Set the modification date/time - - if ( info.hasSetFlag(FileInfo.SetModifyDate)) { - - // Create the transaction - - beginWriteTransaction( sess); - - // Set the modification date on the file/folder node - - Date modifyDate = new Date( info.getModifyDateTime()); - nodeService.setProperty( nodeRef, ContentModel.PROP_MODIFIED, modifyDate); - - // Update the change date/time, clear the cached modification date/time - - if ( fstate != null) { - fstate.updateChangeDateTime(); - fstate.updateModifyDateTime( 0L); - } - - // DEBUG - - if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_INFO)) - logger.debug("Set modifyDate=" + modifyDate + " file=" + name); + // Update the change date/time, clear the cached modification date/time + fstate.updateChangeDateTime(); + fstate.updateModifyDateTime(0L); + } } } catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) @@ -3439,25 +3554,11 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa * @param tree TreeConnection * @param fromNode NodeRef * @param toNode NodeRef - * @exception IOException */ private void copyContentData( SrvSession sess, TreeConnection tree, NodeRef fromNode, NodeRef toNode) - throws IOException { - - try { - - // Open the input and output files - - ContentReader fromReader = contentService.getReader( fromNode, ContentModel.PROP_CONTENT); - ContentWriter toWriter = contentService.getWriter( toNode, ContentModel.PROP_CONTENT, true); - - // Copy the content - - toWriter.putContent( fromReader); - } - catch ( Exception ex) { - throw new IOException("Failed to copy content"); - } + { + ContentData content = (ContentData) nodeService.getProperty(fromNode, ContentModel.PROP_CONTENT); + nodeService.setProperty(toNode, ContentModel.PROP_CONTENT, content); } diff --git a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java index 8af6d735d5..131c15d201 100644 --- a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java @@ -40,6 +40,8 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.content.AbstractContentReader; import org.alfresco.repo.content.encoding.ContentCharsetFinder; import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.cmr.repository.ContentAccessor; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentIOException; @@ -78,7 +80,7 @@ public class ContentNetworkFile extends NodeRefNetworkFile // File content private ContentAccessor content; - private ContentReader preUpdateContentReader; + private String preUpdateContentURL; // Indicate if file has been written to or truncated/resized @@ -319,16 +321,20 @@ public class ContentNetworkFile extends NodeRefNetworkFile } content = null; - preUpdateContentReader = null; + preUpdateContentURL = null; if (write) { // Get a writeable channel to the content, along with the original content content = contentService.getWriter( getNodeRef(), ContentModel.PROP_CONTENT, false); - + // Keep the original content for later comparison - preUpdateContentReader = contentService.getReader( getNodeRef(), ContentModel.PROP_CONTENT); + ContentData preUpdateContentData = (ContentData) nodeService.getProperty( getNodeRef(), ContentModel.PROP_CONTENT); + if (preUpdateContentData != null) + { + preUpdateContentURL = preUpdateContentData.getContentUrl(); + } // Indicate that we have a writable channel to the file @@ -388,7 +394,7 @@ public class ContentNetworkFile extends NodeRefNetworkFile return; } - else if (channel == null) + else if (!hasContent()) { // File was not read/written so channel was not opened @@ -399,26 +405,37 @@ public class ContentNetworkFile extends NodeRefNetworkFile if (modified) { - // Take a guess at the mimetype - channel.position(0); - InputStream is = new BufferedInputStream(Channels.newInputStream(channel)); - ContentCharsetFinder charsetFinder = mimetypeService.getContentCharsetFinder(); - Charset charset = charsetFinder.getCharset(is, content.getMimetype()); - content.setEncoding(charset.name()); - - // Close the channel - - channel.close(); - channel = null; + // We may be in a retry block, in which case this section will already have executed and channel will be null + if (channel != null) + { + // Take a guess at the mimetype + channel.position(0); + InputStream is = new BufferedInputStream(Channels.newInputStream(channel)); + ContentCharsetFinder charsetFinder = mimetypeService.getContentCharsetFinder(); + Charset charset = charsetFinder.getCharset(is, content.getMimetype()); + content.setEncoding(charset.name()); + + // Close the channel + + channel.close(); + channel = null; + } + // Retrieve the content data and stop the content URL from being 'eagerly deleted', in case we need to + // retry the transaction + + final ContentData contentData = content.getContentData(); + contentData.reference(); + // Update node properties, but only if the binary has changed (ETHREEOH-1861) ContentReader postUpdateContentReader = ((ContentWriter) content).getReader(); - boolean contentChanged = !AbstractContentReader.compareContentReaders(preUpdateContentReader, postUpdateContentReader); + boolean contentChanged = preUpdateContentURL == null + || !AbstractContentReader.compareContentReaders(contentService.getRawReader(preUpdateContentURL), + postUpdateContentReader); if (contentChanged) { - ContentData contentData = content.getContentData(); NodeRef contentNodeRef = getNodeRef(); nodeService.removeAspect(contentNodeRef, ContentModel.ASPECT_NO_CONTENT); try @@ -430,8 +447,20 @@ public class ContentNetworkFile extends NodeRefNetworkFile throw new DiskFullException(qe.getMessage()); } } + + // Tidy up after ourselves after a successful commit. Otherwise leave things to allow a retry. + AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() + { + @Override + public void afterCommit() + { + content = null; + contentData.deReference(); + preUpdateContentURL = null; + } + }); } - else + else if (channel != null) { // Close it - it was not modified diff --git a/source/java/org/alfresco/filesys/repo/desk/CheckInOutDesktopAction.java b/source/java/org/alfresco/filesys/repo/desk/CheckInOutDesktopAction.java index 0620eb0369..ab00a0dde5 100644 --- a/source/java/org/alfresco/filesys/repo/desk/CheckInOutDesktopAction.java +++ b/source/java/org/alfresco/filesys/repo/desk/CheckInOutDesktopAction.java @@ -14,13 +14,22 @@ * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" */ package org.alfresco.filesys.repo.desk; import java.io.Serializable; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import org.alfresco.filesys.alfresco.DesktopAction; import org.alfresco.filesys.alfresco.DesktopParams; @@ -31,10 +40,14 @@ import org.alfresco.jlan.server.filesys.FileStatus; import org.alfresco.jlan.server.filesys.NotifyChange; import org.alfresco.jlan.server.filesys.cache.FileState; import org.alfresco.jlan.server.filesys.cache.FileStateCache; +import org.alfresco.jlan.smb.server.notify.NotifyChangeHandler; import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.util.Pair; /** * Check In/Out Desktop Action Class @@ -45,10 +58,6 @@ import org.alfresco.service.cmr.repository.NodeService; */ public class CheckInOutDesktopAction extends DesktopAction { - // Check in/out service - - private CheckOutCheckInService m_checkInOutService; - /** * Class constructor */ @@ -74,109 +83,124 @@ public class CheckInOutDesktopAction extends DesktopAction { * @return DesktopResponse */ @Override - public DesktopResponse runAction(DesktopParams params) { + public DesktopResponse runAction(final DesktopParams params) { // Check if there are any files/folders to process if ( params.numberOfTargetNodes() == 0) return new DesktopResponse(StsSuccess); - - // Get required services - - NodeService nodeService = getServiceRegistry().getNodeService(); - - // Start a transaction - - params.getDriver().beginWriteTransaction( params.getSession()); - - // Process the list of target nodes - - DesktopResponse response = new DesktopResponse(StsSuccess); - - for ( int idx = 0; idx < params.numberOfTargetNodes(); idx++) + + class WriteTxn implements Callable { - // Get the current target node - - DesktopTarget target = params.getTarget(idx); - - // Check if the node is a working copy - - if ( nodeService.hasAspect( target.getNode(), ContentModel.ASPECT_WORKING_COPY)) + private List> fileChanges; + + /* (non-Javadoc) + * @see java.util.concurrent.Callable#call() + */ + public DesktopResponse call() throws Exception { - try + // Initialize / reset the list of file changes + fileChanges = new LinkedList>(); + + // Get required services + + ServiceRegistry serviceRegistry = getServiceRegistry(); + NodeService nodeService = serviceRegistry.getNodeService(); + CheckOutCheckInService checkOutCheckInService = serviceRegistry.getCheckOutCheckInService(); + + // Process the list of target nodes + + DesktopResponse response = new DesktopResponse(StsSuccess); + + for ( int idx = 0; idx < params.numberOfTargetNodes(); idx++) { - // Check in the file, pass an empty version properties so that versionable nodes create a new version + // Get the current target node - Map versionProperties = new HashMap(); - getCheckInOutService().checkin( target.getNode(), versionProperties, null, false); - - // Check if there are any file/directory change notify requests active - - if ( getContext().hasFileServerNotifications()) { - - // Build the relative path to the checked in file - - String fileName = null; - - if ( target.getTarget().startsWith(FileName.DOS_SEPERATOR_STR)) + DesktopTarget target = params.getTarget(idx); + + // Check if the node is a working copy + + if ( nodeService.hasAspect( target.getNode(), ContentModel.ASPECT_WORKING_COPY)) + { + try { - // Path is already relative to filesystem root - - fileName = target.getTarget(); + // Check in the file, pass an empty version properties so that veriosnable nodes create a new version + + Map versionProperties = new HashMap(); + checkOutCheckInService.checkin( target.getNode(), versionProperties, null, false); + + // Check if there are any file/directory change notify requests active + + if ( getContext().hasFileServerNotifications()) { + + // Build the relative path to the checked in file + + String fileName = null; + + if ( target.getTarget().startsWith(FileName.DOS_SEPERATOR_STR)) + { + // Path is already relative to filesystem root + + fileName = target.getTarget(); + } + else + { + // Build a root relative path for the file + + fileName = FileName.buildPath( params.getFolder().getFullName(), null, target.getTarget(), FileName.DOS_SEPERATOR); + } + + // Queue a file deleted change notification + fileChanges.add(new Pair(NotifyChange.ActionRemoved, fileName)); + } } - else + catch (Exception ex) { - // Build a root relative path for the file - - fileName = FileName.buildPath( params.getFolder().getFullName(), null, target.getTarget(), FileName.DOS_SEPERATOR); + // If this is a 'retryable' exception, pass it on + if (RetryingTransactionHelper.extractRetryCause(ex) != null) + { + throw ex; + } + + // Dump the error + + if ( logger.isErrorEnabled()) + logger.error("Desktop action error", ex); + + // Return an error status and message + + response.setStatus(StsError, "Checkin failed for " + target.getTarget() + ", " + ex.getMessage()); } - - // Queue a file deleted change notification - - getContext().getChangeHandler().notifyFileChanged(NotifyChange.ActionRemoved, fileName); } - } - catch (Exception ex) - { - // Dump the error - - if ( logger.isErrorEnabled()) - logger.error("Desktop action error", ex); - - // Return an error status and message - - response.setStatus(StsError, "Checkin failed for " + target.getTarget() + ", " + ex.getMessage()); - } - } - else - { - try - { - // Check if the file is locked - - if ( nodeService.hasAspect( target.getNode(), ContentModel.ASPECT_LOCKABLE)) { - - // Get the lock type - - String lockTypeStr = (String) nodeService.getProperty( target.getNode(), ContentModel.PROP_LOCK_TYPE); - if ( lockTypeStr != null) { - response.setStatus(StsError, "Checkout failed, file is locked"); - return response; - } - } - - // Check out the file - - NodeRef workingCopyNode = getCheckInOutService().checkout( target.getNode()); + else + { + try + { + // Check if the file is locked + + if ( nodeService.hasAspect( target.getNode(), ContentModel.ASPECT_LOCKABLE)) { + + // Get the lock type + + String lockTypeStr = (String) nodeService.getProperty( target.getNode(), ContentModel.PROP_LOCK_TYPE); + if ( lockTypeStr != null) { + response.setStatus(StsError, "Checkout failed, file is locked"); + return response; + } + } + + // Check out the file + + NodeRef workingCopyNode = checkOutCheckInService.checkout( target.getNode()); - // Get the working copy file name - - String workingCopyName = (String) nodeService.getProperty( workingCopyNode, ContentModel.PROP_NAME); - - // Check out was successful, pack the working copy name + // Get the working copy file name + + String workingCopyName = (String) nodeService.getProperty( workingCopyNode, ContentModel.PROP_NAME); + + // Check out was successful, pack the working copy name - response.setStatus(StsSuccess, "Checked out working copy " + workingCopyName); - + response.setStatus(StsSuccess, "Checked out working copy " + workingCopyName); + // Build the relative path to the checked out file String fileName = FileName.buildPath( params.getFolder().getFullName(), null, workingCopyName, FileName.DOS_SEPERATOR); @@ -193,50 +217,63 @@ public class CheckInOutDesktopAction extends DesktopAction { fstate.setFileStatus( FileStatus.FileExists); } - // Check if there are any file/directory change notify requests active + // Check if there are any file/directory change notify requests active - if ( getContext().hasChangeHandler()) { + if ( getContext().hasChangeHandler()) { - // Queue a file added change notification - - getContext().getChangeHandler().notifyFileChanged(NotifyChange.ActionAdded, fileName); + // Build the relative path to the checked in file + + // Queue a file added change notification + fileChanges.add(new Pair(NotifyChange.ActionAdded, fileName)); + } + } + catch (Exception ex) + { + // If this is a 'retryable' exception, pass it on + if (RetryingTransactionHelper.extractRetryCause(ex) != null) + { + throw ex; + } + + // Dump the error + + if ( logger.isErrorEnabled()) + logger.error("Desktop action error", ex); + + // Return an error status and message + + response.setStatus(StsError, "Failed to checkout " + target.getTarget() + ", " + ex.getMessage()); + } } } - catch (Exception ex) + + // Return a success status for now + + return response; + } + + + /** + * Queue the file change notifications resulting from this successfully processed transaction. + */ + public void notifyChanges() + { + NotifyChangeHandler notifyChangeHandler = getContext().getChangeHandler(); + for (Pair fileChange : fileChanges) { - // Dump the error - - if ( logger.isErrorEnabled()) - logger.error("Desktop action error", ex); - - // Return an error status and message - - response.setStatus(StsError, "Failed to checkout " + target.getTarget() + ", " + ex.getMessage()); + notifyChangeHandler.notifyFileChanged(fileChange.getFirst(), fileChange.getSecond()); } } + } - // Return a success status for now + // Process the transaction + WriteTxn callback = new WriteTxn(); + DesktopResponse response = params.getDriver().doInWriteTransaction(params.getSession(), callback); + + // Queue file change notifications + callback.notifyChanges(); - return response; - } - - /** - * Get the check in/out service - * - * @return CheckOutCheckInService - */ - protected final CheckOutCheckInService getCheckInOutService() - { - // Check if the service has been cached - - if ( m_checkInOutService == null) - { - m_checkInOutService = getServiceRegistry().getCheckOutCheckInService(); - } - - // Return the check in/out service - - return m_checkInOutService; - } + return response; + } } diff --git a/source/java/org/alfresco/filesys/repo/desk/JavaScriptDesktopAction.java b/source/java/org/alfresco/filesys/repo/desk/JavaScriptDesktopAction.java index 2dccb964ae..f091aa1fa6 100644 --- a/source/java/org/alfresco/filesys/repo/desk/JavaScriptDesktopAction.java +++ b/source/java/org/alfresco/filesys/repo/desk/JavaScriptDesktopAction.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; +import java.util.concurrent.Callable; import org.springframework.extensions.config.ConfigElement; import org.alfresco.filesys.alfresco.AlfrescoContext; @@ -34,6 +35,7 @@ import org.alfresco.filesys.alfresco.DesktopActionException; import org.alfresco.filesys.alfresco.DesktopParams; import org.alfresco.filesys.alfresco.DesktopResponse; import org.alfresco.jlan.server.filesys.DiskSharedDevice; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.scripts.ScriptException; import org.alfresco.service.cmr.repository.ScriptService; import org.alfresco.util.ResourceFinder; @@ -48,10 +50,6 @@ import org.springframework.core.io.Resource; */ public class JavaScriptDesktopAction extends DesktopAction { - // Script service - - private ScriptService m_scriptService; - // Script name private String m_scriptName; @@ -193,40 +191,37 @@ public class JavaScriptDesktopAction extends DesktopAction { @Override public DesktopResponse runAction(DesktopParams params) throws DesktopActionException - { - // Check if the script file has been changed - - DesktopResponse response = new DesktopResponse(StsSuccess); - + { File scriptFile = new File(m_scriptPath); - if ( scriptFile.lastModified() != m_lastModified) - { - // Reload the script - - m_lastModified = scriptFile.lastModified(); - - try - { - loadScript( scriptFile); - } - catch ( IOException ex) - { - response.setStatus(StsError, "Failed to reload script file, " + getScriptName()); - return response; - } - } - - // Start a transaction - params.getDriver().beginWriteTransaction( params.getSession()); + synchronized (this) + { + if (scriptFile.lastModified() != m_lastModified) + { + // Reload the script + m_lastModified = scriptFile.lastModified(); + + try + { + loadScript(scriptFile); + } + catch (IOException ex) + { + // Check if the script file has been changed + return new DesktopResponse(StsError, "Failed to reload script file, " + getScriptName()); + } + } + } + // Access the script service + final ScriptService scriptService = getServiceRegistry().getScriptService(); - if ( getScriptService() != null) + if ( scriptService != null) { - // Create the objects to be passed to the script + // Create the objects to be passed to the script - Map model = new HashMap(); + final Map model = new HashMap(); model.put("deskParams", params); model.put("out", System.out); @@ -235,105 +230,95 @@ public class JavaScriptDesktopAction extends DesktopAction { if ( hasWebappURL()) model.put("webURL", getWebappURL()); - // Start a transaction - - params.getDriver().beginWriteTransaction( params.getSession()); + // Compute the response in a retryable write transaction + return params.getDriver().doInWriteTransaction(params.getSession(), new Callable() + { - // Run the script - - Object result = null; - - try - { - // Run the script - - result = getScriptService().executeScriptString( getScript(), model); - - // Check the result - - if ( result != null) - { - // Check for a full response object - - if ( result instanceof DesktopResponse) - { - response = (DesktopResponse) result; - } - - // Status code only response - - else if ( result instanceof Double) - { - Double jsSts = (Double) result; - response.setStatus( jsSts.intValue(), ""); - } - - // Encoded response in the format ',' - - else if ( result instanceof String) - { - String responseMsg = (String) result; - - // Parse the status message - - StringTokenizer token = new StringTokenizer( responseMsg, ","); - String stsToken = token.nextToken(); - String msgToken = token.nextToken(); - - int sts = -1; - try - { - sts = Integer.parseInt( stsToken); - } - catch ( NumberFormatException ex) - { - response.setStatus( StsError, "Bad response from script"); - } - - // Set the response - - response.setStatus( sts, msgToken != null ? msgToken : ""); - } - } - } - catch (ScriptException ex) - { - // Set the error response for the client - - response.setStatus( StsError, ex.getMessage()); - } - } - else - { - // Return an error response, script service not available - - response.setStatus( StsError, "Script service not available"); - } - - // Return the response - - return response; - } - - /** - * Get the script service - * - * @return ScriptService - */ - protected final ScriptService getScriptService() - { - // Check if the script service has been initialized - - if ( m_scriptService == null) - { - // Get the script service - - m_scriptService = getServiceRegistry().getScriptService(); - } - - // Return the script service - - return m_scriptService; + public DesktopResponse call() throws Exception + { + DesktopResponse response = new DesktopResponse(StsSuccess); + + // Run the script + + Object result = null; + + try + { + // Run the script + + result = scriptService.executeScriptString(getScript(), model); + + // Check the result + + if (result != null) + { + // Check for a full response object + + if (result instanceof DesktopResponse) + { + response = (DesktopResponse) result; + } + + // Status code only response + + else if (result instanceof Double) + { + Double jsSts = (Double) result; + response.setStatus(jsSts.intValue(), ""); + } + + // Encoded response in the format ',' + + else if (result instanceof String) + { + String responseMsg = (String) result; + + // Parse the status message + + StringTokenizer token = new StringTokenizer(responseMsg, ","); + String stsToken = token.nextToken(); + String msgToken = token.nextToken(); + + int sts = -1; + try + { + sts = Integer.parseInt(stsToken); + } + catch (NumberFormatException ex) + { + response.setStatus(StsError, "Bad response from script"); + } + + // Set the response + + response.setStatus(sts, msgToken != null ? msgToken : ""); + } + } + } + catch (ScriptException ex) + { + if (RetryingTransactionHelper.extractRetryCause(ex) != null) + { + throw ex; + } + + // Set the error response for the client + response.setStatus(StsError, ex.getMessage()); + } + + // Return the response + + return response; + } + }); + } + else + { + // Return an error response, script service not available + + return new DesktopResponse(StsError, "Script service not available"); + } + } /** diff --git a/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java b/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java index bb50096024..50452a56e9 100644 --- a/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java +++ b/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java @@ -288,12 +288,12 @@ public class AVMLockingAwareService implements AVMService, ApplicationContextAwa } /* (non-Javadoc) - * @see org.alfresco.service.cmr.avm.AVMService#getContentWriter(java.lang.String) + * @see org.alfresco.service.cmr.avm.AVMService#getContentWriter(java.lang.String, boolean) */ - public ContentWriter getContentWriter(String path) + public ContentWriter getContentWriter(String path, boolean update) { grabLock(path); - return fService.getContentWriter(path); + return fService.getContentWriter(path, update); } /* (non-Javadoc) diff --git a/source/java/org/alfresco/repo/avm/AVMRepository.java b/source/java/org/alfresco/repo/avm/AVMRepository.java index 08b63d58a1..ec4b27d570 100644 --- a/source/java/org/alfresco/repo/avm/AVMRepository.java +++ b/source/java/org/alfresco/repo/avm/AVMRepository.java @@ -660,9 +660,12 @@ public class AVMRepository * * @param path * The path to the file. + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. * @return A ContentWriter. */ - public ContentWriter createContentWriter(String path) + public ContentWriter createContentWriter(String path, boolean update) { fLookupCount.set(1); try @@ -674,7 +677,7 @@ public class AVMRepository throw new AVMNotFoundException("Store not found: " + pathParts[0]); } fLookupCache.onWrite(pathParts[0]); - ContentWriter writer = store.createContentWriter(pathParts[1]); + ContentWriter writer = store.createContentWriter(pathParts[1], update); return writer; } finally diff --git a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java index 7dd0a3878c..566cd1046f 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java @@ -147,15 +147,18 @@ public class AVMServiceImpl implements AVMService /** * Get a ContentWriter to a file node. * @param path The path to the file. + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. * @return A ContentWriter. */ - public ContentWriter getContentWriter(String path) + public ContentWriter getContentWriter(String path, boolean update) { if (path == null) { throw new AVMBadArgumentException("Null path."); } - return fAVMRepository.createContentWriter(path); + return fAVMRepository.createContentWriter(path, update); } /** diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTest.java b/source/java/org/alfresco/repo/avm/AVMServiceTest.java index 5a49ecba5d..a6f333e830 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTest.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTest.java @@ -1691,37 +1691,37 @@ public class AVMServiceTest extends AVMServiceTestBase { setupBasicTree(); - ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo"); + ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo V1"); fService.createSnapshot("main", "v1", null); - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo V2"); fService.createSnapshot("main", "v2", null); - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo V3"); fService.createSnapshot("main", "v3", null); - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo V4"); fService.createSnapshot("main", "v4", null); - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo V5"); fService.createSnapshot("main", "v5", null); - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo HEAD"); @@ -3785,7 +3785,7 @@ public class AVMServiceTest extends AVMServiceTestBase assertEquals(0, results.length()); results.close(); - ContentWriter writer = fService.getContentWriter("main:/testdir/testfile"); + ContentWriter writer = fService.getContentWriter("main:/testdir/testfile", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); PrintStream out = new PrintStream(writer.getContentOutputStream()); @@ -3794,7 +3794,7 @@ public class AVMServiceTest extends AVMServiceTestBase out = new PrintStream(fService.getFileOutputStream("main:/testfile2")); - writer = fService.getContentWriter("main:/testfile2"); + writer = fService.getContentWriter("main:/testfile2", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); out = new PrintStream(writer.getContentOutputStream()); diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java b/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java index 6f54b26d95..8810ef9494 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java @@ -268,13 +268,13 @@ public class AVMServiceTestBase extends TestCase fService.createDirectory("main:/d/e", "f"); fService.createFile("main:/a/b/c", "foo").close(); - ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo"); + ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo"); fService.createFile("main:/a/b/c", "bar").close(); - writer = fService.getContentWriter("main:/a/b/c/bar"); + writer = fService.getContentWriter("main:/a/b/c/bar", true); /* // Force a conversion writer.setEncoding("UTF-16"); diff --git a/source/java/org/alfresco/repo/avm/AVMStore.java b/source/java/org/alfresco/repo/avm/AVMStore.java index 82e00c1095..abe1389399 100644 --- a/source/java/org/alfresco/repo/avm/AVMStore.java +++ b/source/java/org/alfresco/repo/avm/AVMStore.java @@ -183,9 +183,12 @@ public interface AVMStore /** * Get a ContentWriter to a file. * @param path The path to the file. + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. * @return A ContentWriter. */ - public ContentWriter createContentWriter(String path); + public ContentWriter createContentWriter(String path, boolean update); /** * Remove a node and all of its contents. diff --git a/source/java/org/alfresco/repo/avm/AVMStoreImpl.java b/source/java/org/alfresco/repo/avm/AVMStoreImpl.java index 1171e8c7fb..b04e68a496 100644 --- a/source/java/org/alfresco/repo/avm/AVMStoreImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMStoreImpl.java @@ -585,7 +585,7 @@ public class AVMStoreImpl implements AVMStore file.setProperties(props); } - return createContentWriter(AVMNodeConverter.ExtendAVMPath(path, name)); + return createContentWriter(AVMNodeConverter.ExtendAVMPath(path, name), true); } /** @@ -710,14 +710,17 @@ public class AVMStoreImpl implements AVMStore * Get a ContentWriter to a file. * @param path The path to the file. * @return A ContentWriter. + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. */ - public ContentWriter createContentWriter(String path) + public ContentWriter createContentWriter(String path, boolean update) { try { NodeRef nodeRef = AVMNodeConverter.ToNodeRef(-1, getName() + ":" + path); ContentWriter writer = - RawServices.Instance().getContentService().getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + RawServices.Instance().getContentService().getWriter(nodeRef, ContentModel.PROP_CONTENT, update); return writer; } catch (InvalidNodeRefException inre) @@ -826,7 +829,7 @@ public class AVMStoreImpl implements AVMStore */ public OutputStream getOutputStream(String path) { - ContentWriter writer = createContentWriter(path); + ContentWriter writer = createContentWriter(path, true); return writer.getContentOutputStream(); } diff --git a/source/java/org/alfresco/repo/avm/MultiTAVMService.java b/source/java/org/alfresco/repo/avm/MultiTAVMService.java index d5c69f501d..51c5a53f1d 100644 --- a/source/java/org/alfresco/repo/avm/MultiTAVMService.java +++ b/source/java/org/alfresco/repo/avm/MultiTAVMService.java @@ -265,11 +265,11 @@ public class MultiTAVMService implements AVMService } /* (non-Javadoc) - * @see org.alfresco.service.cmr.avm.AVMService#getContentWriter(java.lang.String) + * @see org.alfresco.service.cmr.avm.AVMService#getContentWriter(java.lang.String, boolean) */ - public ContentWriter getContentWriter(String path) + public ContentWriter getContentWriter(String path, boolean update) { - return fService.getContentWriter(getTenantPath(path)); + return fService.getContentWriter(getTenantPath(path), update); } /* (non-Javadoc) diff --git a/source/java/org/alfresco/repo/avm/locking/AVMLockingServiceTest.java b/source/java/org/alfresco/repo/avm/locking/AVMLockingServiceTest.java index 1aadd9adda..45b500ea94 100644 --- a/source/java/org/alfresco/repo/avm/locking/AVMLockingServiceTest.java +++ b/source/java/org/alfresco/repo/avm/locking/AVMLockingServiceTest.java @@ -510,13 +510,13 @@ public class AVMLockingServiceTest extends TestCase fService.createDirectory("main:/d/e", "f"); fService.createFile("main:/a/b/c", "foo").close(); - ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo"); + ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo"); fService.createFile("main:/a/b/c", "bar").close(); - writer = fService.getContentWriter("main:/a/b/c/bar"); + writer = fService.getContentWriter("main:/a/b/c/bar", true); // Force a conversion writer.setEncoding("UTF-16"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); diff --git a/source/java/org/alfresco/repo/copy/CrossRepositoryCopyServiceImpl.java b/source/java/org/alfresco/repo/copy/CrossRepositoryCopyServiceImpl.java index 168c0d00dd..077d5bef52 100644 --- a/source/java/org/alfresco/repo/copy/CrossRepositoryCopyServiceImpl.java +++ b/source/java/org/alfresco/repo/copy/CrossRepositoryCopyServiceImpl.java @@ -312,7 +312,7 @@ public class CrossRepositoryCopyServiceImpl implements CrossRepositoryCopyServic throw new AlfrescoRuntimeException("I/O Error.", e); } } - ContentWriter writer = fAVMService.getContentWriter(childPath); + ContentWriter writer = fAVMService.getContentWriter(childPath, true); writer.setEncoding(reader.getEncoding()); writer.setMimetype(reader.getMimetype()); OutputStream out = writer.getContentOutputStream(); diff --git a/source/java/org/alfresco/repo/deploy/ASRDeploymentTest.java b/source/java/org/alfresco/repo/deploy/ASRDeploymentTest.java index f275a98116..1b07437400 100644 --- a/source/java/org/alfresco/repo/deploy/ASRDeploymentTest.java +++ b/source/java/org/alfresco/repo/deploy/ASRDeploymentTest.java @@ -89,7 +89,7 @@ public class ASRDeploymentTest extends AVMServiceTestBase String rootText = "Angel is an American television series, a spin-off of the television series Buffy the Vampire Slayer. The series was created by Buffy's creator, Joss Whedon, in collaboration with David Greenwalt, and first aired on October 5, 1999. Like Buffy, it was produced by Whedon's production company, Mutant Enemy."; fService.createFile("main:/", "rootFile").close(); - ContentWriter writer = fService.getContentWriter("main:/rootFile"); + ContentWriter writer = fService.getContentWriter("main:/rootFile", true); // Force a conversion writer.setEncoding("UTF-16"); @@ -104,13 +104,13 @@ public class ASRDeploymentTest extends AVMServiceTestBase fService.createFile("main:/a/b/c", "foo").close(); String fooText="I am main:/a/b/c/foo"; - writer = fService.getContentWriter("main:/a/b/c/foo"); + writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo"); fService.createFile("main:/a/b/c", "bar").close(); - writer = fService.getContentWriter("main:/a/b/c/bar"); + writer = fService.getContentWriter("main:/a/b/c/bar", true); // Force a conversion writer.setEncoding("UTF-16"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); @@ -118,7 +118,7 @@ public class ASRDeploymentTest extends AVMServiceTestBase String buffyText = "This is test data: Buffy the Vampire Slayer is an Emmy Award-winning and Golden Globe-nominated American cult television series that aired from March 10, 1997 until May 20, 2003. The series was created in 1997 by writer-director Joss Whedon under his production tag, Mutant Enemy Productions with later co-executive producers being Jane Espenson, David Fury, and Marti Noxon. The series narrative follows Buffy Summers (played by Sarah Michelle Gellar), the latest in a line of young women chosen by fate to battle against vampires, demons, and the forces of darkness as the Slayer. Like previous Slayers, Buffy is aided by a Watcher, who guides and trains her. Unlike her predecessors, Buffy surrounds herself with a circle of loyal friends who become known as the Scooby Gang."; fService.createFile("main:/a/b", "buffy").close(); - writer = fService.getContentWriter("main:/a/b/buffy"); + writer = fService.getContentWriter("main:/a/b/buffy", true); // Force a conversion writer.setEncoding("UTF-16"); diff --git a/source/java/org/alfresco/repo/deploy/FSDeploymentTest.java b/source/java/org/alfresco/repo/deploy/FSDeploymentTest.java index 80a0e1bf91..4a8e4f63a3 100644 --- a/source/java/org/alfresco/repo/deploy/FSDeploymentTest.java +++ b/source/java/org/alfresco/repo/deploy/FSDeploymentTest.java @@ -137,13 +137,13 @@ public class FSDeploymentTest extends AVMServiceTestBase fService.createFile("main:/a/b/c", "foo").close(); String fooText="I am main:/a/b/c/foo"; - ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo"); + ContentWriter writer = fService.getContentWriter("main:/a/b/c/foo", true); writer.setEncoding("UTF-8"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); writer.putContent("I am main:/a/b/c/foo"); fService.createFile("main:/a/b/c", "bar").close(); - writer = fService.getContentWriter("main:/a/b/c/bar"); + writer = fService.getContentWriter("main:/a/b/c/bar", true); // Force a conversion writer.setEncoding("UTF-16"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); @@ -151,7 +151,7 @@ public class FSDeploymentTest extends AVMServiceTestBase String buffyText = "This is test data: Buffy the Vampire Slayer is an Emmy Award-winning and Golden Globe-nominated American cult television series that aired from March 10, 1997 until May 20, 2003. The series was created in 1997 by writer-director Joss Whedon under his production tag, Mutant Enemy Productions with later co-executive producers being Jane Espenson, David Fury, and Marti Noxon. The series narrative follows Buffy Summers (played by Sarah Michelle Gellar), the latest in a line of young women chosen by fate to battle against vampires, demons, and the forces of darkness as the Slayer. Like previous Slayers, Buffy is aided by a Watcher, who guides and trains her. Unlike her predecessors, Buffy surrounds herself with a circle of loyal friends who become known as the Scooby Gang."; fService.createFile("main:/a/b", "buffy").close(); - writer = fService.getContentWriter("main:/a/b/buffy"); + writer = fService.getContentWriter("main:/a/b/buffy", true); // Force a conversion writer.setEncoding("UTF-16"); writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); diff --git a/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java b/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java index b7d95287ed..c913109ab2 100644 --- a/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java @@ -300,7 +300,7 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO if (contentUrl != null) { // We must find or create the ContentUrlEntity - contentUrlId = getOrCreateContentUrlEntity(contentUrl, size).getId(); + contentUrlId = getOrCreateContentUrlEntity(contentUrl, size, contentData.isReferenced()).getId(); } // Resolve the mimetype Long mimetypeId = null; @@ -347,7 +347,7 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO } if (newContentUrl != null) { - Long contentUrlId = getOrCreateContentUrlEntity(newContentUrl, contentData.getSize()).getId(); + Long contentUrlId = getOrCreateContentUrlEntity(newContentUrl, contentData.getSize(), contentData.isReferenced()).getId(); contentDataEntity.setContentUrlId(contentUrlId); contentDataEntity.setContentUrl(newContentUrl); } @@ -389,8 +389,9 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO /** * Method to create (or get an existing) content URL. The URL will be unorphaned * whether it has been created or is being re-used. + * @param isReferenced if true we won't worry about eagerly deleting the content on transaction rollback */ - private ContentUrlEntity getOrCreateContentUrlEntity(String contentUrl, long size) + private ContentUrlEntity getOrCreateContentUrlEntity(String contentUrl, long size, boolean isReferenced) { // Create the content URL entity ContentUrlEntity contentUrlEntity = getContentUrlEntity(contentUrl); @@ -416,7 +417,7 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO else { // Create it - contentUrlEntity = createContentUrlEntity(contentUrl, size); + contentUrlEntity = createContentUrlEntity(contentUrl, size, isReferenced); } // Done return contentUrlEntity; @@ -424,8 +425,9 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO /** * @param contentUrl the content URL to create or search for + * @param isReferenced if true we won't worry about eagerly deleting the content on transaction rollback */ - protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size); + protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size, boolean isReferenced); /** * @param id the ID of the content url entity diff --git a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java index d9ba6273e3..8686a772e2 100644 --- a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java @@ -77,7 +77,7 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl } @Override - protected ContentUrlEntity createContentUrlEntity(String contentUrl, long size) + protected ContentUrlEntity createContentUrlEntity(String contentUrl, long size, boolean isReferenced) { ContentUrlEntity contentUrlEntity = new ContentUrlEntity(); contentUrlEntity.setContentUrl(contentUrl); @@ -85,8 +85,14 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl contentUrlEntity.setOrphanTime(null); /* Long id = (Long) */ template.insert(INSERT_CONTENT_URL, contentUrlEntity); /*contentUrlEntity.setId(id);*/ - // Register the url as new - registerNewContentUrl(contentUrl); + + // Don't register this URL for tidy up if it is still referenced by code outside of this transaction + if (!isReferenced) + { + // Register the url as new + registerNewContentUrl(contentUrl); + } + // Done return contentUrlEntity; } diff --git a/source/java/org/alfresco/service/cmr/avm/AVMService.java b/source/java/org/alfresco/service/cmr/avm/AVMService.java index 572056c87f..eaaa711b25 100644 --- a/source/java/org/alfresco/service/cmr/avm/AVMService.java +++ b/source/java/org/alfresco/service/cmr/avm/AVMService.java @@ -1320,11 +1320,14 @@ public interface AVMService * Low-level internal function:   Fetch a ContentWriter to a file node. * * @param path The path to the file. + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. * @return A ContentWriter. * @throws AVMNotFoundException * @throws AVMWrongTypeException */ - public ContentWriter getContentWriter(String path); + public ContentWriter getContentWriter(String path, boolean update); /** * Low-level internal function:   Get the ContentData for diff --git a/source/java/org/alfresco/service/cmr/repository/ContentData.java b/source/java/org/alfresco/service/cmr/repository/ContentData.java index 19ee93307e..55db31096f 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentData.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentData.java @@ -43,6 +43,7 @@ public class ContentData implements Serializable private final long size; private final String encoding; private final Locale locale; + private int refCount; /** * Construct a content property from a string @@ -222,6 +223,32 @@ public class ContentData implements Serializable EqualsHelper.nullSafeEquals(this.locale, that.locale)); } + /** + * Use to 'hold on' to content data outside of a transaction (e.g. on transaction retry). + */ + public void reference() + { + this.refCount++; + } + + /** + * Use to release a content data previously held on to by {@link #reference()}. + */ + public void deReference() + { + this.refCount--; + } + + /** + * Determines whether this content data should be deleted on rollback. + * + * @return false if this content data should be deleted on rollback. + */ + public boolean isReferenced() + { + return this.refCount > 0; + } + /** * @return Returns a string of form: contentUrl=xxx;mimetype=xxx;size=xxx;encoding=xxx */ diff --git a/source/java/org/alfresco/wcm/asset/AssetServiceImpl.java b/source/java/org/alfresco/wcm/asset/AssetServiceImpl.java index a965f800b7..45c791e391 100644 --- a/source/java/org/alfresco/wcm/asset/AssetServiceImpl.java +++ b/source/java/org/alfresco/wcm/asset/AssetServiceImpl.java @@ -198,7 +198,7 @@ public class AssetServiceImpl implements AssetService String avmPath = avmParentPath + PATH_SEPARATOR + name; - return avmService.getContentWriter(avmPath); + return avmService.getContentWriter(avmPath, true); } /* (non-Javadoc) @@ -221,7 +221,7 @@ public class AssetServiceImpl implements AssetService setProperties(avmPath, properties); } - return avmService.getContentWriter(avmPath); + return avmService.getContentWriter(avmPath, true); } private void createFileAVM(String avmParentPath, String name) @@ -264,7 +264,7 @@ public class AssetServiceImpl implements AssetService if (! isWebProjectStagingSandbox(asset.getSandboxId())) { - return avmService.getContentWriter(asset.getAvmPath()); + return avmService.getContentWriter(asset.getAvmPath(), true); } else {