diff --git a/config/alfresco/extension/mt/mt-admin-context.xml b/config/alfresco/extension/mt/mt-admin-context.xml index 0a138f0c35..b066fb04f2 100644 --- a/config/alfresco/extension/mt/mt-admin-context.xml +++ b/config/alfresco/extension/mt/mt-admin-context.xml @@ -14,25 +14,26 @@ - - - - - - - + + + + + + + ${alfresco_user_store.adminusername} - - - + + + alfresco.messages.tenant-interpreter-help - + + diff --git a/config/alfresco/extension/mt/mt-contentstore-context.xml b/config/alfresco/extension/mt/mt-contentstore-context.xml index 6fc64c92ca..8366256327 100644 --- a/config/alfresco/extension/mt/mt-contentstore-context.xml +++ b/config/alfresco/extension/mt/mt-contentstore-context.xml @@ -3,60 +3,12 @@ - - - + + + - - - - - - - - org.alfresco.cache.tenantFileStoresCache - - - + + - - - - - - - ${dir.contentstore} - - - - - - - - - - - - - - ${system.content.eagerOrphanCleanup} - - - - - - - - - - - - - - - - - - diff --git a/config/alfresco/extension/mt/mt-contentstore-context.xml.sample b/config/alfresco/extension/mt/mt-contentstore-context.xml.sample index 2f444d26e8..8366256327 100644 --- a/config/alfresco/extension/mt/mt-contentstore-context.xml.sample +++ b/config/alfresco/extension/mt/mt-contentstore-context.xml.sample @@ -7,9 +7,8 @@ - + - diff --git a/config/alfresco/extension/mt/mt-context.xml b/config/alfresco/extension/mt/mt-context.xml index 686026bd68..340ea89fc2 100644 --- a/config/alfresco/extension/mt/mt-context.xml +++ b/config/alfresco/extension/mt/mt-context.xml @@ -2,49 +2,15 @@ - - - - - - - - - - - - - - org.alfresco.cache.tenantsCache - - - - - - - - - - - - - org.alfresco.tenantsTransactionalCache - - - 100 - - - - - + - - + + - - + + diff --git a/config/alfresco/mt/mt-base-context.xml b/config/alfresco/mt/mt-base-context.xml index 34594e226b..f142206b53 100644 --- a/config/alfresco/mt/mt-base-context.xml +++ b/config/alfresco/mt/mt-base-context.xml @@ -2,7 +2,7 @@ - + - ${alfresco_user_store.adminusername} + - + + + + + diff --git a/config/alfresco/site-services-context.xml b/config/alfresco/site-services-context.xml index d5e5e7f2b1..178bdcf009 100644 --- a/config/alfresco/site-services-context.xml +++ b/config/alfresco/site-services-context.xml @@ -153,6 +153,7 @@ + diff --git a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml index 35c240a145..c37b971488 100644 --- a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml +++ b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml @@ -145,8 +145,10 @@ - - ^\._.txt* + + ^.*\.txt$ + + .*(\\\..*\\)+.* 60000 HIGH @@ -176,7 +178,7 @@ - ^[^\._].*[0-9].pptx$ + ^[^\._].*[0-9].ppt[x]*$ 60000 HIGH diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java index cc2913a284..5be2550bf2 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java @@ -4733,18 +4733,16 @@ public class ContentDiskDriverTest extends TestCase * This test tries to simulate the cifs shuffling that is done * from Save from Mac Lion by TextEdit * - * a) Lock file created. (._test.txt) - * b) Temp file created in temporary folder (test.txt) - * c) Target file deleted - * d) Temp file renamed to target file. - * e) Lock file deleted - * + * a) Temp file created in temporary folder (test.txt) + * b) Resource fork file created in temporary folder (._test.txt) + * b) Target file deleted + * c) Temp file moved to target file. */ - public void DISABLED_TestScenarioMacLionTextEdit() throws Exception + public void testScenarioMacLionTextEdit() throws Exception { logger.debug("testScenarioLionTextEdit"); final String FILE_NAME = "test.txt"; - final String LOCK_FILE_NAME = "._test.txt"; + final String FORK_FILE_NAME = "._test.txt"; final String TEMP_FILE_NAME = "test.txt"; final String UPDATED_TEXT = "Mac Lion Text Updated Content"; @@ -4813,23 +4811,10 @@ public class ContentDiskDriverTest extends TestCase driver.writeFile(testSession, testConnection, testContext.tempFileHandle, testContentBytes, 0, testContentBytes.length, 0); driver.closeFile(testSession, testConnection, testContext.tempFileHandle); - return null; - } - }; - tran.doInTransaction(createFileCB, false, true); - - /** - * a) create the lock file - */ - RetryingTransactionCallback createLockFileCB = new RetryingTransactionCallback() { - - @Override - public Void execute() throws Throwable - { /** - * Create the lock file we are going to use + * Create the temp resource fork file we are going to use */ - FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + LOCK_FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + createFileParams = new FileOpenParams(TEST_TEMP_DIR + "\\" + FORK_FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); testContext.lockFileHandle = driver.createFile(testSession, testConnection, createFileParams); assertNotNull(testContext.lockFileHandle); testContext.lockFileHandle.closeFile(); @@ -4840,12 +4825,11 @@ public class ContentDiskDriverTest extends TestCase testContext.testNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, null); - return null; } }; - tran.doInTransaction(createLockFileCB, false, true); - + tran.doInTransaction(createFileCB, false, true); + /** * b) Delete the target file */ @@ -4869,28 +4853,15 @@ public class ContentDiskDriverTest extends TestCase public Void execute() throws Throwable { driver.renameFile(testSession, testConnection, TEST_TEMP_DIR + "\\" + TEMP_FILE_NAME, TEST_DIR + "\\" + FILE_NAME); + driver.renameFile(testSession, testConnection, TEST_TEMP_DIR + "\\" + FORK_FILE_NAME, TEST_DIR + "\\" + FORK_FILE_NAME); return null; } }; tran.doInTransaction(moveTempFileCB, false, true); - /** - * d) Delete Lock File + * Validate results. */ - RetryingTransactionCallback deleteLockFileCB = new RetryingTransactionCallback() { - - @Override - public Void execute() throws Throwable - { - driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + LOCK_FILE_NAME); - - return null; - } - }; - - tran.doInTransaction(deleteLockFileCB, false, true); - RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { @Override @@ -4900,7 +4871,7 @@ public class ContentDiskDriverTest extends TestCase NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); assertEquals("shuffledNode ref is different", shuffledNodeRef, testContext.testNodeRef); - assertTrue("", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + assertTrue("node is not versionable", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); ContentReader reader = contentService.getReader(shuffledNodeRef, ContentModel.PROP_CONTENT); assertNotNull("Reader is null", reader); diff --git a/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java index 3a200a445f..98ef3f7dd7 100644 --- a/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java @@ -70,6 +70,11 @@ public class NonTransactionalRuleContentDiskDriver implements ExtendedDiskInterf */ private class DriverState { + /** + * key, value pair storage for the session + */ + Map sessionState = new ConcurrentHashMap(); + /** * Map of folderName to Evaluator Context. */ @@ -150,16 +155,7 @@ public class NonTransactionalRuleContentDiskDriver implements ExtendedDiskInterf String folder = paths[0]; String file = paths[1]; - EvaluatorContext ctx = driverState.contextMap.get(folder); - if(ctx == null) - { - ctx = ruleEvaluator.createContext(); - driverState.contextMap.put(folder, ctx); - if(logger.isDebugEnabled()) - { - logger.debug("new driver context: " + folder); - } - } + EvaluatorContext ctx = getEvaluatorContext(driverState, folder); Operation o = new CloseFileOperation(file, param, rootNode, param.getFullName(), param.hasDeleteOnClose()); Command c = ruleEvaluator.evaluate(ctx, o); @@ -586,7 +582,7 @@ public class NonTransactionalRuleContentDiskDriver implements ExtendedDiskInterf EvaluatorContext ctx = driverState.contextMap.get(folder); if(ctx == null) { - ctx = ruleEvaluator.createContext(); + ctx = ruleEvaluator.createContext(driverState.sessionState); driverState.contextMap.put(folder, ctx); if(logger.isDebugEnabled()) { diff --git a/source/java/org/alfresco/filesys/repo/rules/EvaluatorContext.java b/source/java/org/alfresco/filesys/repo/rules/EvaluatorContext.java index 1aa1994b8f..33077dc9ee 100644 --- a/source/java/org/alfresco/filesys/repo/rules/EvaluatorContext.java +++ b/source/java/org/alfresco/filesys/repo/rules/EvaluatorContext.java @@ -6,11 +6,25 @@ package org.alfresco.filesys.repo.rules; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * EvaluatorContext */ public interface EvaluatorContext { + /** + * Get the current scenario instances for this context + * @return a list of the curent scenario instances + */ public List getScenarioInstances(); + + /** + * Get the session state for this context. + * @return the session state for this context. + */ + public Map getSessionState(); + + } diff --git a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java index 0520179c23..3d7a8b400b 100644 --- a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java +++ b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java @@ -18,6 +18,8 @@ */ package org.alfresco.filesys.repo.rules; +import java.util.Map; + /** * The Rule Evaluator evaluates the operation and returns * details of the commands to implement those operations. @@ -31,7 +33,7 @@ public interface RuleEvaluator * An evaluator context groups operations together. * @return the new context. */ - public EvaluatorContext createContext(); + public EvaluatorContext createContext(MapsessionContext); /** * Evaluate the scenarios contained within the context against the current operation diff --git a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluatorImpl.java b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluatorImpl.java index 6da6688b23..a2e9a6c16c 100644 --- a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluatorImpl.java +++ b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluatorImpl.java @@ -33,17 +33,23 @@ import org.apache.commons.logging.LogFactory; * The Rule Evaluator evaluates the operation and returns * details of the commands to implement those operations. *

- * It is configured with a list of scenarios. + * It is configured with a list of scenarios which act as factories for scenario instances. */ public class RuleEvaluatorImpl implements RuleEvaluator { private static Log logger = LogFactory.getLog(RuleEvaluatorImpl.class); /** - * The evaluator context + * The evaluator context, one for each folder */ private class EvaluatorContextImpl implements EvaluatorContext { + MapsessionState; + + EvaluatorContextImpl (MapsessionState) + { + this.sessionState = sessionState; + } /** * Current instances of scenarios */ @@ -53,6 +59,12 @@ public class RuleEvaluatorImpl implements RuleEvaluator public List getScenarioInstances() { return currentScenarioInstances; + } + + @Override + public Map getSessionState() + { + return sessionState; } } @@ -87,7 +99,7 @@ public class RuleEvaluatorImpl implements RuleEvaluator { for(Scenario scenario : scenarios) { - ScenarioInstance instance = scenario.createInstance(context.getScenarioInstances(), operation); + ScenarioInstance instance = scenario.createInstance(context, operation); if(instance != null) { context.getScenarioInstances().add(instance); @@ -161,9 +173,11 @@ public class RuleEvaluatorImpl implements RuleEvaluator } @Override - public EvaluatorContext createContext() + public EvaluatorContext createContext(MapsessionState) { - return new EvaluatorContextImpl(); + EvaluatorContextImpl impl = new EvaluatorContextImpl(sessionState); + + return impl; } } diff --git a/source/java/org/alfresco/filesys/repo/rules/Scenario.java b/source/java/org/alfresco/filesys/repo/rules/Scenario.java index beb4f83f6c..2fc43aba70 100644 --- a/source/java/org/alfresco/filesys/repo/rules/Scenario.java +++ b/source/java/org/alfresco/filesys/repo/rules/Scenario.java @@ -35,7 +35,7 @@ public interface Scenario * @param operation the operation to be performed * @return the scenario instance or null if a new instance is not required. */ - ScenarioInstance createInstance(final List currentInstances, Operation operation); + ScenarioInstance createInstance(EvaluatorContext ctx, Operation operation); } diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffle.java index ad004c279d..21fd198a52 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffle.java @@ -51,7 +51,7 @@ public class ScenarioCreateDeleteRenameShuffle implements Scenario private Ranking ranking = Ranking.HIGH; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by a create of a file matching diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java index 179f70336f..559a90e6f1 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java @@ -46,11 +46,11 @@ import org.apache.commons.logging.LogFactory; * *

* If this filter is active then this is what happens. - * a) New file created. New file created (X). - * b) Existing file deleted (Y to Z). File moved to temporary location. + * a) New file created. New file created. + * b) Existing file deleted. File moved to temporary location instead. * c) Rename - Scenario fires * - File moved back from temporary location - * - Content updated/ + * - Content updated. * - temporary file deleted */ public class ScenarioCreateDeleteRenameShuffleInstance implements ScenarioInstance diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateShuffle.java index 73e843de6d..d1ad567c77 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateShuffle.java @@ -52,7 +52,7 @@ public class ScenarioCreateShuffle implements Scenario private Ranking ranking = Ranking.HIGH; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by a create of a file matching diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffle.java index b7521d225b..b921665ce2 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffle.java @@ -51,7 +51,7 @@ public class ScenarioDoubleRenameShuffle implements Scenario private Ranking ranking = Ranking.HIGH; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by a rename of a file matching diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java index d9568aa8e7..0838b713ba 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java @@ -47,7 +47,7 @@ public class ScenarioLockedDeleteShuffle implements Scenario private Ranking ranking = Ranking.HIGH; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by a create of a file matching diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFile.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFile.java index 1e43724699..ed92fcb4fb 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFile.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFile.java @@ -54,7 +54,7 @@ public class ScenarioOpenFile implements Scenario private long timeout = 300000; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by an open or create of a new file @@ -71,7 +71,7 @@ public class ScenarioOpenFile implements Scenario if(c.getName().matches(pattern)) { - if(checkScenarioActive(c.getName(),currentInstances)) + if(checkScenarioActive(c.getName(),ctx.getScenarioInstances())) { logger.debug("scenario already active for name" + c.getName()); return null; @@ -102,7 +102,7 @@ public class ScenarioOpenFile implements Scenario if(o.getName().matches(pattern)) { - if(checkScenarioActive(o.getName(),currentInstances)) + if(checkScenarioActive(o.getName(),ctx.getScenarioInstances())) { logger.debug("scenario already active for name" + o.getName()); return null; diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java index 7ffa139071..1828795c38 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java @@ -50,7 +50,7 @@ public class ScenarioRenameShuffle implements Scenario private long timeout = 30000; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * This scenario is triggered by a rename of a file matching diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBuffered.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBuffered.java index 697b930fad..7e0817648d 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBuffered.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBuffered.java @@ -34,7 +34,7 @@ public class ScenarioSimpleNonBuffered implements Scenario private Ranking ranking = Ranking.LOW; @Override - public ScenarioInstance createInstance(final List currentInstances, Operation operation) + public ScenarioInstance createInstance(final EvaluatorContext ctx, Operation operation) { /** * The bog standard scenario is always interested. diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioTempDeleteShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioTempDeleteShuffle.java new file mode 100644 index 0000000000..c86bb560dc --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioTempDeleteShuffle.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2005-2011 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 java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.filesys.repo.rules.ScenarioInstance.Ranking; +import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; +import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; +import org.alfresco.jlan.server.filesys.FileName; +import org.alfresco.util.MaxSizeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A temp delete shuffle. + * + * Files are created in a temporary directory + * and then a delete and move. + */ +public class ScenarioTempDeleteShuffle implements Scenario +{ + private static Log logger = LogFactory.getLog(ScenarioTempDeleteShuffle.class); + + protected final static String SCENARIO_KEY = "org.alfresco.filesys.repo.rules.ScenarioTempDeleteShuffle"; + + /** + * The regex pattern of a create that will identify a temporary directory. + */ + private Pattern tempDirPattern; + private String strTempDirPattern; + + /** + * 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 delete of a file matching + * the pattern + */ + if(operation instanceof CreateFileOperation) + { + CreateFileOperation c = (CreateFileOperation)operation; + + // check whether file is below .TemporaryItems + String path = c.getPath(); + + // if path contains .TemporaryItems + Matcher d = tempDirPattern.matcher(path); + if(d.matches()) + { + logger.debug("pattern matches temp dir folder so this is a new create in a temp dir"); + Matcher m = pattern.matcher(c.getName()); + if(m.matches()) + { + // and how to lock - since we are already have one lock on the scenarios/folder here + // this is a potential deadlock and synchronization bottleneck + Map createdTempFiles = (Map)ctx.getSessionState().get(SCENARIO_KEY); + + if(createdTempFiles == null) + { + synchronized(ctx.getSessionState()) + { + logger.debug("created new temp file map and added it to the session state"); + createdTempFiles = (Map)ctx.getSessionState().get(SCENARIO_KEY); + if(createdTempFiles == null) + { + createdTempFiles = Collections.synchronizedMap(new MaxSizeMap(5, false)); + ctx.getSessionState().put(SCENARIO_KEY, createdTempFiles); + } + } + } + createdTempFiles.put(c.getName(), c.getName()); + + // TODO - Return a different scenario instance here ??? + // So it can time out and have anti-patterns etc? + } + } + } + + if(operation instanceof DeleteFileOperation) + { + DeleteFileOperation c = (DeleteFileOperation)operation; + + Matcher m = pattern.matcher(c.getName()); + if(m.matches()) + { + Map createdTempFiles = (Map)ctx.getSessionState().get(SCENARIO_KEY); + + if(createdTempFiles != null) + { + if(createdTempFiles.containsKey(c.getName())) + { + if(logger.isDebugEnabled()) + { + logger.debug("New Scenario Temp Delete Shuffle Instance:" + c.getName()); + } + + ScenarioTempDeleteShuffleInstance instance = new ScenarioTempDeleteShuffleInstance() ; + 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 setTempDirPattern(String tempDirPattern) + { + this.tempDirPattern = Pattern.compile(tempDirPattern, Pattern.CASE_INSENSITIVE); + this.strTempDirPattern = tempDirPattern; + } + + public String getTempDirPattern() + { + return this.strTempDirPattern; + } + + 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/ScenarioTempDeleteShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioTempDeleteShuffleInstance.java new file mode 100644 index 0000000000..fab004d210 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioTempDeleteShuffleInstance.java @@ -0,0 +1,241 @@ +/* + * 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 java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.filesys.repo.rules.commands.CompoundCommand; +import org.alfresco.filesys.repo.rules.commands.CopyContentCommand; +import org.alfresco.filesys.repo.rules.commands.DeleteFileCommand; +import org.alfresco.filesys.repo.rules.commands.RenameFileCommand; +import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; +import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; +import org.alfresco.filesys.repo.rules.operations.MoveFileOperation; +import org.alfresco.filesys.repo.rules.operations.RenameFileOperation; +import org.alfresco.jlan.server.filesys.FileName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This is an instance of a "temp delete shuffle" triggered by a delete of a file matching + * a newly created file in a temporary directory. + * + *

First implemented for TextEdit from MacOS Lion + * + *

+ * Sequence of operations. + * a) Temporary Directory Created + * b) Temporary file created in temporary directory. + * c) Target file deleted + * d) Temp file moved in place of target file. + * e) Temporary directory deleted. + *

+ * If this filter is active then this is what happens. + * a) Temp file created - in another folder. + * b) Existing file deleted. Scenario kicks in to rename rather than delete. + * c) New file moved into place (X to Y). Scenario kicks in + * 1) renames file from step c + * 2) copies content from temp file to target file + * 3) deletes temp file. + * d) Clean up scenario. + */ +public class ScenarioTempDeleteShuffleInstance implements ScenarioInstance +{ + private static Log logger = LogFactory.getLog(ScenarioTempDeleteShuffleInstance.class); + + enum InternalState + { + NONE, + DELETE_SUBSTITUTED, // Scenario has intervened and renamed rather than delete + MOVED + } + + InternalState internalState = InternalState.NONE; + + private Date startTime = new Date(); + + private String lockName; + + private Ranking ranking; + + /** + * Timeout in ms. Default 30 seconds. + */ + private long timeout = 60000; + + private boolean isComplete; + + /** + * Keep track of deletes that we substitute with a rename + * could be more than one if scenarios overlap + * + * From, TempFileName + */ + private Map deletes = new HashMap(); + + /** + * 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 lockName:" + lockName); + isComplete = true; + return null; + } + } + + switch (internalState) + { + + case NONE: + + /** + * Looking for target file being deleted + * + * Need to intervene and replace delete with a rename to temp file. + */ + if(operation instanceof DeleteFileOperation) + { + DeleteFileOperation d = (DeleteFileOperation)operation; + + + if(logger.isDebugEnabled()) + { + logger.debug("entering DELETE_SUBSTITUTED state: " + lockName); + } + + String tempName = ".shuffle" + d.getName(); + + deletes.put(d.getName(), tempName); + + String[] paths = FileName.splitPath(d.getPath()); + String currentFolder = paths[0]; + + RenameFileCommand r1 = new RenameFileCommand(d.getName(), tempName, d.getRootNodeRef(), d.getPath(), currentFolder + "\\" + tempName); + + internalState = InternalState.DELETE_SUBSTITUTED; + + return r1; + + } + else + { + // anything else bomb out + if(logger.isDebugEnabled()) + { + logger.debug("State error, expected a DELETE"); + } + isComplete = true; + } + break; + + case DELETE_SUBSTITUTED: + + /** + * Looking for a move operation of the deleted file + */ + if(operation instanceof MoveFileOperation) + { + MoveFileOperation m = (MoveFileOperation)operation; + + String targetFile = m.getTo(); + + if(deletes.containsKey(targetFile)) + { + String tempName = deletes.get(targetFile); + + String[] paths = FileName.splitPath(m.getToPath()); + String currentFolder = paths[0]; + + /** + * This is where the scenario fires. + * a) Rename the temp file back to the targetFile + * b) Copy content from moved file + * c) Delete rather than move file + */ + logger.debug("scenario fires"); + ArrayList commands = new ArrayList(); + + RenameFileCommand r1 = new RenameFileCommand(tempName, targetFile, m.getRootNodeRef(), currentFolder + "\\" + tempName, m.getToPath()); + + CopyContentCommand copyContent = new CopyContentCommand(m.getFrom(), targetFile, m.getRootNodeRef(), m.getFromPath(), m.getToPath()); + + DeleteFileCommand d1 = new DeleteFileCommand(m.getFrom(), m.getRootNodeRef(), m.getFromPath()); + + commands.add(r1); + commands.add(copyContent); + commands.add(d1); + + logger.debug("Scenario complete"); + isComplete = true; + + return new CompoundCommand(commands); + } + } + + } + + 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 "ScenarioTempDeleteShuffleInstance:" + lockName; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public long getTimeout() + { + return timeout; + } +} diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java index c5cff08223..12875d301e 100644 --- a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java @@ -22,11 +22,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import org.alfresco.model.ContentModel; import org.alfresco.repo.batch.BatchProcessWorkProvider; @@ -100,6 +101,7 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean; public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean { private static final Log logger = LogFactory.getLog(HomeFolderProviderSynchronizer.class); + private static final Log batchLogger = LogFactory.getLog(HomeFolderProviderSynchronizer.class+".batch"); private static final String GUEST_HOME_FOLDER_PROVIDER = "guestHomeFolderProvider"; private static final String BOOTSTRAP_HOME_FOLDER_PROVIDER = "bootstrapHomeFolderProvider"; @@ -225,6 +227,8 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean * * Alternative approaches are possible, but the above has the advantage that * nodes are not moved if they are already in their preferred location. + * + * Also needed to change the case of parent folders. */ // Using authorities rather than Person objects as they are much lighter @@ -279,9 +283,9 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean for (RunAsWorker worker: workers) { String name = worker.getName(); - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" -- "+ + logger.info(" -- "+ (TenantService.DEFAULT_DOMAIN.equals(tenantDomain)? "" : tenantDomain+" ")+ name+" --"); } @@ -296,11 +300,11 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean new WorkProvider(authorities), threadCount, peoplePerTransaction, null, - logger, 100); + batchLogger, 100); processor.process(worker, true); if (processor.getTotalErrors() > 0) { - logger.debug(" -- Give up after error --"); + logger.info(" -- Give up after error --"); break; } } @@ -319,7 +323,11 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean { public Set execute() throws Exception { - return authorityService.getAllAuthorities(AuthorityType.USER); + // Returns a sorted set (using natural ordering) rather than a hashCode + // so that it is more obvious what the order is for processing users. + Set result = new TreeSet(); + result.addAll(authorityService.getAllAuthorities(AuthorityType.USER)); + return result; } }; return txnHelper.doInTransaction(restoreCallback, false, true); @@ -414,17 +422,24 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean private String createTmpFolderName(NodeRef root) { // Try a few times but then give up. - for (int i = 1; i <= 100; i++) + String temporary = "Temporary-"; + int from = 1; + int to = 100; + for (int i = from; i <= to; i++) { - String tmpFolderName = "Temporary"+i; + String tmpFolderName = temporary+i; if (fileFolderService.searchSimple(root, tmpFolderName) == null) { fileFolderService.create(root, tmpFolderName, ContentModel.TYPE_FOLDER); return tmpFolderName; } } - throw new PersonException("Unable to create a temporty " + - "folder into which home folders could be moved."); + String msg = "Unable to create a temporary " + + "folder into which home folders will be moved. " + + "Tried creating " + temporary + from + " .. " + temporary + to + + ". Remove these folders and try again."; + logger.error(" # "+msg); + throw new PersonException(msg); } }.doWork(); } @@ -480,18 +495,18 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean @Override protected void handleInPreferredLocation() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+toPath(actualPath)+" is already in preferred location."); + logger.info(" # "+toPath(actualPath)+" is already in preferred location."); } } @Override protected void handleSharedHomeProvider() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+userName+" "+providerName+" creates shared home folders - These are not moved."); + logger.info(" # "+userName+" "+providerName+" creates shared home folders - These are not moved."); } } @@ -499,36 +514,36 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean @Override protected void handleOriginalSharedHomeProvider() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+userName+" Original "+originalProviderName+" creates shared home folders - These are not moved."); + logger.info(" # "+userName+" Original "+originalProviderName+" creates shared home folders - These are not moved."); } } @Override protected void handleNotAHomeFolderProvider2() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+userName+" "+providerName+" for is not a HomeFolderProvider2."); + logger.info(" # "+userName+" "+providerName+" for is not a HomeFolderProvider2."); } } @Override protected void handleSpecialHomeFolderProvider() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+userName+" Original "+originalProviderName+" is an internal type - These are not moved."); + logger.info(" # "+userName+" Original "+originalProviderName+" is an internal type - These are not moved."); } } @Override protected void handleHomeFolderNotSet() { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" "+userName+" Home folder is not set - ignored"); + logger.info(" # "+userName+" Home folder is not set - ignored"); } } }.doWork(); @@ -538,6 +553,11 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean * @return a String for debug a folder list. */ private String toPath(List folders) + { + return toPath(folders, (folders == null) ? 0 : folders.size()-1); + } + + private String toPath(List folders, int depth) { StringBuilder sb = new StringBuilder(""); if (folders != null) @@ -549,8 +569,16 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean sb.append('/'); } sb.append(folder); + if (depth-- <= 0) + { + break; + } } } + else + { + sb.append('.'); + } return sb.toString(); } @@ -634,20 +662,23 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean // parent folders should have been created. NodeRef newParent = createNewParentIfRequired(root, preferredPath); + // If the preferred home folder already exists, append "-N" + homeFolderManager.modifyHomeFolderNameIfItExists(root, preferredPath); String homeFolderName = preferredPath.get(preferredPath.size() - 1); - // Throw our own FileExistsException before we get one that - // marks the transaction for rollback, as there is no point - // trying again. - if (nodeService.getChildByName(newParent, ContentModel.ASSOC_CONTAINS, - homeFolderName) != null) - { - throw new FileExistsException(newParent, homeFolderName); - } - // Get the old parent before we move anything. NodeRef oldParent = nodeService.getPrimaryParent(homeFolder) .getParentRef(); + // Log action + if (logger.isInfoEnabled()) + { + logger.info(" mv "+toPath(actualPath)+ + " "+ toPath(preferredPath)+ + ((providerName != null && !providerName.equals(originalProviderName)) + ? " # AND reset provider to "+providerName + : "") + "."); + } + // Perform the move homeFolder = fileFolderService.move(homeFolder, newParent, homeFolderName).getNodeRef(); @@ -662,16 +693,6 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean ContentModel.PROP_HOME_FOLDER_PROVIDER, providerName); } - // Log action - if (logger.isDebugEnabled()) - { - logger.debug(" mv "+toPath(actualPath)+ - " "+ toPath(preferredPath)+ - ((providerName != null && !providerName.equals(originalProviderName)) - ? " AND reset provider to "+providerName - : "") + "."); - } - // Tidy up removeEmptyParentFolders(oldParent, oldRoot); } @@ -679,7 +700,7 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean { String message = "mv "+toPath(actualPath)+" "+toPath(preferredPath)+ " failed as the target already existed."; - logger.error(" "+message); + logger.error(" # "+message); throw new PersonException(message); } catch (FileNotFoundException e) @@ -691,7 +712,7 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean throw new PersonException(message); } } - + private NodeRef createNewParentIfRequired(NodeRef root, List homeFolderPath) { NodeRef parent = root; @@ -701,8 +722,13 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean String pathElement = homeFolderPath.get(i); NodeRef nodeRef = nodeService.getChildByName(parent, ContentModel.ASSOC_CONTAINS, pathElement); + String path = toPath(homeFolderPath, i); if (nodeRef == null) { + if (logger.isInfoEnabled()) + { + logger.info(" mkdir "+path); + } parent = fileFolderService.create(parent, pathElement, ContentModel.TYPE_FOLDER).getNodeRef(); } @@ -714,7 +740,13 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean // there is no point trying again. if (!fileFolderService.getFileInfo(nodeRef).isFolder()) { - throw new FileExistsException(parent, null); + if (logger.isErrorEnabled()) + { + logger.error(" # cannot create folder " + path + + " as content with the same name exists. " + + "Move the content and try again."); + } + throw new FileExistsException(parent, path); } parent = nodeRef; @@ -757,9 +789,9 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean { return; } - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - logger.debug(" rm "+toPath(root, nodeRef)); + logger.info(" rm "+toPath(root, nodeRef)); } nodeService.deleteNode(nodeRef); } @@ -983,84 +1015,226 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean } } - // Gathers and checks parent folder paths. + // Records the parents of the preferred folder paths (the leaf folder are not recorded) + // and checks actual paths against these. private class ParentFolderStructure { - // Sets of parent folders within each root node - private Map>> folders = new HashMap>>(); + // Parent folders within each root node + private Map folders = new HashMap(); public void recordParentFolder(NodeRef root, List path) { - Set> rootsFolders = getFolders(root); + RootFolder rootsFolders = getFolders(root); synchronized(rootsFolders) { - // If parent is the root, all home folders clash - int parentSize = path.size() - 1; - if (parentSize == 0) - { - // We could optimise the code a little by clearing - // all other entries and putting a contains(null) - // check just inside the synchronized(rootsFolders) - // but it might be useful to have a complete lit of - // folders. - rootsFolders.add(null); - - if (logger.isDebugEnabled()) - { - logger.debug(" Recorded root as parent"); - } - } - else - { - while (parentSize-- > 0) - { - List parentPath = new ArrayList(); - for (int j = 0; j <= parentSize; j++) - { - parentPath.add(path.get(j)); - } - - if (logger.isDebugEnabled() - && !rootsFolders.contains(parentPath)) - { - logger.debug(" Recorded parent: " - + toPath(parentPath)); - } - - rootsFolders.add(parentPath); - } - } + rootsFolders.add(path); } } /** - * @return {@code true} if the {@code path} is a parent folder - * or the parent folders includes the root itself. In - * the latter case all existing folders might clash - * so must be moved out of the way. + * Checks to see if there is a clash between the preferred paths and the + * existing folder structure. If there is a clash, the existing home folder + * (the leaf folder) is moved to a temporary structure. This allows any + * parent folders to be tidied up (if empty), so that the new preferred + * structure can be recreated.

+ * + * 1. There is no clash if the path is null or empty. + * + * 2. There is a clash if there is a parent structure included the root + * folder itself.

+ * + * 3. There is a clash if the existing path exists in the parent structure. + * This comparison ignores case as Alfresco does not allow duplicates + * regardless of case.

+ * + * 4. There is a clash if any of the folders in the existing path don't + * match the case of the parent folders. + * + * 5. There is a clash there are different case versions of the parent + * folders themselves or other existing folders. + * + * When 4 takes place, we will end up with the first one we try to recreate + * being used for all. */ public boolean clash(NodeRef root, List path) { - Set> rootsFolders = getFolders(root); + if (path == null || path.isEmpty()) + { + return false; + } + + RootFolder rootsFolders = getFolders(root); synchronized(rootsFolders) { - return rootsFolders.contains(path) || - rootsFolders.contains(null); + return rootsFolders.clash(path); } } - private Set> getFolders(NodeRef root) + private RootFolder getFolders(NodeRef root) { synchronized(folders) { - Set> rootsFolders = folders.get(root); + RootFolder rootsFolders = folders.get(root); if (rootsFolders == null) { - rootsFolders = new HashSet>(); + rootsFolders = new RootFolder(); folders.put(root, rootsFolders); } return rootsFolders; } } + + // Records the parents of the preferred folder paths (the leaf folder are not recorded) + // and checks actual paths against these BUT only for a single root. + private class RootFolder extends Folder + { + private boolean includesRoot; + + public RootFolder() + { + super(null); + } + + // Adds a path (but not the leaf folder) if it does not already exist. + public void add(List path) + { + if (!includesRoot) + { + int parentSize = path.size() - 1; + if (parentSize == 0) + { + includesRoot = true; + children = null; // can discard children as all home folders now clash. + if (logger.isInfoEnabled()) + { + logger.info(" # Recorded root as parent - no need to record other parents as all home folders will clash"); + } + } + else + { + add(path, 0); + } + } + } + + /** + * See description of {@link ParentFolderStructure#clash(NodeRef, List)}.

+ * + * Performs check 2 and then calls {@link Folder#clash(List, int)} to + * perform 3, 4 and 5. + */ + public boolean clash(List path) + { + // Checks 2. + return includesRoot ? false : clash(path, 0); + } + } + + private class Folder + { + // Case specific name of first folder added. + String name; + + // Indicates if there is another preferred name that used different case. + boolean duplicateWithDifferentCase; + + List children; + + public Folder(String name) + { + this.name = name; + } + + /** + * Adds a path (but not the leaf folder) if it does not already exist. + * @param path the full path to add + * @param depth the current depth into the path starting with 0. + */ + protected void add(List path, int depth) + { + int parentSize = path.size() - 1; + String name = path.get(depth); + Folder child = getChild(name); + if (child == null) + { + child = new Folder(name); + if (children == null) + { + children = new LinkedList(); + } + children.add(child); + if (logger.isInfoEnabled()) + { + logger.info(" " + toPath(path, depth)); + } + } + else if (!child.name.equals(name)) + { + child.duplicateWithDifferentCase = true; + } + + // Don't add the leaf folder + if (++depth < parentSize) + { + add(path, depth); + } + } + + /** + * See description of {@link ParentFolderStructure#clash(NodeRef, List)}.

+ * + * Performs checks 3, 4 and 5 for a single level and then recursively checks + * lower levels. + */ + protected boolean clash(List path, int depth) + { + String name = path.get(depth); + Folder child = getChild(name); // Uses equalsIgnoreCase + if (child == null) + { + // Negation of check 3. + return false; + } + else if (child.duplicateWithDifferentCase) // if there folders using different case! + { + // Check 5. + return true; + } + else if (!child.name.equals(name)) // if the case does not match + { + // Check 4. + child.duplicateWithDifferentCase = true; + return true; + } + + // If a match (including case) has been made to the end of the path + if (++depth == path.size()) + { + // Check 3. + return true; + } + + // Check lower levels. + return clash(path, depth); + } + + /** + * Returns the child folder with the specified name (ignores case). + */ + private Folder getChild(String name) + { + if (children != null) + { + for (Folder child: children) + { + if (name.equalsIgnoreCase(child.name)) + { + return child; + } + } + } + return null; + } + } } } diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java index 498295271d..46539e60c0 100644 --- a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java @@ -28,8 +28,8 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Properties; import java.util.Set; +import java.util.TreeSet; import javax.transaction.Status; import javax.transaction.UserTransaction; @@ -51,7 +51,6 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; @@ -83,6 +82,7 @@ public class HomeFolderProviderSynchronizerTest private static AuthorityService authorityService; private static TenantAdminService tenantAdminService; private static TenantService tenantService; + private static UserNameMatcherImpl userNameMatcher; private static PortableHomeFolderManager homeFolderManager; private static RegexHomeFolderProvider largeHomeFolderProvider; private static String largeHomeFolderProviderName; @@ -109,6 +109,7 @@ public class HomeFolderProviderSynchronizerTest authorityService = (AuthorityService) applicationContext.getBean("authorityService"); tenantAdminService = (TenantAdminService) applicationContext.getBean("tenantAdminService"); tenantService = (TenantService) applicationContext.getBean("tenantService"); + userNameMatcher = (UserNameMatcherImpl) applicationContext.getBean("userNameMatcher"); homeFolderManager = (PortableHomeFolderManager) applicationContext.getBean("homeFolderManager"); largeHomeFolderProvider = (RegexHomeFolderProvider) applicationContext.getBean("largeHomeFolderProvider"); largeHomeFolderProviderName = largeHomeFolderProvider.getName(); @@ -207,6 +208,7 @@ public class HomeFolderProviderSynchronizerTest trans.commit(); trans = null; AuthenticationUtil.clearCurrentSecurityContext(); + userNameMatcher.setUserNamesAreCaseSensitive(false); // Put back the default } private Set deleteNonAdminGuestUsers() @@ -704,6 +706,8 @@ public class HomeFolderProviderSynchronizerTest @Test public void testPathAlreadyInUseByContent() throws Exception { + System.out.println("testPathAlreadyInUseByContent: EXPECT TO SEE AN EXCEPTION IN THE LOG ======================== "); + createUser("", "fred"); createContent("", "fr"); @@ -731,7 +735,7 @@ public class HomeFolderProviderSynchronizerTest assertHomeFolderLocation("peter", "pe/peter"); assertHomeFolderLocation("pe", "pe/pe"); - assertFalse("The Temporary1 folder should have been removed", exists("Temporary1")); + assertFalse("The Temporary-1 folder should have been removed", exists("Temporary-1")); } @Test @@ -739,24 +743,26 @@ public class HomeFolderProviderSynchronizerTest { createUser("", "fr"); createUser("", "fred"); - createFolder("Temporary1"); - createFolder("Temporary2"); - createFolder("Temporary3"); + createFolder("Temporary-1"); + createFolder("Temporary-2"); + createFolder("Temporary-3"); // Don't delete the temporary folder homeFolderProviderSynchronizer.setKeepEmptyParents("true"); moveUserHomeFolders(); - assertTrue("The existing Temporary1 folder should still exist", exists("Temporary1")); - assertTrue("The existing Temporary2 folder should still exist", exists("Temporary2")); - assertTrue("The existing Temporary3 folder should still exist", exists("Temporary3")); - assertTrue("The existing Temporary4 folder should still exist", exists("Temporary4")); + assertTrue("The existing Temporary-1 folder should still exist", exists("Temporary-1")); + assertTrue("The existing Temporary-2 folder should still exist", exists("Temporary-2")); + assertTrue("The existing Temporary-3 folder should still exist", exists("Temporary-3")); + assertTrue("The existing Temporary-4 folder should still exist", exists("Temporary-4")); } @Test public void testException() throws Exception { + System.out.println("testException: EXPECT TO SEE AN EXCEPTION IN THE LOG ======================== "); + // Force the need for a temporary folder createUser("", "fr"); createUser("", "fred"); @@ -764,7 +770,7 @@ public class HomeFolderProviderSynchronizerTest // Use up all possible temporary folder names for (int i=1; i<=100; i++) { - createFolder("Temporary"+i); + createFolder("Temporary-"+i); } moveUserHomeFolders(); @@ -939,4 +945,38 @@ public class HomeFolderProviderSynchronizerTest assertHomeFolderLocation(tenant2, "fred", "fr/"+tenantService.getDomainUser("fred", tenant2)); } } + + // ALF-11535 + @Test + public void testChangeParentFolderCase() throws Exception + { + // By default, user names are case sensitive + createUser("fr", "FRED"); + moveUserHomeFolders(); + assertHomeFolderLocation("FRED", "FR/FRED"); + assertHomeFolderLocation("fred", "FR/FRED"); // Same user + } + + // ALF-11535 + @Test + public void testCaseSensitiveUsers() throws Exception + { + userNameMatcher.setUserNamesAreCaseSensitive(true); + + // Users are processed in a sorted order (natural ordering). + // The preferred parent folder structure of the first user + // is used where there is a clash between users. + + // The following users are in their natural order. + createUser("Ab", "Abby"); + createUser("TE", "TESS"); + createUser("TE", "Tess"); + createUser("Ab", "aBBY"); + + moveUserHomeFolders(); + assertHomeFolderLocation("Abby", "Ab/Abby"); + assertHomeFolderLocation("TESS", "TE/TESS"); + assertHomeFolderLocation("Tess", "TE/Tess-1"); + assertHomeFolderLocation("aBBY", "Ab/aBBY-1"); + } } diff --git a/source/java/org/alfresco/repo/security/person/PersonTest.java b/source/java/org/alfresco/repo/security/person/PersonTest.java index 4958b51210..df5d12c8f9 100644 --- a/source/java/org/alfresco/repo/security/person/PersonTest.java +++ b/source/java/org/alfresco/repo/security/person/PersonTest.java @@ -72,6 +72,8 @@ public class PersonTest extends TestCase private TransactionService transactionService; private PersonService personService; + private UserNameMatcherImpl userNameMatcher; + private BehaviourFilter policyBehaviourFilter; private NodeService nodeService; private NodeRef rootNodeRef; @@ -93,6 +95,7 @@ public class PersonTest extends TestCase transactionService = (TransactionService) ctx.getBean("transactionService"); personService = (PersonService) ctx.getBean("personService"); + userNameMatcher = (UserNameMatcherImpl) ctx.getBean("userNameMatcher"); nodeService = (NodeService) ctx.getBean("nodeService"); permissionService = (PermissionService) ctx.getBean("permissionService"); authorityService = (AuthorityService) ctx.getBean("authorityService"); @@ -124,6 +127,7 @@ public class PersonTest extends TestCase @Override protected void tearDown() throws Exception { + userNameMatcher.setUserNamesAreCaseSensitive(false); // Put back the default if ((testTX.getStatus() == Status.STATUS_ACTIVE) || (testTX.getStatus() == Status.STATUS_MARKED_ROLLBACK)) { @@ -1125,9 +1129,7 @@ public class PersonTest extends TestCase final String TEST_PERSON_UPPER = TEST_PERSON_MIXED.toUpperCase(); final String TEST_PERSON_LOWER = TEST_PERSON_MIXED.toLowerCase(); - UserNameMatcherImpl usernameMatcher = new UserNameMatcherImpl(); - usernameMatcher.setUserNamesAreCaseSensitive(true); - ((PersonServiceImpl)personService).setUserNameMatcher(usernameMatcher); // case-sensitive + userNameMatcher.setUserNamesAreCaseSensitive(true); AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); @@ -1217,9 +1219,6 @@ public class PersonTest extends TestCase } // ignore - expected } - - usernameMatcher.setUserNamesAreCaseSensitive(false); - ((PersonServiceImpl)personService).setUserNameMatcher(usernameMatcher); // case-insensitive } public void testUpdateUserNameCase() @@ -1227,6 +1226,7 @@ public class PersonTest extends TestCase final String TEST_PERSON_UPPER = "TEST_PERSON_THREE"; final String TEST_PERSON_LOWER = TEST_PERSON_UPPER.toLowerCase(); + userNameMatcher.setUserNamesAreCaseSensitive(true); AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); diff --git a/source/java/org/alfresco/repo/security/person/PortableHomeFolderManager.java b/source/java/org/alfresco/repo/security/person/PortableHomeFolderManager.java index 54d0e61e49..91cec9aa15 100644 --- a/source/java/org/alfresco/repo/security/person/PortableHomeFolderManager.java +++ b/source/java/org/alfresco/repo/security/person/PortableHomeFolderManager.java @@ -353,43 +353,51 @@ public class PortableHomeFolderManager implements HomeFolderManager } else { + // If the preferred home folder already exists, append "-N" + NodeRef root = getRootPathNodeRef(provider); List homeFolderPath = provider.getHomeFolderPath(person); - - FileInfo fileInfo; + modifyHomeFolderNameIfItExists(root, homeFolderPath); - // Test if it already exists - NodeRef existing = getExisting(provider, fileFolderService, homeFolderPath); - if (existing != null) - { - fileInfo = fileFolderService.getFileInfo(existing); - } - else - { - fileInfo = createTree(provider, getRootPathNodeRef(provider), homeFolderPath, - provider.getTemplateNodeRef(), fileFolderService); - } + // Create folder + FileInfo fileInfo = createTree(provider, getRootPathNodeRef(provider), homeFolderPath, + provider.getTemplateNodeRef(), fileFolderService); NodeRef homeFolderNodeRef = fileInfo.getNodeRef(); return new HomeSpaceNodeRef(homeFolderNodeRef, HomeSpaceNodeRef.Status.CREATED); } return homeSpaceNodeRef; } - private NodeRef getExisting(HomeFolderProvider2 provider, FileFolderService fileFolderService, - List homeFolderPath) + /** + * Modifies (if required) the leaf folder name in the {@code homeFolderPath} by + * appending {@code "-N"} (where N is an integer starting with 1), so that a + * new folder will be created. + * @param root folder. + * @param homeFolderPath the full path. Only the final element is used. + */ + public void modifyHomeFolderNameIfItExists(NodeRef root, List homeFolderPath) { - NodeRef existing; + int n = 0; + int last = homeFolderPath.size()-1; + String name = homeFolderPath.get(last); + String homeFolderName = name; try { - FileInfo existingFileInfo = fileFolderService.resolveNamePath(getRootPathNodeRef(provider), homeFolderPath); - existing = existingFileInfo.getNodeRef(); + do + { + if (n > 0) + { + homeFolderName = name+'-'+n; + homeFolderPath.set(last, homeFolderName); + } + n++; + } while (fileFolderService.resolveNamePath(root, homeFolderPath, false) != null); } - catch (FileNotFoundException fnfe) + catch (FileNotFoundException e) { - existing = null;// home folder noderef doesn't exist yet + // Should not be thrown as call to resolveNamePath passes in false } - return existing; } - + /** * creates a tree of folder nodes based on the path elements provided. */ diff --git a/source/java/org/alfresco/repo/site/SiteServiceImpl.java b/source/java/org/alfresco/repo/site/SiteServiceImpl.java index e276bb12c5..f60725b401 100644 --- a/source/java/org/alfresco/repo/site/SiteServiceImpl.java +++ b/source/java/org/alfresco/repo/site/SiteServiceImpl.java @@ -80,6 +80,7 @@ import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.NoSuchPersonException; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.security.PublicServiceAccessService; import org.alfresco.service.cmr.security.AuthorityService.AuthorityFilter; import org.alfresco.service.cmr.site.SiteInfo; import org.alfresco.service.cmr.site.SiteService; @@ -161,6 +162,7 @@ public class SiteServiceImpl extends AbstractLifecycleBean implements SiteServic private BehaviourFilter behaviourFilter; private SitesPermissionCleaner sitesPermissionsCleaner; private PolicyComponent policyComponent; + private PublicServiceAccessService publicServiceAccessService; private NamedObjectRegistry> cannedQueryRegistry; @@ -327,6 +329,11 @@ public class SiteServiceImpl extends AbstractLifecycleBean implements SiteServic this.sitesPermissionsCleaner = sitesPermissionsCleaner; } + public void setPublicServiceAccessService(PublicServiceAccessService publicServiceAccessService) + { + this.publicServiceAccessService = publicServiceAccessService; + } + /** * Set the registry of {@link CannedQueryFactory canned queries} */ @@ -385,13 +392,9 @@ public class SiteServiceImpl extends AbstractLifecycleBean implements SiteServic */ public boolean hasCreateSitePermissions() { - final NodeRef siteRoot = getSiteRoot(); - if (siteRoot == null) - { - throw new SiteServiceException("No root sites folder exists"); - } - boolean result = permissionService.hasPermission(siteRoot, PermissionService.CONTRIBUTOR).equals(AccessStatus.ALLOWED); - return result; + // NOTE: see ALF-13580 - since 3.4.6 PermissionService.CONTRIBUTOR is no longer used as the default on the Sites folder + // instead the ability to call createSite() and the Spring configured ACL is the mechanism used to protect access. + return (publicServiceAccessService.hasAccess("SiteService", "createSite", "", "", "", "", true) == AccessStatus.ALLOWED); } /** diff --git a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java index b65826a292..b7019b3027 100644 --- a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java +++ b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java @@ -432,11 +432,11 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo /** * Create tenant by restoring from a complete repository export. This is equivalent to a bootstrap import using restore-context.xml. */ - public void importTenant(String tenantDomain, final File directorySource, String rootContentStoreDir) - { - tenantDomain = getTenantDomain(tenantDomain); - - initTenant(tenantDomain, rootContentStoreDir); + public void importTenant(final String tenantDomainIn, final File directorySource, String contentRoot) + { + final String tenantDomain = getTenantDomain(tenantDomainIn); + + initTenant(tenantDomain, contentRoot); try { diff --git a/source/test-resources/tenant/mt-admin-context.xml b/source/test-resources/tenant/mt-admin-context.xml index 0a138f0c35..b066fb04f2 100644 --- a/source/test-resources/tenant/mt-admin-context.xml +++ b/source/test-resources/tenant/mt-admin-context.xml @@ -14,25 +14,26 @@ - - - - - - - + + + + + + + ${alfresco_user_store.adminusername} - - - + + + alfresco.messages.tenant-interpreter-help - + +