.*D_[0-9]*.TMP$
diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffle.java
new file mode 100755
index 0000000000..1191fa101a
--- /dev/null
+++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffle.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 .
+ */
+package org.alfresco.filesys.repo.rules;
+
+import org.alfresco.filesys.repo.rules.ScenarioInstance.Ranking;
+import org.alfresco.filesys.repo.rules.operations.RenameFileOperation;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is an instance of a "multiple rename shuffle" triggered by rename of a file to a special pattern
+ * file matching a specified pattern.
+ *
+ * a) Original file renamed to the temporary
+ * b) Any operations with temporary (optional):
+ * b1) Temporary file renamed to other temporary
+ * b2) Temporary file deleted
+ * c) Temporary file (maybe not the same, as it was at step 1) renamed to the original file
+ *
+ * If this filter is active then this is what happens.
+ * a) Temporary file created. Content copied from original file to temporary file.
+ * b) Original file deleted (temporary).
+ * c) any operations with temporary file
+ * d) Original file restored. Content copied from temporary file to original file.
+ *
+ */
+public class ScenarioMultipleRenameShuffle implements Scenario
+{
+ private static Log logger = LogFactory.getLog(ScenarioMultipleRenameShuffle.class);
+
+ /**
+ * The regex pattern of a create that will trigger a new instance of
+ * the scenario.
+ */
+ private Pattern pattern;
+ private String strPattern;
+
+
+ private long timeout = 30000;
+
+ private Ranking ranking = Ranking.HIGH;
+
+ @Override
+ public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation)
+ {
+ /**
+ * This scenario is triggered by a rename of a file matching
+ * the pattern
+ */
+ if(operation instanceof RenameFileOperation)
+ {
+ RenameFileOperation r = (RenameFileOperation)operation;
+
+ Matcher m = pattern.matcher(r.getTo());
+ if(m.matches())
+ {
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("New Scenario Multiple Rename Shuffle strPattern: " + strPattern + " matches" + r.getTo());
+ }
+ ScenarioMultipleRenameShuffleInstance instance = new ScenarioMultipleRenameShuffleInstance();
+ instance.setTimeout(timeout);
+ instance.setRanking(ranking);
+ return instance;
+ }
+ }
+
+ // No not interested.
+ return null;
+
+ }
+
+ public void setPattern(String pattern)
+ {
+ this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
+ this.strPattern = pattern;
+ }
+
+ public String getPattern()
+ {
+ return this.strPattern;
+ }
+
+ public void setTimeout(long timeout)
+ {
+ this.timeout = timeout;
+ }
+
+ public long getTimeout()
+ {
+ return timeout;
+ }
+
+ public void setRanking(Ranking ranking)
+ {
+ this.ranking = ranking;
+ }
+
+ public Ranking getRanking()
+ {
+ return ranking;
+ }
+}
diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffleInstance.java
new file mode 100755
index 0000000000..c8ef871653
--- /dev/null
+++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioMultipleRenameShuffleInstance.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 .
+ */
+package org.alfresco.filesys.repo.rules;
+
+import org.alfresco.filesys.repo.ResultCallback;
+import org.alfresco.filesys.repo.rules.commands.*;
+import org.alfresco.filesys.repo.rules.operations.RenameFileOperation;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+/**
+ * This is an instance of a "multiple rename shuffle" triggered by rename of a file to a special pattern
+ * file matching a specified pattern.
+ *
+ * a) Original file renamed to the temporary
+ * b) Any operations with temporary (optional):
+ * b1) Temporary file renamed to other temporary
+ * b2) Temporary file deleted
+ * c) Temporary file (maybe not the same, as it was at step 1) renamed to the original file
+ *
+ * If this filter is active then this is what happens.
+ * a) Temporary file created. Content copied from original file to temporary file.
+ * b) Original file deleted (temporary).
+ * c) any operations with temporary file
+ * d) Original file restored. Content copied from temporary file to original file.
+ *
+ */
+public class ScenarioMultipleRenameShuffleInstance implements ScenarioInstance
+{
+ private static Log logger = LogFactory.getLog(ScenarioMultipleRenameShuffleInstance.class);
+ private NodeRef originalNodeRef;
+
+ enum InternalState
+ {
+ NONE,
+ INITIALISED
+ }
+
+ InternalState internalState = InternalState.NONE;
+ private Date startTime = new Date();
+ private String originalName;
+ private Ranking ranking;
+
+ /**
+ * Timeout in ms. Default 30 seconds.
+ */
+ private long timeout = 30000;
+ private boolean isComplete;
+
+ /**
+ * Evaluate the next operation
+ * @param operation
+ */
+ public Command evaluate(Operation operation)
+ {
+
+ /**
+ * Anti-pattern : timeout
+ */
+ Date now = new Date();
+ if(now.getTime() > startTime.getTime() + getTimeout())
+ {
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Instance timed out");
+
+ }
+ isComplete = true;
+ return null;
+ }
+
+
+ switch (internalState)
+ {
+ case NONE:
+ /**
+ * Looking for first rename O(original) to T(temporary)
+ */
+ if(operation instanceof RenameFileOperation)
+ {
+ RenameFileOperation r = (RenameFileOperation)operation;
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Got first rename - tracking rename: " + operation);
+ }
+ originalName = r.getFromPath();
+ ArrayList commands = new ArrayList();
+ ArrayList postCommitCommands = new ArrayList();
+ ArrayList postErrorCommands = new ArrayList();
+
+ CreateFileCommand c1 = new CreateFileCommand(r.getTo(), r.getRootNodeRef(), r.getToPath(), 0, true);
+ CopyContentCommand copyContent = new CopyContentCommand(r.getFrom(), r.getTo(), r.getRootNodeRef(), r.getFromPath(), r.getToPath());
+ DeleteFileCommand d1 = new DeleteFileCommand(r.getFrom(), r.getRootNodeRef(), r.getFromPath());
+ postCommitCommands.add(deleteFileCallbackCommand());
+
+ commands.add(c1);
+ commands.add(copyContent);
+ commands.add(d1);
+
+ internalState = InternalState.INITIALISED;
+
+ return new CompoundCommand(commands, postCommitCommands, postErrorCommands);
+ }
+ else
+ {
+ // anything else bomb out
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("State error, expected a RENAME");
+ }
+ isComplete = true;
+ }
+
+ case INITIALISED:
+
+ /**
+ * Looking for last rename T(temporary) to O(original)
+ */
+ if (operation instanceof RenameFileOperation)
+ {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Tracking rename: " + operation);
+ }
+ RenameFileOperation r = (RenameFileOperation) operation;
+
+ if(r.getToPath().equalsIgnoreCase(originalName))
+ {
+ ArrayList commands = new ArrayList();
+ RestoreFileCommand r1 = new RestoreFileCommand(r.getTo(), r.getRootNodeRef(), r.getToPath(), 0, originalNodeRef);
+ CopyContentCommand copyContent = new CopyContentCommand(r.getFrom(), r1.getName(), r.getRootNodeRef(), r.getFromPath(), r1.getPath());
+ DeleteFileCommand d1 = new DeleteFileCommand(r.getFrom(), r.getRootNodeRef(), r.getFromPath());
+
+ commands.add(r1);
+ commands.add(copyContent);
+ commands.add(d1);
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Scenario complete");
+ }
+ isComplete = true;
+ return new CompoundCommand(commands);
+ }
+ }
+
+ break;
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isComplete()
+ {
+ return isComplete;
+ }
+
+ @Override
+ public Ranking getRanking()
+ {
+ return ranking;
+ }
+
+ public void setRanking(Ranking ranking)
+ {
+ this.ranking = ranking;
+ }
+
+ public String toString()
+ {
+ return "ScenarioMultipleRenameShuffleInstance: createName:" + originalName;
+ }
+
+ public void setTimeout(long timeout)
+ {
+ this.timeout = timeout;
+ }
+
+ public long getTimeout()
+ {
+ return timeout;
+ }
+
+
+ /**
+ * Called for delete file.
+ */
+ private ResultCallback deleteFileCallbackCommand()
+ {
+ return new ResultCallback()
+ {
+ @Override
+ public void execute(Object result)
+ {
+ if(result instanceof NodeRef)
+ {
+ logger.debug("got node ref of deleted node");
+ originalNodeRef = (NodeRef)result;
+ }
+ }
+
+ @Override
+ public AlfrescoTransactionSupport.TxnReadState getTransactionRequired()
+ {
+ return AlfrescoTransactionSupport.TxnReadState.TXN_NONE;
+ }
+ };
+ }
+}
diff --git a/source/test-java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/test-java/org/alfresco/filesys/repo/ContentDiskDriverTest.java
index 2f96940f26..3a41c2f45e 100644
--- a/source/test-java/org/alfresco/filesys/repo/ContentDiskDriverTest.java
+++ b/source/test-java/org/alfresco/filesys/repo/ContentDiskDriverTest.java
@@ -3226,6 +3226,250 @@ public class ContentDiskDriverTest extends TestCase
}
+ /**
+ * Excel 2013 With Versionable file (MNT-13078)
+ *
+ * CreateFile Cherries.xlsx
+ * CreateFile ~herries.tmp
+ * CreateFile Cherries.xlsx~RF172f241.TMP
+ * DeleteFile Cherries.xlsx~RF172f241.TMP
+ * RenameFile oldName: Cherries.xls,
+ * newName: Cherries.xlsx~RF172f241.TMP
+ * Delete On Close for Cherries.xlsx~RF172f241.TMP
+ * RenameFile oldName: ~herries.tmp,
+ * newName: Cherries.xlsx
+ *
+ */
+ public void testExcel2013SaveShuffle() throws Exception
+ {
+ logger.debug("testScenarioExcel2013SaveShuffle");
+ final String FILE_NAME = "Cherries.xlsx";
+ final String FILE_ORIGINAL_TITLE = "Original";
+ final String FILE_UNUSED_TEMP = "Cherries.xlsx~RF172f241.TMP";
+ final String FILE_USED_TEMP = "~herries.tmp";
+ final String TEST_DIR = TEST_ROOT_DOS_PATH + "\\testScenarioMSExcel2013SaveShuffle";
+
+ class TestContext
+ {
+ NetworkFile firstFileHandle;
+ NodeRef testNodeRef; // node ref of test.doc
+ };
+
+ final TestContext testContext = new TestContext();
+
+ ServerConfiguration scfg = new ServerConfiguration("testServer");
+ TestServer testServer = new TestServer("testServer", scfg);
+ final SrvSession testSession = new TestSrvSession(666, testServer, "test", "remoteName");
+ DiskSharedDevice share = getDiskSharedDevice();
+ final TreeConnection testConnection = testServer.getTreeConnection(share);
+ final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
+
+ /**
+ * Clean up just in case garbage is left from a previous run
+ */
+ RetryingTransactionCallback deleteGarbageFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+ driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME);
+ return null;
+ }
+ };
+
+ /**
+ * Delete a file in the test directory
+ */
+
+ try
+ {
+ tran.doInTransaction(deleteGarbageFileCB);
+ }
+ catch (Exception e)
+ {
+ // expect to go here
+ }
+
+ /**
+ * Create a file in the test directory
+ */
+ RetryingTransactionCallback createOriginalFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable {
+
+ /**
+ * Create the test directory we are going to use
+ */
+ FileOpenParams createRootDirParams = new FileOpenParams(TEST_ROOT_DOS_PATH, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0);
+ FileOpenParams createDirParams = new FileOpenParams(TEST_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0);
+ driver.createDirectory(testSession, testConnection, createRootDirParams);
+ driver.createDirectory(testSession, testConnection, createDirParams);
+
+ /**
+ * Create the file we are going to use
+ */
+ FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0);
+ testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams);
+ assertNotNull(testContext.firstFileHandle);
+ String testContent = "MS Word 2013 shuffle test. This is first file content";
+ byte[] testContentBytes = testContent.getBytes();
+ testContext.firstFileHandle.writeFile(testContentBytes, testContentBytes.length, 0, 0);
+ testContext.firstFileHandle.close();
+
+ // now load up the node with lots of other stuff that we will test to see if it gets preserved during the
+ // shuffle.
+ testContext.testNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME);
+
+ nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, null);
+ // need to remove. Otherwise file will be deleted (not placed into archive spaces store)
+ nodeService.removeAspect(testContext.testNodeRef, ContentModel.ASPECT_NO_CONTENT);
+ // test non CM namespace property
+ nodeService.setProperty(testContext.testNodeRef, TransferModel.PROP_ENABLED, true);
+
+ nodeService.setProperty(testContext.testNodeRef, ContentModel.PROP_TITLE, FILE_ORIGINAL_TITLE);
+
+ return null;
+ }
+ };
+ tran.doInTransaction(createOriginalFileCB, false, true);
+
+ /**
+ * Create a file in the test directory
+ */
+ RetryingTransactionCallback createUsedTempFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+
+ /**
+ * Create the file we are going to use
+ */
+ FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_USED_TEMP, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0);
+ NetworkFile fileHandle = driver.createFile(testSession, testConnection, createFileParams);
+ assertNotNull(fileHandle);
+ String testContent = "MS Word 2013 shuffle test. This is used file content";
+ byte[] testContentBytes = testContent.getBytes();
+ fileHandle.writeFile(testContentBytes, testContentBytes.length, 0, 0);
+ fileHandle.close();
+
+ NodeRef usedRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_USED_TEMP);
+
+ nodeService.addAspect(usedRef, ContentModel.ASPECT_VERSIONABLE, null);
+ nodeService.removeAspect(usedRef, ContentModel.ASPECT_NO_CONTENT);
+ nodeService.setProperty(usedRef, ContentModel.PROP_TITLE, "Used");
+
+ return null;
+ }
+ };
+ tran.doInTransaction(createUsedTempFileCB, false, true);
+
+ /**
+ * Create a file in the test directory
+ */
+ RetryingTransactionCallback createUnusedTempFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+
+ /**
+ * Create the file we are going to use
+ */
+ FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_UNUSED_TEMP, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0);
+ NetworkFile unusedFile = driver.createFile(testSession, testConnection, createFileParams);
+ assertNotNull(unusedFile);
+
+ NodeRef unusedNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_UNUSED_TEMP);
+ nodeService.addAspect(unusedNodeRef, ContentModel.ASPECT_VERSIONABLE, null);
+ nodeService.setProperty(unusedNodeRef, TransferModel.PROP_ENABLED, true);
+ nodeService.setProperty(unusedNodeRef, ContentModel.PROP_TITLE, "Unused");
+
+ return null;
+ }
+ };
+ tran.doInTransaction(createUnusedTempFileCB, false, true);
+
+ /**
+ * Delete unused temporary file
+ */
+ RetryingTransactionCallback deleteUnusedFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+ try
+ {
+ driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_UNUSED_TEMP);
+ }
+ catch (IOException e)
+ {
+ // expect to go here since previous step renamed the file.
+ }
+
+ return null;
+ }
+ };
+ tran.doInTransaction(deleteUnusedFileCB, false, true);
+
+ /**
+ * Rename the original file to unused file
+ */
+ RetryingTransactionCallback renameToUnusedFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+ driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME, TEST_DIR + "\\" + FILE_UNUSED_TEMP);
+ return null;
+ }
+ };
+ tran.doInTransaction(renameToUnusedFileCB, false, true);
+
+ /**
+ * Delete unused temporary file
+ */
+ tran.doInTransaction(deleteUnusedFileCB, false, true);
+
+ /**
+ * Rename the used temporary file to original file
+ */
+ RetryingTransactionCallback renameToUsedFileCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+ driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_USED_TEMP, TEST_DIR + "\\" + FILE_NAME);
+ return null;
+ }
+ };
+ tran.doInTransaction(renameToUsedFileCB, false, true);
+
+
+
+ RetryingTransactionCallback validateCB = new RetryingTransactionCallback() {
+
+ @Override
+ public Void execute() throws Throwable
+ {
+ NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME);
+
+ Map props = nodeService.getProperties(shuffledNodeRef);
+
+ // Check trx:enabled has been shuffled.
+ assertTrue("node does not contain shuffled ENABLED property", props.containsKey(TransferModel.PROP_ENABLED));
+ assertTrue("node doesn't contain property 'TITLE'", props.containsKey(ContentModel.PROP_TITLE));
+ assertEquals("propety 'TITLE' isn't correct", FILE_ORIGINAL_TITLE, props.get(ContentModel.PROP_TITLE));
+ assertEquals("name wrong", FILE_NAME, nodeService.getProperty(shuffledNodeRef, ContentModel.PROP_NAME));
+
+ return null;
+ }
+ };
+
+ tran.doInTransaction(validateCB, true, true);
+ }
+
/**
* Excel 2003 CSV file with Versionable file
*