diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 0042f20f5f..2156a1ae05 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -683,6 +683,9 @@ + + + + + + SoftDelete + false + + \ No newline at end of file diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 32052dd628..847263f5cc 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -45,7 +45,7 @@ system.webdav.rootPath=${protocols.rootPath} system.webdav.activities.enabled=true # File name patterns that trigger rename shuffle detection # pattern is used by move - tested against full path after it has been lower cased. -system.webdav.renameShufflePattern=(.*/\\..*)|(.*[a-f0-9]{8}+$)|(.*\\.tmp$)|(.*\\.wbk$)|(.*\\.bak$)|(.*\\~$) +system.webdav.renameShufflePattern=(.*/\\..*)|(.*[a-f0-9]{8}+$)|(.*\\.tmp$)|(.*\\.wbk$)|(.*\\.bak$)|(.*\\~$)|(.*backup.*\\.do[ct]{1}[x]?[m]?$) # Is the JBPM Deploy Process Servlet enabled? diff --git a/config/alfresco/subsystems/fileServers/default/file-servers.properties b/config/alfresco/subsystems/fileServers/default/file-servers.properties index 982c1b9e14..be66cc9886 100644 --- a/config/alfresco/subsystems/fileServers/default/file-servers.properties +++ b/config/alfresco/subsystems/fileServers/default/file-servers.properties @@ -19,6 +19,10 @@ filesystem.rootPath=${protocols.rootPath} # pattern is used by rename - tested against full path after it has been lower cased. filesystem.renameShufflePattern=(.*[a-f0-9]{8}+$)|(.*\\.tmp$)|(.*\\.wbk$)|(.*\\.bak$)|(.*\\~$) +# MNT-211 +# File name patterns for rename shuffle detection CSV files. +filesystem.renameCSVShufflePattern=.*[a-f0-9]{8}+$ + # Should we ever set the read only flag on folders? This may cause problematic # behaviour in Windows clients. See ALF-6727. filesystem.setReadOnlyFlagOnFolders=false diff --git a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml index b2cb6ea006..ff374b8223 100644 --- a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml +++ b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml @@ -84,6 +84,7 @@ + ${filesystem.renameCSVShufflePattern} @@ -193,12 +194,24 @@ 60000 HIGH + + + \._[0-9A-F]{8}\b + 60000 + HIGH + .*~$ 30000 HIGH + + + ^[0-9A-F]{8}$ + 30000 + HIGH + LOW diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties index bccbdb2178..4a0e7c46fe 100644 --- a/config/alfresco/version.properties +++ b/config/alfresco/version.properties @@ -13,9 +13,13 @@ version.label= version.edition=Community +# SCM Revision number + +version.scmrevision=@scm-path@@@scm-revision@ + # Build number -version.build=@build-number@ +version.build=r@scm-revision@-b@build-number@ # Schema number diff --git a/source/java/org/alfresco/cmis/dictionary/CMISAbstractTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISAbstractTypeDefinition.java index 14484b2612..a45c160c2a 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISAbstractTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISAbstractTypeDefinition.java @@ -124,7 +124,7 @@ public abstract class CMISAbstractTypeDefinition implements CMISTypeDefinition, { if (cmisMapping.getDataType(propDef.getDataType()) != null) { - CMISPropertyDefinition cmisPropDef = createProperty(cmisMapping, propDef); + CMISPropertyDefinition cmisPropDef = createProperty(cmisMapping, dictionaryService, propDef); properties.put(cmisPropDef.getPropertyId().getId(), cmisPropDef); } } @@ -140,12 +140,12 @@ public abstract class CMISAbstractTypeDefinition implements CMISTypeDefinition, * @param propDef * @return */ - private CMISPropertyDefinition createProperty(CMISMapping cmisMapping, PropertyDefinition propDef) + private CMISPropertyDefinition createProperty(CMISMapping cmisMapping, DictionaryService dictionaryService, PropertyDefinition propDef) { QName propertyQName = propDef.getName(); String propertyId = cmisMapping.getCmisPropertyId(propertyQName); CMISPropertyId cmisPropertyId = new CMISPropertyId(propertyQName, propertyId); - return new CMISBasePropertyDefinition(cmisMapping, cmisPropertyId, propDef, this); + return new CMISBasePropertyDefinition(cmisMapping, cmisPropertyId, dictionaryService, propDef, this); } /** diff --git a/source/java/org/alfresco/cmis/dictionary/CMISBasePropertyDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISBasePropertyDefinition.java index e3ace5c872..92e27003ff 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISBasePropertyDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISBasePropertyDefinition.java @@ -49,6 +49,7 @@ import org.alfresco.repo.search.impl.lucene.analysis.VerbatimAnalyser; import org.alfresco.service.cmr.dictionary.Constraint; import org.alfresco.service.cmr.dictionary.ConstraintDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.util.ISO9075; @@ -106,14 +107,14 @@ public class CMISBasePropertyDefinition implements CMISPropertyDefinition, Seria * @param propDef * @param typeDef */ - public CMISBasePropertyDefinition(CMISMapping cmisMapping, CMISPropertyId propertyId, PropertyDefinition propDef, + public CMISBasePropertyDefinition(CMISMapping cmisMapping, CMISPropertyId propertyId, DictionaryService dictionaryService, PropertyDefinition propDef, CMISTypeDefinition typeDef) { this.propertyId = propertyId; this.typeDef = typeDef; queryName = ISO9075.encodeSQL(cmisMapping.buildPrefixEncodedString(propertyId.getQName())); - displayName = (propDef.getTitle() != null) ? propDef.getTitle() : propertyId.getId(); - description = propDef.getDescription() != null ? propDef.getDescription() : displayName; + displayName = (propDef.getTitle(dictionaryService) != null) ? propDef.getTitle(dictionaryService) : propertyId.getId(); + description = propDef.getDescription(dictionaryService) != null ? propDef.getDescription(dictionaryService) : displayName; propertyType = cmisMapping.getDataType(propDef.getDataType()); cardinality = propDef.isMultiValued() ? CMISCardinalityEnum.MULTI_VALUED : CMISCardinalityEnum.SINGLE_VALUED; for (ConstraintDefinition constraintDef : propDef.getConstraints()) diff --git a/source/java/org/alfresco/cmis/dictionary/CMISDocumentTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISDocumentTypeDefinition.java index 2d250a7659..090ba25a80 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISDocumentTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISDocumentTypeDefinition.java @@ -24,6 +24,7 @@ import org.alfresco.cmis.CMISScope; import org.alfresco.cmis.CMISTypeId; import org.alfresco.cmis.mapping.CMISMapping; import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.namespace.QName; import org.alfresco.util.ISO9075; @@ -49,15 +50,15 @@ public class CMISDocumentTypeDefinition extends CMISAbstractTypeDefinition * @param typeId * @param cmisClassDef */ - public CMISDocumentTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, ClassDefinition cmisClassDef) + public CMISDocumentTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, DictionaryService dictionaryService, ClassDefinition cmisClassDef) { isPublic = true; // Object type properties this.cmisClassDef = cmisClassDef; objectTypeId = typeId; - displayName = (cmisClassDef.getTitle() != null) ? cmisClassDef.getTitle() : typeId.getId(); - description = cmisClassDef.getDescription() != null ? cmisClassDef.getDescription() : displayName; + displayName = (cmisClassDef.getTitle(dictionaryService) != null) ? cmisClassDef.getTitle(dictionaryService) : typeId.getId(); + description = cmisClassDef.getDescription(dictionaryService) != null ? cmisClassDef.getDescription(dictionaryService) : displayName; QName parentQName = cmisMapping.getCmisType(cmisClassDef.getParentName()); if (typeId == CMISDictionaryModel.DOCUMENT_TYPE_ID) diff --git a/source/java/org/alfresco/cmis/dictionary/CMISFolderTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISFolderTypeDefinition.java index d858a068b9..fb9375c029 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISFolderTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISFolderTypeDefinition.java @@ -24,6 +24,7 @@ import org.alfresco.cmis.CMISTypeId; import org.alfresco.cmis.mapping.CMISMapping; import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.namespace.QName; import org.alfresco.util.ISO9075; @@ -45,15 +46,15 @@ public class CMISFolderTypeDefinition extends CMISAbstractTypeDefinition * @param typeId * @param cmisClassDef */ - public CMISFolderTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, ClassDefinition cmisClassDef, boolean isSystem) + public CMISFolderTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, DictionaryService dictionaryService, ClassDefinition cmisClassDef, boolean isSystem) { isPublic = true; // Object type properties this.cmisClassDef = cmisClassDef; objectTypeId = typeId; - displayName = (cmisClassDef.getTitle() != null) ? cmisClassDef.getTitle() : typeId.getId(); - description = cmisClassDef.getDescription() != null ? cmisClassDef.getDescription() : displayName; + displayName = (cmisClassDef.getTitle(dictionaryService) != null) ? cmisClassDef.getTitle(dictionaryService) : typeId.getId(); + description = cmisClassDef.getDescription(dictionaryService) != null ? cmisClassDef.getDescription(dictionaryService) : displayName; QName parentQName = cmisMapping.getCmisType(cmisClassDef.getParentName()); if (typeId == CMISDictionaryModel.FOLDER_TYPE_ID) diff --git a/source/java/org/alfresco/cmis/dictionary/CMISObjectTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISObjectTypeDefinition.java index be16b02717..8179348f50 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISObjectTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISObjectTypeDefinition.java @@ -47,7 +47,7 @@ public class CMISObjectTypeDefinition extends CMISAbstractTypeDefinition * @param typeId * @param cmisClassDef */ - public CMISObjectTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, ClassDefinition cmisClassDef, boolean isPublic) + public CMISObjectTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, DictionaryService dictionaryService, ClassDefinition cmisClassDef, boolean isPublic) { this.isPublic = isPublic; @@ -58,8 +58,8 @@ public class CMISObjectTypeDefinition extends CMISAbstractTypeDefinition if (cmisClassDef != null) { this.cmisClassDef = cmisClassDef; - displayName = (cmisClassDef.getTitle() != null) ? cmisClassDef.getTitle() : typeId.getId(); - description = cmisClassDef.getDescription() != null ? cmisClassDef.getDescription() : displayName; + displayName = (cmisClassDef.getTitle(dictionaryService) != null) ? cmisClassDef.getTitle(dictionaryService) : typeId.getId(); + description = cmisClassDef.getDescription(dictionaryService) != null ? cmisClassDef.getDescription(dictionaryService) : displayName; QName parentQName = cmisMapping.getCmisType(cmisClassDef.getParentName()); if (parentQName != null) { diff --git a/source/java/org/alfresco/cmis/dictionary/CMISPolicyTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISPolicyTypeDefinition.java index 1114227abf..b527b6bee7 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISPolicyTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISPolicyTypeDefinition.java @@ -50,14 +50,14 @@ public class CMISPolicyTypeDefinition extends CMISAbstractTypeDefinition * @param typeId * @param cmisClassDef */ - public CMISPolicyTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, ClassDefinition cmisClassDef) + public CMISPolicyTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, DictionaryService dictionaryService, ClassDefinition cmisClassDef) { isPublic = true; // Object Type definitions this.cmisClassDef = cmisClassDef; objectTypeId = typeId; - displayName = (cmisClassDef.getTitle() != null) ? cmisClassDef.getTitle() : typeId.getId(); + displayName = (cmisClassDef.getTitle(dictionaryService) != null) ? cmisClassDef.getTitle(dictionaryService) : typeId.getId(); if (typeId == CMISDictionaryModel.POLICY_TYPE_ID) { objectTypeQueryName = typeId.getId(); @@ -72,7 +72,7 @@ public class CMISPolicyTypeDefinition extends CMISAbstractTypeDefinition objectTypeQueryName = ISO9075.encodeSQL(cmisMapping.buildPrefixEncodedString(typeId.getQName())); parentTypeId = CMISDictionaryModel.POLICY_TYPE_ID; } - description = cmisClassDef.getDescription() != null ? cmisClassDef.getDescription() : displayName; + description = cmisClassDef.getDescription(dictionaryService) != null ? cmisClassDef.getDescription(dictionaryService) : displayName; actionEvaluators = cmisMapping.getActionEvaluators(objectTypeId.getScope()); diff --git a/source/java/org/alfresco/cmis/dictionary/CMISRelationshipTypeDefinition.java b/source/java/org/alfresco/cmis/dictionary/CMISRelationshipTypeDefinition.java index 57f1deafef..2290f2d912 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISRelationshipTypeDefinition.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISRelationshipTypeDefinition.java @@ -65,7 +65,7 @@ public class CMISRelationshipTypeDefinition extends CMISAbstractTypeDefinition * @param cmisClassDef * @param assocDef */ - public CMISRelationshipTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, ClassDefinition cmisClassDef, AssociationDefinition assocDef) + public CMISRelationshipTypeDefinition(CMISMapping cmisMapping, CMISTypeId typeId, DictionaryService dictionaryService, ClassDefinition cmisClassDef, AssociationDefinition assocDef) { isPublic = true; this.cmisClassDef = cmisClassDef; @@ -83,22 +83,22 @@ public class CMISRelationshipTypeDefinition extends CMISAbstractTypeDefinition { // TODO: Add CMIS Association mapping?? creatable = false; - displayName = (cmisClassDef.getTitle() != null) ? cmisClassDef.getTitle() : typeId.getId(); + displayName = (cmisClassDef.getTitle(dictionaryService) != null) ? cmisClassDef.getTitle(dictionaryService) : typeId.getId(); objectTypeQueryName = typeId.getId(); QName parentQName = cmisMapping.getCmisType(cmisClassDef.getParentName()); if (parentQName != null) { parentTypeId = cmisMapping.getCmisTypeId(CMISScope.OBJECT, parentQName); } - description = cmisClassDef.getDescription() != null ? cmisClassDef.getDescription() : displayName; + description = cmisClassDef.getDescription(dictionaryService) != null ? cmisClassDef.getDescription(dictionaryService) : displayName; } else { creatable = true; - displayName = (assocDef.getTitle() != null) ? assocDef.getTitle() : typeId.getId(); + displayName = (assocDef.getTitle(dictionaryService) != null) ? assocDef.getTitle(dictionaryService) : typeId.getId(); objectTypeQueryName = ISO9075.encodeSQL(cmisMapping.buildPrefixEncodedString(typeId.getQName())); parentTypeId = CMISDictionaryModel.RELATIONSHIP_TYPE_ID; - description = assocDef.getDescription() != null ? assocDef.getDescription() : displayName; + description = assocDef.getDescription(dictionaryService) != null ? assocDef.getDescription(dictionaryService) : displayName; CMISTypeId sourceTypeId = cmisMapping.getCmisTypeId(cmisMapping.getCmisType(assocDef.getSourceClass().getName())); if (sourceTypeId != null) diff --git a/source/java/org/alfresco/cmis/dictionary/CMISStrictDictionaryService.java b/source/java/org/alfresco/cmis/dictionary/CMISStrictDictionaryService.java index 5940c95965..5f3daa08b9 100644 --- a/source/java/org/alfresco/cmis/dictionary/CMISStrictDictionaryService.java +++ b/source/java/org/alfresco/cmis/dictionary/CMISStrictDictionaryService.java @@ -76,20 +76,20 @@ public class CMISStrictDictionaryService extends CMISAbstractDictionaryService CMISAbstractTypeDefinition objectTypeDef = null; if (typeId.getScope() == CMISScope.DOCUMENT) { - objectTypeDef = new CMISDocumentTypeDefinition(cmisMapping, typeId, classDef); + objectTypeDef = new CMISDocumentTypeDefinition(cmisMapping, typeId, dictionaryService, classDef); } else if (typeId.getScope() == CMISScope.FOLDER) { boolean isSystem = dictionaryService.isSubClass(classDef.getName(), ContentModel.TYPE_SYSTEM_FOLDER); - objectTypeDef = new CMISFolderTypeDefinition(cmisMapping, typeId, classDef, isSystem); + objectTypeDef = new CMISFolderTypeDefinition(cmisMapping, typeId, dictionaryService, classDef, isSystem); } else if (typeId.getScope() == CMISScope.POLICY) { - objectTypeDef = new CMISPolicyTypeDefinition(cmisMapping, typeId, classDef); + objectTypeDef = new CMISPolicyTypeDefinition(cmisMapping, typeId, dictionaryService, classDef); } else if (typeId.getScope() == CMISScope.OBJECT) { - objectTypeDef = new CMISObjectTypeDefinition(cmisMapping, typeId, classDef, false); + objectTypeDef = new CMISObjectTypeDefinition(cmisMapping, typeId, dictionaryService, classDef, false); } registry.registerTypeDefinition(objectTypeDef); @@ -106,7 +106,7 @@ public class CMISStrictDictionaryService extends CMISAbstractDictionaryService { CMISTypeId typeId = cmisMapping.getCmisTypeId(CMISScope.RELATIONSHIP, CMISMapping.RELATIONSHIP_QNAME); ClassDefinition classDef = dictionaryService.getClass(cmisMapping.getCmisType(typeId.getQName())); - CMISAbstractTypeDefinition objectTypeDef = new CMISRelationshipTypeDefinition(cmisMapping, typeId, classDef, null); + CMISAbstractTypeDefinition objectTypeDef = new CMISRelationshipTypeDefinition(cmisMapping, typeId, dictionaryService, classDef, null); registry.registerTypeDefinition(objectTypeDef); for (QName classQName : classQNames) @@ -117,7 +117,7 @@ public class CMISStrictDictionaryService extends CMISAbstractDictionaryService // create appropriate kind of type definition typeId = cmisMapping.getCmisTypeId(CMISScope.RELATIONSHIP, classQName); AssociationDefinition assocDef = dictionaryService.getAssociation(classQName); - objectTypeDef = new CMISRelationshipTypeDefinition(cmisMapping, typeId, null, assocDef); + objectTypeDef = new CMISRelationshipTypeDefinition(cmisMapping, typeId, dictionaryService, null, assocDef); registry.registerTypeDefinition(objectTypeDef); } diff --git a/source/java/org/alfresco/filesys/alfresco/NetworkFileLegacyReferenceCount.java b/source/java/org/alfresco/filesys/alfresco/NetworkFileLegacyReferenceCount.java new file mode 100644 index 0000000000..e933c2b43a --- /dev/null +++ b/source/java/org/alfresco/filesys/alfresco/NetworkFileLegacyReferenceCount.java @@ -0,0 +1,29 @@ +package org.alfresco.filesys.alfresco; + +/** + * Does this NetworkFile have reference counting? + */ +public interface NetworkFileLegacyReferenceCount +{ + /** + * Increment the file open count, first open = 1; + * + * @return the current open count + */ + public int incrementLegacyOpenCount(); + + /** + * Decrement the file open count + * + * @return the current open count + */ + public int decrementLagacyOpenCount(); + + /** + * Return the open file count + * + * @return the current open count + */ + public int getLegacyOpenCount(); + +} diff --git a/source/java/org/alfresco/filesys/alfresco/RepositoryDiskInterface.java b/source/java/org/alfresco/filesys/alfresco/RepositoryDiskInterface.java index 68b112753f..ddba77b87b 100644 --- a/source/java/org/alfresco/filesys/alfresco/RepositoryDiskInterface.java +++ b/source/java/org/alfresco/filesys/alfresco/RepositoryDiskInterface.java @@ -123,5 +123,18 @@ public interface RepositoryDiskInterface * @param path */ public void deleteEmptyFile(NodeRef rootNode, String path); + + /** + * Rename the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param oldName java.lang.String + * @param newName java.lang.String + * @exception java.io.IOException The exception description. + */ + public void renameFile(NodeRef rootNode, String oldName, String newName, boolean soft) + throws java.io.IOException; + } diff --git a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java index b5c842ad75..153c5bb29c 100644 --- a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java +++ b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java @@ -254,13 +254,14 @@ public class CommandExecutorImpl implements CommandExecutor { logger.debug("rename command"); RenameFileCommand rename = (RenameFileCommand)command; - diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); + + repositoryDiskInterface.renameFile(rename.getRootNode(), rename.getFromPath(), rename.getToPath(), rename.isSoft()); } else if(command instanceof MoveFileCommand) { logger.debug("move command"); - MoveFileCommand rename = (MoveFileCommand)command; - diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); + MoveFileCommand move = (MoveFileCommand)command; + repositoryDiskInterface.renameFile(move.getRootNode(), move.getFromPath(), move.getToPath(), false); } else if(command instanceof CopyContentCommand) { diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 1e6eb73ec6..5cfe415a9f 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.Serializable; import java.net.InetAddress; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -50,6 +51,7 @@ import org.alfresco.jlan.server.filesys.DiskFullException; import org.alfresco.jlan.server.filesys.DiskInterface; import org.alfresco.jlan.server.filesys.DiskSizeInterface; import org.alfresco.jlan.server.filesys.FileAttribute; +import org.alfresco.jlan.server.filesys.FileExistsException; import org.alfresco.jlan.server.filesys.FileInfo; import org.alfresco.jlan.server.filesys.FileName; import org.alfresco.jlan.server.filesys.FileOpenParams; @@ -173,10 +175,11 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter protected static final long DiskSizeDefault = 1 * MemorySize.TERABYTE; protected static final long DiskFreeDefault = DiskSizeDefault / 2; - + private boolean isReadOnly; private boolean isLockedFilesAsOffline; - + // pattern for detect CSV files. + private Pattern renameCSVShufflePattern = Pattern.compile(".*[a-f0-9]{8}+$"); // Services and helpers private CifsHelper cifsHelper; @@ -509,6 +512,17 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter this.ownableService = ownableService; } + /** + * Set the regular expression that will be applied to CSV files during renames. + * MNT-211 + * + * @param renameCSVShufflePattern a regular expression CSV filename match + */ + public void setRenameCSVShufflePattern(Pattern renameCSVShufflePattern) + { + this.renameCSVShufflePattern = renameCSVShufflePattern; + } + /** * Parse and validate the parameter string and create a device context object for this instance * of the shared device. The same DeviceInterface implementation may be used for multiple @@ -2091,9 +2105,23 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter { logger.debug("create new file" + path); } - - NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, ContentModel.TYPE_CONTENT); - nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); + + + NodeRef nodeRef = null; + + try + { + nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, ContentModel.TYPE_CONTENT); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); + } + catch ( FileExistsException ex) + { + nodeRef = cifsHelper.getNodeRef(deviceRootNodeRef, path); + if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_SOFT_DELETE)) + { + throw ex; + } + } return new Pair(parentPath, nodeRef); }}); @@ -2108,6 +2136,12 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter parentState = ctx.getStateCache().findFileState(parentPath, true); } + NodeRef nodeRef = result.getSecond(); + if (nodeRef != null && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_SOFT_DELETE)) + { + nodeService.removeAspect(nodeRef, ContentModel.ASPECT_SOFT_DELETE); + } + // Create the network file ContentNetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, mimetypeService, cifsHelper, result.getSecond(), params.getPath(), params.isReadOnlyAccess(), params.isAttributesOnlyAccess(), sess); @@ -3338,6 +3372,7 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter newState.setFileStatus(FileExists); newState.setFilesystemObject(nodeToMoveRef); + newState.setFileSize(oldState.getFileSize()); // 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 @@ -3387,6 +3422,7 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter newState.setFileStatus(FileExists); newState.setFilesystemObject(finalTargetNodeRef); + newState.setFileSize(oldState.getFileSize()); // 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 @@ -3423,7 +3459,15 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter { logger.debug("Rename shuffle for versioning content is assumed. Deleting " + nodeToMoveRef + " as system user"); } - nodeService.deleteNode(nodeToMoveRef); + if (renameCSVShufflePattern.matcher(newName.toLowerCase()).matches()) + { + Map props = Collections.emptyMap(); + nodeService.addAspect(nodeToMoveRef, ContentModel.ASPECT_SOFT_DELETE, props); + } + else + { + nodeService.deleteNode(nodeToMoveRef); + } return null; } diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java index 3074c1e62e..510e98f032 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java @@ -1296,34 +1296,34 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD } return null; } - + + public void renameFile(final SrvSession session, final TreeConnection tree, final String oldName, final String newName) + throws IOException + { + throw new AlfrescoRuntimeException("obsolete method called"); + } + /** * Rename the specified file. * - * @param sess Server session - * @param tree Tree connection - * @param oldName java.lang.String - * @param newName java.lang.String + * @param rootNode + * @param oldName path/name of old file + * @param newName path/name of new file * @exception java.io.IOException The exception description. */ - public void renameFile(final SrvSession session, final TreeConnection tree, final String oldName, final String newName) + public void renameFile(NodeRef rootNode, final String oldName, final String newName, boolean soft) throws IOException { - // Get the device context - final ContentContext ctx = (ContentContext) tree.getContext(); - - // DEBUG - + if (logger.isDebugEnabled()) { - logger.debug("RenameFile oldName=" + oldName + ", newName=" + newName + ", session:" + session.getUniqueId()); + logger.debug("RenameFile oldName=" + oldName + ", newName=" + newName + ", soft" + soft); } try { // Get the file/folder to move - - final NodeRef nodeToMoveRef = getNodeForPath(tree, oldName); + final NodeRef nodeToMoveRef = getCifsHelper().getNodeRef(rootNode, oldName); // Check if the node is a link node @@ -1336,8 +1336,8 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD String[] splitPaths = FileName.splitPath(newName); String[] oldPaths = FileName.splitPath(oldName); - final NodeRef targetFolderRef = getNodeForPath(tree, splitPaths[0]); - final NodeRef sourceFolderRef = getNodeForPath(tree, oldPaths[0]); + final NodeRef targetFolderRef = getCifsHelper().getNodeRef(rootNode, splitPaths[0]); + final NodeRef sourceFolderRef = getCifsHelper().getNodeRef(rootNode, oldPaths[0]); final String name = splitPaths[1]; // Check if this is a rename within the same folder @@ -1347,7 +1347,8 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD // Check if we are renaming a folder, or the rename is to a different folder boolean isFolder = getCifsHelper().isDirectory(nodeToMoveRef); - if ( isFolder == true || sameFolder == false) { + if ( isFolder == true || sameFolder == false) + { // Rename or move the file/folder to another folder if (sameFolder == true) @@ -1383,7 +1384,8 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD else { // Rename a file within the same folder - + + if (logger.isDebugEnabled()) { logger.debug( @@ -1395,8 +1397,16 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD " Node: " + nodeToMoveRef + "\n" + " Aspects: " + nodeService.getAspects(nodeToMoveRef)); } - fileFolderService.rename(nodeToMoveRef, name); - + if(soft) + { + logger.debug("this is a soft delete - use copy rather than rename"); + fileFolderService.copy(nodeToMoveRef, null, name); + nodeService.addAspect(nodeToMoveRef, ContentModel.ASPECT_SOFT_DELETE, null); + } + else + { + fileFolderService.rename(nodeToMoveRef, name); + } } } catch (org.alfresco.service.cmr.model.FileNotFoundException e) @@ -2347,12 +2357,31 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD dirNodeRef = rootNode; folderName = path; } - - NodeRef nodeRef = cifsHelper.createNode(dirNodeRef, folderName, ContentModel.TYPE_CONTENT); - - nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); - + boolean soft = false; + + NodeRef existing = fileFolderService.searchSimple(dirNodeRef, folderName); + if (existing != null) + { + if(nodeService.hasAspect(existing, ContentModel.ASPECT_SOFT_DELETE)) + { + logger.debug("existing node has soft delete aspect"); + soft = true; + } + } + + NodeRef nodeRef = null; + + if(soft) + { + nodeRef = existing; + } + else + { + nodeRef = cifsHelper.createNode(dirNodeRef, folderName, ContentModel.TYPE_CONTENT); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT, null); + } + File file = TempFileProvider.createTempFile("cifs", ".bin"); TempNetworkFile netFile = new TempNetworkFile(file, path); diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java index 722220f359..d6e04a5ac7 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java @@ -277,6 +277,7 @@ public class ContentDiskDriverTest extends TestCase final NetworkFile file = driver.createFile(testSession, testConnection, params); assertNotNull("file is null", file); assertFalse("file is read only, should be read-write", file.isReadOnly()); + assertFalse("file is not closed ", file.isClosed()); RetryingTransactionCallback writeStuffCB = new RetryingTransactionCallback() { @@ -643,6 +644,7 @@ public class ContentDiskDriverTest extends TestCase NetworkFile file = driver.openFile(testSession, testConnection, params); assertNotNull(file); + assertFalse("file is closed", file.isClosed()); /** * Step 3: Open the root directory. @@ -652,6 +654,7 @@ public class ContentDiskDriverTest extends TestCase FileOpenParams rootParams = new FileOpenParams("\\", FileAction.CreateNotExist, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); NetworkFile file3 = driver.openFile(testSession, testConnection, rootParams); assertNotNull(file3); + assertFalse("file is closed", file3.isClosed()); } // testOpenFile @@ -3216,8 +3219,223 @@ public class ContentDiskDriverTest extends TestCase } + /** + * Excel 2003 CSV file with Versionable file + * + * CreateFile csv.csv and 5EE27101 + * Add versionable aspect + * RenameFile oldPath:\Espaces Utilisateurs\System\csv.csv, newPath:\Espaces\Utilisateurs\System\5EE27101 + * CreateFile name=\Espaces Utilisateurs\System\csv.csv + * Add content + */ + public void testCSVExcel2003SaveShuffle() throws Exception + { + logger.debug("testCSVExcel2003SaveShuffle"); + final String FILE_NAME = "csv.csv"; + final String FILE_TITLE = "csv"; + final String FILE_DESCRIPTION = "This is a test document to test CIFS shuffle"; + final String FILE_TEMP = "AAAA0000"; + class TestContext + { + NetworkFile firstFileHandle; + NetworkFile newFileHandle; + NodeRef testNodeRef; + + Serializable testCreatedDate; + } + ; + + final TestContext testContext = new TestContext(); + + final String TEST_DIR = TEST_ROOT_DOS_PATH + "\\testMSExcel2003CSVShuffle"; + + 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 from a previous run + */ + RetryingTransactionCallback deleteGarbageFileCB = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + try + { + tran.doInTransaction(deleteGarbageFileCB); + } + catch (Exception e) + { + // expect to go here + } + + /** + * Create a file in the test directory + */ + RetryingTransactionCallback createTestFileFirstTime = 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); + + testContext.testNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + + nodeService.setProperty(testContext.testNodeRef, ContentModel.PROP_TITLE, FILE_TITLE); + nodeService.setProperty(testContext.testNodeRef, ContentModel.PROP_DESCRIPTION, FILE_DESCRIPTION); + + return null; + } + }; + tran.doInTransaction(createTestFileFirstTime, false, true); + + /** + * Write some content to the test file. Add versionable aspect + */ + RetryingTransactionCallback writeToTestFileAndAddVersionableAspect = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + String testContent = "MS Excel 2003 for CSV shuffle test"; + byte[] testContentBytes = testContent.getBytes(); + testContext.firstFileHandle.writeFile(testContentBytes, testContentBytes.length, 0, 0); + testContext.firstFileHandle.close(); + + testContext.testCreatedDate = nodeService.getProperty(testContext.testNodeRef, ContentModel.PROP_CREATED); + + nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + return null; + } + }; + tran.doInTransaction(writeToTestFileAndAddVersionableAspect, false, true); + + /** + * rename the test file to the temp + */ + RetryingTransactionCallback renameTestFileToTemp = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME, TEST_DIR + "\\" + FILE_TEMP); + return null; + } + }; + tran.doInTransaction(renameTestFileToTemp, false, true); + + /** + * create the test file one more + */ + RetryingTransactionCallback createTestFileOneMore = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + + FileOpenParams params = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, FileAction.TruncateExisting, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + NetworkFile file = driver.createFile(testSession, testConnection, params); + driver.closeFile(testSession, testConnection, file); + + return null; + } + }; + tran.doInTransaction(createTestFileOneMore, false, true); + + /** + * Write the new content + */ + RetryingTransactionCallback writeUpdate = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.newFileHandle = driver.openFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.newFileHandle); + String testContent = "MS Word 2003 for CSV shuffle test This is new content"; + byte[] testContentBytes = testContent.getBytes(); + testContext.newFileHandle.writeFile(testContentBytes, testContentBytes.length, 0, 0); + testContext.newFileHandle.close(); + + return null; + } + }; + tran.doInTransaction(writeUpdate, false, true); + + // Check results + RetryingTransactionCallback validate = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + + // Check versionable aspect, version label and nodeRef + assertTrue("VERSIONABLE aspect not present", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + assertEquals("nodeRef changed", testContext.testNodeRef, shuffledNodeRef); + + // Check the titled aspect is correct + assertEquals("name wrong", FILE_NAME, nodeService.getProperty(shuffledNodeRef, ContentModel.PROP_NAME)); + assertEquals("title wrong", FILE_TITLE, nodeService.getProperty(shuffledNodeRef, ContentModel.PROP_TITLE)); + assertEquals("description wrong", FILE_DESCRIPTION, nodeService.getProperty(shuffledNodeRef, ContentModel.PROP_DESCRIPTION)); + + return null; + } + }; + + tran.doInTransaction(validate, true, true); + + /** + * Clean up just in case garbage is left from a previous run + */ + RetryingTransactionCallback deleteTestFile = new RetryingTransactionCallback() + { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + tran.doInTransaction(deleteTestFile, false, true); + + } /** * Simulates a SaveAs from Word2003 @@ -3468,9 +3686,12 @@ public class ContentDiskDriverTest extends TestCase { logger.debug("create file and close it immediatly"); FileOpenParams createFileParams = new FileOpenParams(FILE_PATH, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); - NetworkFile dummy = driver.createFile(testSession, testConnection, createFileParams); + NetworkFile dummy = driver.createFile(testSession, testConnection, createFileParams); + assertFalse("file is closed after create", dummy.isClosed()); driver.closeFile(testSession, testConnection, dummy); logger.debug("after create and close"); +// TODO Bug in JavaNetworkFile +// assertTrue("file is not closed after close", dummy.isClosed()); return null; } }; @@ -3487,6 +3708,7 @@ public class ContentDiskDriverTest extends TestCase logger.debug("open file1 read only"); NetworkFile file1 = driver.openFile(testSession, testConnection, openRO); assertNotNull(file1); + assertFalse("file1 is closed", file1.isClosed()); final String testString = "Yankee doodle went to town"; byte[] stuff = testString.getBytes("UTF-8"); @@ -3507,6 +3729,7 @@ public class ContentDiskDriverTest extends TestCase logger.debug("open file 2 for read write"); NetworkFile file2 = driver.openFile(testSession, testConnection, openRW); assertNotNull(file2); + assertFalse("file is closed", file2.isClosed()); /** * Write Some Content @@ -3531,8 +3754,8 @@ public class ContentDiskDriverTest extends TestCase /** * Step 2: Negative test - Close the file again - should do nothing quietly! */ -// logger.debug("this is a negative test - should do nothing"); -// driver.closeFile(testSession, testConnection, file1); + logger.debug("this is a negative test - should do nothing"); + driver.closeFile(testSession, testConnection, file1); logger.debug("now validate"); @@ -5080,6 +5303,197 @@ public class ContentDiskDriverTest extends TestCase } // testScenarioMSPowerpoint2011MacSaveShuffle + /** + * Simulates a Save from Excel 2011 Mac + * 0. FileA.xlsx already exists. + * 1. Create new document ._A8A09200 + * 2. Delete FileA.xlsx + * 3. Rename ._A8A09200 to FileA.xlsx + */ + public void testScenarioMSExcel2011MacSaveShuffle() throws Exception + { + logger.debug("testScenarioMSExcel2011MacSaveShuffle("); + + final String FILE_NAME = "FileA.xlsx"; + final String FILE_NEW_TEMP = "._A8A09200"; + + class TestContext + { + NetworkFile firstFileHandle; + }; + + final TestContext testContext = new TestContext(); + + final String TEST_DIR = TEST_ROOT_DOS_PATH + "\\testScenarioMSExcel2011MacSaveShuffle"; + + ServerConfiguration scfg = new ServerConfiguration("testServer"); + TestServer testServer = new TestServer("testServer", scfg); + final SrvSession testSession = new TestSrvSession(666, testServer, "cifs", "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; + } + }; + + /** + * Create a file in the test directory + */ + + try + { + logger.debug("expect to get exception - cleaning garbage"); + tran.doInTransaction(deleteGarbageFileCB); + } + catch (Exception e) + { + // expect to go here + } + + logger.debug("0) create new file"); + RetryingTransactionCallback createFileCB = 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 (FileA.xlsx) + */ + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.firstFileHandle); + + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + + NodeRef file1NodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + nodeService.addAspect(file1NodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + return null; + } + }; + tran.doInTransaction(createFileCB, false, true); + + /** + * b) Save the new file + * Write ContentDiskDriverTest3.doc to the test file, + */ + logger.debug("b) write some content"); + RetryingTransactionCallback writeFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NEW_TEMP, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + + ClassPathResource fileResource = new ClassPathResource("filesys/ContentDiskDriverTest3.doc"); + assertNotNull("unable to find test resource filesys/ContentDiskDriverTest3.doc", fileResource); + + byte[] buffer= new byte[1000]; + InputStream is = fileResource.getInputStream(); + try + { + long offset = 0; + int i = is.read(buffer, 0, buffer.length); + while(i > 0) + { + testContext.firstFileHandle.writeFile(buffer, i, 0, offset); + offset += i; + i = is.read(buffer, 0, buffer.length); + } + } + finally + { + is.close(); + } + + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + + return null; + } + }; + tran.doInTransaction(writeFileCB, false, true); + + /** + * c) delete the old file + */ + logger.debug("c) delete old file"); + RetryingTransactionCallback renameOldFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + tran.doInTransaction(renameOldFileCB, false, true); + + /** + * d) Move the new file into place, stuff should get shuffled + */ + logger.debug("d) rename new file into place"); + RetryingTransactionCallback moveNewFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NEW_TEMP, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + tran.doInTransaction(moveNewFileCB, false, true); + + logger.debug("e) validate results"); + /** + * Now validate everything is correct + */ + RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + + Map props = nodeService.getProperties(shuffledNodeRef); + + ContentData data = (ContentData)props.get(ContentModel.PROP_CONTENT); + assertNotNull("data is null", data); + assertEquals("size is wrong", 26112, data.getSize()); + assertEquals("mimeType is wrong", "application/msword",data.getMimetype()); + + assertTrue("versionable aspect missing", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + assertTrue("hidden aspect still applied", !nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_HIDDEN)); + assertTrue("temporary aspect still applied", !nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_TEMPORARY)); + + return null; + } + }; + + tran.doInTransaction(validateCB, true, true); + + } // testScenarioMSExcel2011MacSaveShuffle + /** * This test tries to simulate the cifs shuffling that is done to * support MS Word 2011 on Mac with backup turned on. diff --git a/source/java/org/alfresco/filesys/repo/LegacyFileStateDriver.java b/source/java/org/alfresco/filesys/repo/LegacyFileStateDriver.java index e2279e3ed6..41071af4d1 100644 --- a/source/java/org/alfresco/filesys/repo/LegacyFileStateDriver.java +++ b/source/java/org/alfresco/filesys/repo/LegacyFileStateDriver.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.Date; import org.alfresco.filesys.alfresco.ExtendedDiskInterface; +import org.alfresco.filesys.alfresco.NetworkFileLegacyReferenceCount; import org.alfresco.filesys.config.ServerConfigurationBean; import org.alfresco.jlan.server.SrvSession; import org.alfresco.jlan.server.core.DeviceContext; @@ -38,6 +39,8 @@ import org.alfresco.jlan.server.filesys.cache.FileState; import org.alfresco.jlan.server.filesys.cache.FileStateCache; import org.alfresco.jlan.server.filesys.cache.NetworkFileStateInterface; import org.alfresco.jlan.server.filesys.pseudo.PseudoFile; +import org.alfresco.jlan.server.locking.FileLockingInterface; +import org.alfresco.jlan.server.locking.LockManager; import org.alfresco.jlan.server.locking.OpLockInterface; import org.alfresco.jlan.server.locking.OpLockManager; import org.alfresco.jlan.smb.SharingMode; @@ -86,12 +89,15 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface { ContentContext tctx = (ContentContext) tree.getContext(); + FileStateCache cache = null; + FileState fstate = null; + FileAccessToken token = null; if(tctx.hasStateCache()) { - FileStateCache cache = tctx.getStateCache(); - FileState fstate = tctx.getStateCache().findFileState( params.getPath(), true); + cache = tctx.getStateCache(); + fstate = tctx.getStateCache().findFileState( params.getPath(), true); token = cache.grantFileAccess(params, fstate, FileStatus.NotExist); if(logger.isDebugEnabled()) { @@ -103,11 +109,18 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface { NetworkFile newFile = diskInterface.createFile(sess, tree, params); - newFile.setAccessToken(token); + int openCount = 1; + + if(newFile instanceof NetworkFileLegacyReferenceCount) + { + NetworkFileLegacyReferenceCount counter = (NetworkFileLegacyReferenceCount)newFile; + openCount = counter.incrementLegacyOpenCount(); + } + // This is the create so we store the first access token always + newFile.setAccessToken(token); if(tctx.hasStateCache()) { - FileState fstate = tctx.getStateCache().findFileState( params.getPath(), true); fstate.setProcessId(params.getProcessId()); fstate.setSharedAccess( params.getSharedAccess()); @@ -158,9 +171,7 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface } if(tctx.hasStateCache() && token != null) { - FileStateCache cache = tctx.getStateCache(); - FileState fstate = tctx.getStateCache().findFileState( params.getPath(), false); - if(fstate != null && token != null) + if(cache != null && fstate != null && token != null) { if(logger.isDebugEnabled()) { @@ -171,6 +182,7 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface } throw ie; } + // TODO what about other throwables ? } @Override @@ -179,30 +191,90 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface { ContentContext tctx = (ContentContext) tree.getContext(); String path = params.getPath(); + + boolean rollbackOpen = false; + boolean rollbackToken = false; + boolean rollbackCount = false; + boolean rollbackSetToken = false; FileAccessToken token = null; + + FileStateCache cache = null; + FileState fstate = null; + NetworkFile openFile = null; if(tctx.hasStateCache()) { + cache = tctx.getStateCache(); + fstate = tctx.getStateCache().findFileState( params.getPath(), true); + if(!params.isDirectory()) { - FileStateCache cache = tctx.getStateCache(); - FileState fstate = tctx.getStateCache().findFileState( params.getPath(), true); - token = cache.grantFileAccess(params, fstate, FileStatus.Unknown); + try + { + token = cache.grantFileAccess(params, fstate, FileStatus.Unknown); + } + catch (IOException e) + { + if(logger.isDebugEnabled()) + { + logger.debug("UNABLE to grant file access for path:" + path + ", params" + params, e); + } + throw e; + } + + rollbackToken = true; if(logger.isDebugEnabled()) { - logger.debug("open file created lock token:" + token); + logger.debug("open file created lock token:" + token + ", for path:" + path); } } } try { - NetworkFile openFile = diskInterface.openFile(sess, tree, params); + openFile = diskInterface.openFile(sess, tree, params); + rollbackOpen = true; + + if(openFile instanceof NetworkFileLegacyReferenceCount) + { + NetworkFileLegacyReferenceCount counter = (NetworkFileLegacyReferenceCount)openFile; + int legacyOpenCount = counter.incrementLegacyOpenCount(); + if(logger.isDebugEnabled()) + { + logger.debug("openFile: legacyOpenCount: " + legacyOpenCount); + } + + rollbackCount = true; + } + else + { + logger.debug("openFile does not implement NetworkFileLegacyReferenceCount"); + } - openFile.setAccessToken(token); - - FileState fstate = null; + if( openFile.hasAccessToken()) + { + // already has an access token, release the second token + if(cache != null && fstate != null && token != null) + { + if(logger.isDebugEnabled()) + { + logger.debug("already has access token, release lock token:" + token); + } + cache.releaseFileAccess(fstate, token); + } + } + else + { + if(logger.isDebugEnabled()) + { + logger.debug("store access token on open network file object token:" + token); + } + + // first access token + openFile.setAccessToken(token); + rollbackSetToken = true; + } if(tctx.hasStateCache()) { @@ -257,20 +329,28 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface fstate.setFileStatus(FileStatus.FileExists); } } + + rollbackToken = false; + rollbackCount = false; + rollbackSetToken = false; + rollbackOpen = false; + + if(logger.isDebugEnabled()) + { + logger.debug("successfully opened file:" + openFile); + } return openFile; } - catch(IOException ie) + finally { - if(logger.isDebugEnabled()) + if(rollbackToken) { - logger.debug("open file exception caught", ie); - } - if(tctx.hasStateCache() && token != null) - { - FileStateCache cache = tctx.getStateCache(); - FileState fstate = tctx.getStateCache().findFileState( params.getPath(), false); - if(fstate != null) + if(logger.isDebugEnabled()) + { + logger.debug("rollback token:" + token); + } + if(cache != null && fstate != null && token != null) { if(logger.isDebugEnabled()) { @@ -279,55 +359,139 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface cache.releaseFileAccess(fstate, token); } } - throw ie; + if(rollbackCount) + { + if(logger.isDebugEnabled()) + { + logger.debug("rollback legacy open count:" + token); + } + if(openFile instanceof NetworkFileLegacyReferenceCount) + { + NetworkFileLegacyReferenceCount counter = (NetworkFileLegacyReferenceCount)openFile; + counter.decrementLagacyOpenCount(); + } + } + if(rollbackSetToken) + { + if(logger.isDebugEnabled()) + { + logger.debug("rollback set access token:" + token); + } + openFile.setAccessToken(null); + } + if(rollbackOpen) + { + if(logger.isDebugEnabled()) + { + logger.debug("rollback open:" + token); + } + diskInterface.closeFile(sess, tree, openFile); + } } - } @Override public void closeFile(SrvSession sess, TreeConnection tree, - NetworkFile param) throws IOException + NetworkFile file) throws IOException { ContentContext tctx = (ContentContext) tree.getContext(); - + FileStateCache cache = null; + FileState fstate = null; + if(logger.isDebugEnabled()) { - logger.debug("closeFile:" + param.getFullName() + ", accessToken:" + param.getAccessToken()); + logger.debug("closeFile:" + file.getFullName() + ", accessToken:" + file.getAccessToken()); } - try + int legacyOpenCount = 0; + + if(file instanceof NetworkFileLegacyReferenceCount) { - if ( param.hasOpLock()) { - + NetworkFileLegacyReferenceCount counter = (NetworkFileLegacyReferenceCount)file; + legacyOpenCount = counter.decrementLagacyOpenCount(); + if(logger.isDebugEnabled()) + { + logger.debug("closeFile: legacyOpenCount=" + legacyOpenCount); + } + } + else + { + logger.debug("file to close does not implement NetworkFileLegacyReferenceCount"); + } + + try + { + if ( file.hasOpLock()) + { + if ( logger.isDebugEnabled()) + { + logger.debug("File Has OpLock - release oplock for closed file, file=" + file.getFullName()); + } // Release the oplock OpLockInterface flIface = (OpLockInterface) this; OpLockManager oplockMgr = flIface.getOpLockManager(sess, tree); - oplockMgr.releaseOpLock( param.getOpLock().getPath()); + oplockMgr.releaseOpLock( file.getOpLock().getPath()); // DEBUG if ( logger.isDebugEnabled()) - logger.debug("Released oplock for closed file, file=" + param.getFullName()); + { + logger.debug("Released oplock for closed file, file=" + file.getFullName()); + } + } + + + // Release any locks on the file owned by this session + + if ( file.hasLocks()) + { + if ( logger.isDebugEnabled()) + { + logger.debug("Release all locks, file=" + file.getFullName()); + } + + // Get the lock manager + FileLockingInterface flIface = (FileLockingInterface) this; + LockManager lockMgr = flIface.getLockManager(sess, tree); + + if(lockMgr != null) + { + if(logger.isDebugEnabled()) + { + logger.debug("Releasing locks for closed file, file=" + file.getFullName() + ", locks=" + file.numberOfLocks()); + } + // Release all locks on the file owned by this session + + lockMgr.releaseLocksForFile(sess, tree, file); + } } - diskInterface.closeFile(sess, tree, param); - + diskInterface.closeFile(sess, tree, file); + + logger.debug("file closed"); + + } + finally + { if(tctx.hasStateCache()) { - FileStateCache cache = tctx.getStateCache(); - FileState fstate = cache.findFileState( param.getFullName(), true); - - if(fstate != null && param.getAccessToken() != null) + cache = tctx.getStateCache(); + fstate = cache.findFileState( file.getFullName(), true); + + if(legacyOpenCount == 0 || file.isForce()) { - FileAccessToken token = param.getAccessToken(); - if(logger.isDebugEnabled() && token != null) + if(cache != null && fstate != null && file.getAccessToken() != null) { - logger.debug("close file, release access token:" + token); + FileAccessToken token = file.getAccessToken(); + if(logger.isDebugEnabled() && token != null) + { + logger.debug("close file, legacy count == 0 release access token:" + token); + } + cache.releaseFileAccess(fstate, token); + file.setAccessToken( null); } - cache.releaseFileAccess(fstate, token); - } if(fstate.getOpenCount() == 0 ) @@ -338,20 +502,8 @@ public class LegacyFileStateDriver implements ExtendedDiskInterface fstate.updateChangeDateTime(0); fstate.updateModifyDateTime(0); } - - // Clear the access token - - param.setAccessToken( null); } } - catch(IOException ie) - { - if(logger.isDebugEnabled()) - { - logger.debug("close file exception caught", ie); - } - throw ie; - } } @Override diff --git a/source/java/org/alfresco/filesys/repo/NodeRefNetworkFile.java b/source/java/org/alfresco/filesys/repo/NodeRefNetworkFile.java index f3561557bb..aa8822b8ca 100644 --- a/source/java/org/alfresco/filesys/repo/NodeRefNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/NodeRefNetworkFile.java @@ -18,6 +18,7 @@ package org.alfresco.filesys.repo; import org.alfresco.filesys.alfresco.AlfrescoNetworkFile; +import org.alfresco.filesys.alfresco.NetworkFileLegacyReferenceCount; import org.alfresco.service.cmr.repository.NodeRef; @@ -26,7 +27,9 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author gkspencer */ -public abstract class NodeRefNetworkFile extends AlfrescoNetworkFile { +public abstract class NodeRefNetworkFile extends AlfrescoNetworkFile + implements NetworkFileLegacyReferenceCount +{ // Associated node ref @@ -129,4 +132,35 @@ public abstract class NodeRefNetworkFile extends AlfrescoNetworkFile { public final int getOpenCount() { return m_openCount; } + + private int legacyOpenCount = 0; + + /** + * Increment the legacy file open count + * + * @return int + */ + public synchronized final int incrementLegacyOpenCount() { + legacyOpenCount++; + return legacyOpenCount; + } + + /** + * Decrement the legacy file open count + * + * @return int + */ + public synchronized final int decrementLagacyOpenCount() { + legacyOpenCount--; + return legacyOpenCount; + } + + /** + * Return the legacy open file count + * + * @return int + */ + public final int getLegacyOpenCount() { + return legacyOpenCount; + } } diff --git a/source/java/org/alfresco/filesys/repo/TempNetworkFile.java b/source/java/org/alfresco/filesys/repo/TempNetworkFile.java index 7e608bac09..4744f51714 100644 --- a/source/java/org/alfresco/filesys/repo/TempNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/TempNetworkFile.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.io.Reader; +import org.alfresco.filesys.alfresco.NetworkFileLegacyReferenceCount; import org.alfresco.jlan.server.filesys.FileAttribute; import org.alfresco.jlan.server.filesys.cache.FileState; import org.alfresco.jlan.server.filesys.cache.NetworkFileStateInterface; @@ -14,11 +15,15 @@ import org.alfresco.jlan.smb.server.disk.JavaNetworkFile; * * @author mrogers */ -public class TempNetworkFile extends JavaNetworkFile implements NetworkFileStateInterface +public class TempNetworkFile extends JavaNetworkFile + implements NetworkFileStateInterface, + NetworkFileLegacyReferenceCount { private boolean changed = false; boolean modificationDateSetDirectly = false; + + /** * Create a new temporary file with no existing content. * @@ -171,6 +176,37 @@ public class TempNetworkFile extends JavaNetworkFile implements NetworkFileState this.modificationDateSetDirectly = modificationDateSetDirectly; } + private int legacyOpenCount = 0; + + /** + * Increment the legacy file open count + * + * @return int + */ + public synchronized final int incrementLegacyOpenCount() { + legacyOpenCount++; + return legacyOpenCount; + } + + /** + * Decrement the legacy file open count + * + * @return int + */ + public synchronized final int decrementLagacyOpenCount() { + legacyOpenCount--; + return legacyOpenCount; + } + + /** + * Return the legacy open file count + * + * @return int + */ + public final int getLegacyOpenCount() { + return legacyOpenCount; + } + private FileState fileState; } diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java index 72313a55b2..a583c1152a 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioCreateDeleteRenameShuffleInstance.java @@ -175,12 +175,27 @@ public class ScenarioCreateDeleteRenameShuffleInstance implements ScenarioInstan /** * For Mac 2011 powerpoint files - add an extra check based on the filename * e.g FileA1 - delete FileA. + * For Mac 2011 excel files - check that file to delete is not temporary */ if(checkFilename) { + boolean isRightTarget = false; int i = deleteName.lastIndexOf('.'); - if((i > 0) && (i < createName.length()) && deleteName.substring(0, i).equalsIgnoreCase(createName.substring(0,i))) + if (i > 0) + { + String extension = deleteName.substring(i+1,deleteName.length()); + if (extension.startsWith("ppt")) + { + isRightTarget = (i < createName.length()) && deleteName.substring(0, i).equalsIgnoreCase(createName.substring(0,i)); + } + else if (extension.startsWith("xls")) + { + isRightTarget = !deleteName.startsWith("._") && !deleteName.startsWith("~$"); + } + } + + if (isRightTarget) { logger.debug("check filenames - does match"); } diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffleInstance.java index 833b153cf7..80d764d68e 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffleInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioDoubleRenameShuffleInstance.java @@ -101,6 +101,8 @@ public class ScenarioDoubleRenameShuffleInstance implements ScenarioInstance logger.debug("Instance timed out"); } + isComplete = true; + return null; } switch (internalState) diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java index 68caa96acd..faccb428fa 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java @@ -257,7 +257,7 @@ class ScenarioOpenFileInstance implements ScenarioInstance, DependentInstance if(isReadOnly(file)) { // Read Only File - if(openReadOnlyCount == 1 || c.isForce()) + if(openReadOnlyCount == 1) { if(logger.isDebugEnabled()) { @@ -304,7 +304,7 @@ class ScenarioOpenFileInstance implements ScenarioInstance, DependentInstance { // This is a close of a Read Write File // Read Only File - if(openReadWriteCount == 1 || c.isForce()) + if(openReadWriteCount == 1) { if(logger.isDebugEnabled()) { diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffle.java new file mode 100644 index 0000000000..bdc08886f5 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffle.java @@ -0,0 +1,88 @@ +package org.alfresco.filesys.repo.rules; + +import java.util.List; +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.RenameFileOperation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ScenarioRenameCreateShuffle implements Scenario +{ + private static Log logger = LogFactory.getLog(ScenarioRenameCreateShuffle.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; + + @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 Rename Shuffle Create Instance strPattern:" + pattern); + } + ScenarioRenameCreateShuffleInstance instance = new ScenarioRenameCreateShuffleInstance(); + instance.setTimeout(timeout); + instance.setRanking(ranking); + return instance; + } + } + + // No not interested. + return null; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public long getTimeout() + { + return timeout; + } + + public void setPattern(String pattern) + { + this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + this.strPattern = pattern; + } + + public String getPattern() + { + return strPattern; + } + + private Ranking ranking = Ranking.HIGH; + + public void setRanking(Ranking ranking) + { + this.ranking = ranking; + } + + public Ranking getRanking() + { + return ranking; + } +} + + diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffleInstance.java new file mode 100644 index 0000000000..317f78198e --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameCreateShuffleInstance.java @@ -0,0 +1,130 @@ +package org.alfresco.filesys.repo.rules; + + +import java.util.ArrayList; +import java.util.Date; + +import org.alfresco.filesys.repo.rules.ScenarioInstance.Ranking; +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.commands.SoftRenameFileCommand; +import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; +import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; +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; + +/** + * A rename create scenario is ... + * + * a) Rename File to File~ + * b) Create File + * + * This rule will kick in and copy the content and then switch the two file over. + * + */ +class ScenarioRenameCreateShuffleInstance implements ScenarioInstance +{ + private static Log logger = LogFactory.getLog(ScenarioRenameShuffleInstance.class); + + private Date startTime = new Date(); + + /** + * Timeout in ms. Default 30 seconds. + */ + private long timeout = 30000; + + private boolean isComplete = false; + + private Ranking ranking = Ranking.HIGH; + + enum InternalState + { + NONE, + INITIALISED, + LOOK_FOR_DELETE + } ; + + InternalState state = InternalState.NONE; + + String from; + String to; + + /** + * 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 (state) + { + case NONE: + if(operation instanceof RenameFileOperation) + { + logger.debug("New scenario initialised"); + RenameFileOperation r = (RenameFileOperation)operation; + this.from = r.getFrom(); + this.to = r.getTo(); + state = InternalState.INITIALISED; + + SoftRenameFileCommand r1 = new SoftRenameFileCommand(from, to, r.getRootNodeRef(), r.getFromPath(), r.getToPath()); + isComplete = true; + return r1; + } + break; + + } + + + return null; + } + + @Override + public boolean isComplete() + { + return isComplete; + } + + public String toString() + { + return "ScenarioRenameCreateShuffleInstance from:" + from + " to:" + to; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public long getTimeout() + { + return timeout; + } + + @Override + public Ranking getRanking() + { + return ranking; + } + + public void setRanking(Ranking ranking) + { + this.ranking = ranking; + } +} diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java index 1828795c38..064b2598a0 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffle.java @@ -110,4 +110,6 @@ public class ScenarioRenameShuffle implements Scenario { return ranking; } + + } diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffleInstance.java index 701e3e8e7c..0158465322 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffleInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioRenameShuffleInstance.java @@ -86,6 +86,8 @@ class ScenarioRenameShuffleInstance implements ScenarioInstance { logger.debug("Instance timed out"); } + isComplete = true; + return null; } switch (state) diff --git a/source/java/org/alfresco/filesys/repo/rules/commands/RenameFileCommand.java b/source/java/org/alfresco/filesys/repo/rules/commands/RenameFileCommand.java index 3b41680418..929b4ebff5 100644 --- a/source/java/org/alfresco/filesys/repo/rules/commands/RenameFileCommand.java +++ b/source/java/org/alfresco/filesys/repo/rules/commands/RenameFileCommand.java @@ -33,6 +33,7 @@ public class RenameFileCommand implements Command private NodeRef rootNode; private String fromPath; private String toPath; + private boolean isSoft = false; public RenameFileCommand(String from, String to, NodeRef rootNode, String fromPath, String toPath) { @@ -96,4 +97,9 @@ public class RenameFileCommand implements Command { return toPath; } + + public boolean isSoft() + { + return isSoft; + } } diff --git a/source/java/org/alfresco/filesys/repo/rules/commands/SoftRenameFileCommand.java b/source/java/org/alfresco/filesys/repo/rules/commands/SoftRenameFileCommand.java new file mode 100644 index 0000000000..32eb9045d9 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/commands/SoftRenameFileCommand.java @@ -0,0 +1,19 @@ +package org.alfresco.filesys.repo.rules.commands; + +import org.alfresco.service.cmr.repository.NodeRef; + +public class SoftRenameFileCommand extends RenameFileCommand +{ + + public SoftRenameFileCommand(String from, String to, NodeRef rootNode, + String fromPath, String toPath) + { + super(from, to, rootNode, fromPath, toPath); + } + + public boolean isSoft() + { + return true; + } + +} diff --git a/source/java/org/alfresco/jcr/exporter/JCRDocumentXMLExporter.java b/source/java/org/alfresco/jcr/exporter/JCRDocumentXMLExporter.java index ee1f161dc8..a4a154ca9d 100644 --- a/source/java/org/alfresco/jcr/exporter/JCRDocumentXMLExporter.java +++ b/source/java/org/alfresco/jcr/exporter/JCRDocumentXMLExporter.java @@ -379,7 +379,7 @@ public class JCRDocumentXMLExporter implements Exporter { } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { } diff --git a/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java b/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java index 8640e86338..eb0b1fe80d 100644 --- a/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java +++ b/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java @@ -439,7 +439,7 @@ public class JCRSystemXMLExporter implements Exporter { } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { } diff --git a/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java b/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java index 372b0c48b4..f035dd9b0d 100644 --- a/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java +++ b/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java @@ -1555,6 +1555,14 @@ public class AlfrescoCmisServiceImpl extends AbstractCmisService implements Alfr break; // Reason for do-while } + if(info.hasPWC()) + { + // is a checked out document. If a delete, don't allow unless checkout is canceled. If a cancel + // checkout, not allowed. + throw new CmisConstraintException( + "Could not delete/cancel checkout on the original checked out document"); + } + // handle versions if (allVersions) { diff --git a/source/java/org/alfresco/opencmis/CMISTest.java b/source/java/org/alfresco/opencmis/CMISTest.java new file mode 100644 index 0000000000..7ab41627c6 --- /dev/null +++ b/source/java/org/alfresco/opencmis/CMISTest.java @@ -0,0 +1,308 @@ +package org.alfresco.opencmis; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.data.AllowableActions; +import org.apache.chemistry.opencmis.commons.data.ObjectData; +import org.apache.chemistry.opencmis.commons.data.PropertyData; +import org.apache.chemistry.opencmis.commons.data.RepositoryInfo; +import org.apache.chemistry.opencmis.commons.enums.Action; +import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships; +import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException; +import org.apache.chemistry.opencmis.commons.impl.server.AbstractServiceFactory; +import org.apache.chemistry.opencmis.commons.server.CallContext; +import org.apache.chemistry.opencmis.commons.server.CmisService; +import org.apache.chemistry.opencmis.commons.spi.Holder; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.GUID; + +/** + * OpenCMIS tests. + * + * @author steveglover + * + */ +public class CMISTest +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private FileFolderService fileFolderService; + private TransactionService transactionService; + private NodeService nodeService; + private Repository repositoryHelper; + + private AlfrescoCmisServiceFactory factory; + private SimpleCallContext context; + + /** + * Test class to provide the service factory + * + * @author Derek Hulley + * @since 4.0 + */ + public static class TestCmisServiceFactory extends AbstractServiceFactory + { + private static AlfrescoCmisServiceFactory serviceFactory = (AlfrescoCmisServiceFactory) ctx.getBean("CMISServiceFactory"); + @Override + public void init(Map parameters) + { + serviceFactory.init(parameters); + } + + @Override + public void destroy() + { + } + + @Override + public CmisService getService(CallContext context) + { + return serviceFactory.getService(context); + } + } + + private static class SimpleCallContext implements CallContext + { + private final Map contextMap = new HashMap(); + + public SimpleCallContext(String user, String password) + { + contextMap.put(USERNAME, user); + contextMap.put(PASSWORD, password); + } + + public String getBinding() + { + return BINDING_LOCAL; + } + + public Object get(String key) + { + return contextMap.get(key); + } + + public String getRepositoryId() + { + return (String) get(REPOSITORY_ID); + } + + public String getUsername() + { + return (String) get(USERNAME); + } + + public String getPassword() + { + return (String) get(PASSWORD); + } + + public String getLocale() + { + return null; + } + + public BigInteger getOffset() + { + return (BigInteger) get(OFFSET); + } + + public BigInteger getLength() + { + return (BigInteger) get(LENGTH); + } + + public boolean isObjectInfoRequired() + { + return false; + } + + public File getTempDirectory() + { + return null; + } + + public int getMemoryThreshold() + { + return 0; + } + + public long getMaxContentSize() + { + return Long.MAX_VALUE; + } + } + + @Before + public void before() + { + this.fileFolderService = (FileFolderService)ctx.getBean("FileFolderService"); + this.transactionService = (TransactionService)ctx.getBean("transactionService"); + this.nodeService = (NodeService)ctx.getBean("NodeService"); + this.repositoryHelper = (Repository)ctx.getBean("repositoryHelper"); + this.factory = (AlfrescoCmisServiceFactory)ctx.getBean("CMISServiceFactory"); + this.context = new SimpleCallContext("admin", "admin"); + } + + /** + * Test for ALF-16310. + * + * Check that, for AtomPub binding, cancel checkout on the originating checked out document i.e. not the working + * copy throws an exception and does not delete the document. + */ + @SuppressWarnings("unchecked") + @Test + public void testCancelCheckout() + { + String repositoryId = null; + ObjectData objectData = null; + Holder objectId = null; + + final String folderName = "testfolder." + GUID.generate(); + final String docName = "testdoc.txt." + GUID.generate(); + + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + try + { + final FileInfo folderInfo = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public FileInfo execute() throws Throwable + { + NodeRef companyHomeNodeRef = repositoryHelper.getCompanyHome(); + + FileInfo folderInfo = fileFolderService.create(companyHomeNodeRef, folderName, ContentModel.TYPE_FOLDER); + nodeService.setProperty(folderInfo.getNodeRef(), ContentModel.PROP_NAME, folderName); + + FileInfo fileInfo = fileFolderService.create(folderInfo.getNodeRef(), docName, ContentModel.TYPE_CONTENT); + nodeService.setProperty(fileInfo.getNodeRef(), ContentModel.PROP_NAME, docName); + + return folderInfo; + } + }); + + CmisService service = factory.getService(context); + try + { + List repositories = service.getRepositoryInfos(null); + assertTrue(repositories.size() > 0); + RepositoryInfo repo = repositories.get(0); + repositoryId = repo.getId(); + objectData = service.getObjectByPath(repositoryId, "/" + folderName + "/" + docName, null, true, + IncludeRelationships.NONE, null, false, true, null); + + // checkout + objectId = new Holder(objectData.getId()); + service.checkOut(repositoryId, objectId, null, new Holder(true)); + } + finally + { + service.close(); + } + + // AtomPub cancel checkout + try + { + service = factory.getService(context); + + // check allowable actions + ObjectData originalDoc = service.getObject(repositoryId, objectData.getId(), null, true, IncludeRelationships.NONE, null, false, true, null); + AllowableActions allowableActions = originalDoc.getAllowableActions(); + assertNotNull(allowableActions); + assertFalse(allowableActions.getAllowableActions().contains(Action.CAN_DELETE_OBJECT)); + + // try to cancel the checkout + service.deleteObjectOrCancelCheckOut(repositoryId, objectData.getId(), Boolean.TRUE, null); + fail(); + } + catch(CmisConstraintException e) + { + // expected + } + finally + { + service.close(); + } + + try + { + service = factory.getService(context); + + // cancel checkout on pwc + service.deleteObjectOrCancelCheckOut(repositoryId, objectId.getValue(), Boolean.TRUE, null); + } + finally + { + service.close(); + } + + try + { + service = factory.getService(context); + + // get original document + ObjectData originalDoc = service.getObject(repositoryId, objectData.getId(), null, true, IncludeRelationships.NONE, null, false, true, null); + Map> properties = originalDoc.getProperties().getProperties(); + PropertyData isVersionSeriesCheckedOutProp = (PropertyData)properties.get(PropertyIds.IS_VERSION_SERIES_CHECKED_OUT); + assertNotNull(isVersionSeriesCheckedOutProp); + Boolean isVersionSeriesCheckedOut = isVersionSeriesCheckedOutProp.getFirstValue(); + assertNotNull(isVersionSeriesCheckedOut); + assertEquals(Boolean.FALSE, isVersionSeriesCheckedOut); + } + finally + { + service.close(); + } + + try + { + service = factory.getService(context); + + // delete original document + service.deleteObject(repositoryId, objectData.getId(), true, null); + } + finally + { + service.close(); + } + + List children = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback>() + { + @Override + public List execute() throws Throwable + { + List children = fileFolderService.list(folderInfo.getNodeRef()); + return children; + } + }); + assertEquals(0, children.size()); + } + finally + { + AuthenticationUtil.popAuthentication(); + } + } +} diff --git a/source/java/org/alfresco/opencmis/mapping/CanDeleteDocumentEvaluator.java b/source/java/org/alfresco/opencmis/mapping/CanDeleteDocumentEvaluator.java new file mode 100644 index 0000000000..78abec70c6 --- /dev/null +++ b/source/java/org/alfresco/opencmis/mapping/CanDeleteDocumentEvaluator.java @@ -0,0 +1,42 @@ +package org.alfresco.opencmis.mapping; + +import org.alfresco.opencmis.dictionary.CMISActionEvaluator; +import org.alfresco.opencmis.dictionary.CMISNodeInfo; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.security.PermissionService; +import org.apache.chemistry.opencmis.commons.enums.Action; + +public class CanDeleteDocumentEvaluator extends AbstractActionEvaluator +{ + private CMISActionEvaluator currentVersionEvaluator; + + /** + * Construct + * + * @param serviceRegistry + */ + protected CanDeleteDocumentEvaluator(ServiceRegistry serviceRegistry) + { + super(serviceRegistry, Action.CAN_DELETE_OBJECT); + this.currentVersionEvaluator = new PermissionActionEvaluator(serviceRegistry, + Action.CAN_DELETE_OBJECT, PermissionService.DELETE_NODE); + } + + public boolean isAllowed(CMISNodeInfo nodeInfo) + { + boolean isAllowed = true; + + if(!nodeInfo.isCurrentVersion() || nodeInfo.hasPWC()) + { + // not allowed if not current version or is checked out + isAllowed = false; + } + else + { + isAllowed = currentVersionEvaluator.isAllowed(nodeInfo); + } + + return isAllowed; + } + +} diff --git a/source/java/org/alfresco/opencmis/mapping/RuntimePropertyAccessorMapping.java b/source/java/org/alfresco/opencmis/mapping/RuntimePropertyAccessorMapping.java index dba52075af..93fbcc88c7 100644 --- a/source/java/org/alfresco/opencmis/mapping/RuntimePropertyAccessorMapping.java +++ b/source/java/org/alfresco/opencmis/mapping/RuntimePropertyAccessorMapping.java @@ -149,9 +149,7 @@ public class RuntimePropertyAccessorMapping implements PropertyAccessorMapping, // order as specified in CMIS-Core.xsd // so that schema validation passes - registerEvaluator(BaseTypeId.CMIS_DOCUMENT, - new CurrentVersionEvaluator(serviceRegistry, new PermissionActionEvaluator(serviceRegistry, - Action.CAN_DELETE_OBJECT, PermissionService.DELETE_NODE), false)); + registerEvaluator(BaseTypeId.CMIS_DOCUMENT, new CanDeleteDocumentEvaluator(serviceRegistry)); registerEvaluator(BaseTypeId.CMIS_DOCUMENT, new CurrentVersionEvaluator(serviceRegistry, new PermissionActionEvaluator(serviceRegistry, Action.CAN_UPDATE_PROPERTIES, PermissionService.WRITE_PROPERTIES), false)); diff --git a/source/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java b/source/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java index 5c48e1dcd8..dd461e912b 100644 --- a/source/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java +++ b/source/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java @@ -54,9 +54,9 @@ public class AspectParameterConstraint extends BaseParameterConstraint for (QName aspect : aspects) { AspectDefinition aspectDef = dictionaryService.getAspect(aspect); - if (aspectDef != null && aspectDef.getTitle() != null) + if (aspectDef != null && aspectDef.getTitle(dictionaryService) != null) { - result.put(aspect.toPrefixString(), aspectDef.getTitle()); + result.put(aspect.toPrefixString(), aspectDef.getTitle(dictionaryService)); } } return result; diff --git a/source/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java b/source/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java index ee473b534e..7c44a145c7 100644 --- a/source/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java +++ b/source/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java @@ -54,9 +54,9 @@ public class PropertyParameterConstraint extends BaseParameterConstraint for (QName property : properties) { PropertyDefinition propertyDef = dictionaryService.getProperty(property); - if (propertyDef != null && propertyDef.getTitle() != null) + if (propertyDef != null && propertyDef.getTitle(dictionaryService) != null) { - result.put(property.toPrefixString(), propertyDef.getTitle()); + result.put(property.toPrefixString(), propertyDef.getTitle(dictionaryService)); } } return result; diff --git a/source/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java b/source/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java index 712eb7c187..45bc6bc447 100644 --- a/source/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java +++ b/source/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java @@ -54,9 +54,9 @@ public class TypeParameterConstraint extends BaseParameterConstraint for (QName type : types) { TypeDefinition typeDef = dictionaryService.getType(type); - if (typeDef != null && typeDef.getTitle() != null) + if (typeDef != null && typeDef.getTitle(dictionaryService) != null) { - result.put(type.toPrefixString(), typeDef.getTitle()); + result.put(type.toPrefixString(), typeDef.getTitle(dictionaryService)); } } return result; diff --git a/source/java/org/alfresco/repo/admin/RepoModelDefinition.java b/source/java/org/alfresco/repo/admin/RepoModelDefinition.java index 472144defb..61554fb0f2 100644 --- a/source/java/org/alfresco/repo/admin/RepoModelDefinition.java +++ b/source/java/org/alfresco/repo/admin/RepoModelDefinition.java @@ -71,7 +71,7 @@ public class RepoModelDefinition sb.append("RepoVersion: " + repoVersion + " , "); sb.append("RepoName: " + repoName + " , "); sb.append("ModelQName: " + (model == null ? "n/a" : model.getName()) + " , "); - sb.append("Description: " + (model == null ? "n/a" : model.getDescription()) + " , "); + sb.append("Description: " + (model == null ? "n/a" : model.getDescription(null)) + " , "); sb.append("Author: " + (model == null ? "n/a" : model.getAuthor()) + " , "); sb.append("Published: " + (model == null ? "n/a" : model.getPublishedDate()) + " , "); sb.append("Version: " + (model == null ? "n/a" : model.getVersion())); diff --git a/source/java/org/alfresco/repo/admin/patch/impl/CopiedFromAspectPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/CopiedFromAspectPatch.java index 6fcab4f8e6..55c2db46cc 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/CopiedFromAspectPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/CopiedFromAspectPatch.java @@ -359,7 +359,8 @@ public class CopiedFromAspectPatch extends AbstractPatch } }; // Keep querying until we have enough results to give back - while (currentId <= maxId) + int minResults = batchMaxQueryRange / 2; + while (currentId <= maxId && results.size() < minResults) { nodeDAO.getNodesWithAspects( aspectQNames, diff --git a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java index fe0e219d52..d744157507 100644 --- a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java +++ b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java @@ -804,6 +804,11 @@ public class CheckOutCheckInServiceImpl implements CheckOutCheckInService name = workingCopyLabel; } } + else + { + throw new IllegalArgumentException("workingCopyLabel is null or empty"); + } + return name; } diff --git a/source/java/org/alfresco/repo/content/transform/TransformerDebug.java b/source/java/org/alfresco/repo/content/transform/TransformerDebug.java index 950869c3d6..bbad7a87b4 100644 --- a/source/java/org/alfresco/repo/content/transform/TransformerDebug.java +++ b/source/java/org/alfresco/repo/content/transform/TransformerDebug.java @@ -643,13 +643,20 @@ public class TransformerDebug { // Trim messages of the form: "Failed... : \n reader:...\n writer:..." String msg = t.getMessage(); - int i = msg.indexOf(": \n"); - if (i != -1) + if (msg != null) { - msg = msg.substring(0, i); + int i = msg.indexOf(": \n"); + if (i != -1) + { + msg = msg.substring(0, i); + } + log(message + ' ' + msg); + } + else + { + log(message); } - log(message + ' ' + msg); Deque ourStack = ThreadInfo.getStack(); if (!ourStack.isEmpty()) diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java b/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java index 7b6700cb81..029c546112 100644 --- a/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java +++ b/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java @@ -675,7 +675,7 @@ public class DictionaryModelType implements ContentServicePolicies.OnContentUpda // Update the meta data for the model Map props = nodeService.getProperties(nodeRef); props.put(ContentModel.PROP_MODEL_NAME, modelDefinition.getName()); - props.put(ContentModel.PROP_MODEL_DESCRIPTION, modelDefinition.getDescription()); + props.put(ContentModel.PROP_MODEL_DESCRIPTION, modelDefinition.getDescription(null)); props.put(ContentModel.PROP_MODEL_AUTHOR, modelDefinition.getAuthor()); props.put(ContentModel.PROP_MODEL_PUBLISHED_DATE, modelDefinition.getPublishedDate()); props.put(ContentModel.PROP_MODEL_VERSION, modelDefinition.getVersion()); diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java b/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java index d85e68102a..005650f16b 100644 --- a/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java +++ b/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java @@ -397,7 +397,7 @@ public class DictionaryModelTypeTest extends BaseAlfrescoSpringTest // Check that the model is now available from the dictionary ModelDefinition modelDefinition2 = DictionaryModelTypeTest.this.dictionaryService.getModel(TEST_MODEL_ONE); assertNotNull(modelDefinition2); - assertEquals("Test model one", modelDefinition2.getDescription()); + assertEquals("Test model one", modelDefinition2.getDescription(DictionaryModelTypeTest.this.dictionaryService)); // Check that the namespace has been added to the namespace service String uri2 = DictionaryModelTypeTest.this.namespaceService.getNamespaceURI("test1"); diff --git a/source/java/org/alfresco/repo/dictionary/RepoDictionaryDAOTest.java b/source/java/org/alfresco/repo/dictionary/RepoDictionaryDAOTest.java index 42bacecd39..2b13b0e892 100644 --- a/source/java/org/alfresco/repo/dictionary/RepoDictionaryDAOTest.java +++ b/source/java/org/alfresco/repo/dictionary/RepoDictionaryDAOTest.java @@ -41,6 +41,7 @@ import org.alfresco.repo.dictionary.constraint.RegexConstraint; import org.alfresco.repo.dictionary.constraint.RegisteredConstraint; import org.alfresco.repo.dictionary.constraint.StringLengthConstraint; import org.alfresco.repo.dictionary.constraint.UserNameConstraint; +import org.alfresco.repo.i18n.StaticMessageLookup; import org.alfresco.repo.tenant.SingleTServiceImpl; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.dictionary.AssociationDefinition; @@ -111,6 +112,7 @@ public class RepoDictionaryDAOTest extends TestCase DictionaryComponent component = new DictionaryComponent(); component.setDictionaryDAO(dictionaryDAO); + component.setMessageLookup(new StaticMessageLookup()); service = component; } @@ -163,19 +165,19 @@ public class RepoDictionaryDAOTest extends TestCase { QName model = QName.createQName(TEST_URL, "dictionarydaotest"); ModelDefinition modelDef = service.getModel(model); - assertEquals("Model Description", modelDef.getDescription()); + assertEquals("Model Description", modelDef.getDescription(service)); QName type = QName.createQName(TEST_URL, "base"); TypeDefinition typeDef = service.getType(type); - assertEquals("Base Title", typeDef.getTitle()); - assertEquals("Base Description", typeDef.getDescription()); + assertEquals("Base Title", typeDef.getTitle(service)); + assertEquals("Base Description", typeDef.getDescription(service)); QName prop = QName.createQName(TEST_URL, "prop1"); PropertyDefinition propDef = service.getProperty(prop); - assertEquals("Prop1 Title", propDef.getTitle()); - assertEquals("Prop1 Description", propDef.getDescription()); + assertEquals("Prop1 Title", propDef.getTitle(service)); + assertEquals("Prop1 Description", propDef.getDescription(service)); QName assoc = QName.createQName(TEST_URL, "assoc1"); AssociationDefinition assocDef = service.getAssociation(assoc); - assertEquals("Assoc1 Title", assocDef.getTitle()); - assertEquals("Assoc1 Description", assocDef.getDescription()); + assertEquals("Assoc1 Title", assocDef.getTitle(service)); + assertEquals("Assoc1 Description", assocDef.getDescription(service)); QName datatype = QName.createQName(TEST_URL, "datatype"); DataTypeDefinition datatypeDef = service.getDataType(datatype); assertEquals("alfresco/model/dataTypeAnalyzers", datatypeDef.getAnalyserResourceBundleName()); @@ -201,15 +203,15 @@ public class RepoDictionaryDAOTest extends TestCase { if (constraintDef.getName().equals(conRegExp1QName)) { - assertEquals("Regex1 title", constraintDef.getTitle()); - assertEquals("Regex1 description", constraintDef.getDescription()); + assertEquals("Regex1 title", constraintDef.getTitle(service)); + assertEquals("Regex1 description", constraintDef.getDescription(service)); found1 = true; } if (constraintDef.getName().equals(conStrLen1QName)) { - assertNull(constraintDef.getTitle()); - assertNull(constraintDef.getDescription()); + assertNull(constraintDef.getTitle(service)); + assertNull(constraintDef.getDescription(service)); found2 = true; } } @@ -236,14 +238,14 @@ public class RepoDictionaryDAOTest extends TestCase assertTrue("Constraint anonymous name incorrect", constraintDef.getName().getLocalName().startsWith("prop1_anon")); // inherit title / description for reference constraint - assertTrue("Constraint title incorrect", constraintDef.getTitle().equals("Regex1 title")); - assertTrue("Constraint description incorrect", constraintDef.getDescription().equals("Regex1 description")); + assertTrue("Constraint title incorrect", constraintDef.getTitle(service).equals("Regex1 title")); + assertTrue("Constraint description incorrect", constraintDef.getDescription(service).equals("Regex1 description")); constraintDef = constraints.get(1); assertTrue("Constraint anonymous name incorrect", constraintDef.getName().getLocalName().startsWith("prop1_anon")); - assertTrue("Constraint title incorrect", constraintDef.getTitle().equals("Prop1 Strlen1 title")); - assertTrue("Constraint description incorrect", constraintDef.getDescription().equals("Prop1 Strlen1 description")); + assertTrue("Constraint title incorrect", constraintDef.getTitle(service).equals("Prop1 Strlen1 title")); + assertTrue("Constraint description incorrect", constraintDef.getDescription(service).equals("Prop1 Strlen1 description")); // check that the constraint implementation is valid (it used a reference) Constraint constraint = constraintDef.getConstraint(); diff --git a/source/java/org/alfresco/repo/download/AbstractExporter.java b/source/java/org/alfresco/repo/download/AbstractExporter.java index 24b0d151d8..2f0cf217e1 100644 --- a/source/java/org/alfresco/repo/download/AbstractExporter.java +++ b/source/java/org/alfresco/repo/download/AbstractExporter.java @@ -109,7 +109,7 @@ public class AbstractExporter implements Exporter } @Override - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { } diff --git a/source/java/org/alfresco/repo/download/BaseExporter.java b/source/java/org/alfresco/repo/download/BaseExporter.java index 96fd5bfdb5..be959d83fa 100644 --- a/source/java/org/alfresco/repo/download/BaseExporter.java +++ b/source/java/org/alfresco/repo/download/BaseExporter.java @@ -144,7 +144,7 @@ abstract class BaseExporter implements Exporter } @Override - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { } diff --git a/source/java/org/alfresco/repo/exporter/ChainedExporter.java b/source/java/org/alfresco/repo/exporter/ChainedExporter.java index 508f71b54d..cc1693635a 100644 --- a/source/java/org/alfresco/repo/exporter/ChainedExporter.java +++ b/source/java/org/alfresco/repo/exporter/ChainedExporter.java @@ -346,11 +346,11 @@ import org.alfresco.service.namespace.QName; } } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { for (Exporter exporter : exporters) { - exporter.startValueMLText(nodeRef, locale); + exporter.startValueMLText(nodeRef, locale, isNull); } } diff --git a/source/java/org/alfresco/repo/exporter/ExporterComponent.java b/source/java/org/alfresco/repo/exporter/ExporterComponent.java index 99dcceb6db..f9aa1e6953 100644 --- a/source/java/org/alfresco/repo/exporter/ExporterComponent.java +++ b/source/java/org/alfresco/repo/exporter/ExporterComponent.java @@ -485,12 +485,7 @@ public class ExporterComponent for (Locale locale : locales) { String localeValue = valueMLT.getValue(locale); - if (localeValue == null) - { - walkProperty(nodeRef, property, localeValue, -1, parameters, exporter); - continue; - } - exporter.startValueMLText(nodeRef, locale); + exporter.startValueMLText(nodeRef, locale, localeValue == null); walkProperty(nodeRef, property, localeValue, -1, parameters, exporter); exporter.endValueMLText(nodeRef); } diff --git a/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java b/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java index 98ea6bfe3a..a0b911f341 100644 --- a/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java +++ b/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java @@ -42,6 +42,7 @@ import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.MLText; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; @@ -55,6 +56,7 @@ import org.alfresco.service.cmr.view.ImportPackageHandler; import org.alfresco.service.cmr.view.ImporterService; import org.alfresco.service.cmr.view.Location; import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.BaseSpringTest; import org.alfresco.util.TempFileProvider; import org.alfresco.util.debug.NodeStoreInspector; @@ -69,6 +71,7 @@ public class ExporterComponentTest extends BaseSpringTest private ImporterService importerService; private FileFolderService fileFolderService; private CategoryService categoryService; + private TransactionService transactionService; private StoreRef storeRef; private AuthenticationComponent authenticationComponent; @@ -81,7 +84,8 @@ public class ExporterComponentTest extends BaseSpringTest importerService = (ImporterService)applicationContext.getBean("importerComponent"); fileFolderService = (FileFolderService) applicationContext.getBean("fileFolderService"); categoryService = (CategoryService) applicationContext.getBean("categoryService"); - + transactionService = (TransactionService) applicationContext.getBean("transactionService"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); this.authenticationComponent.setSystemUserAsCurrentUser(); this.storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); @@ -142,7 +146,7 @@ public class ExporterComponentTest extends BaseSpringTest ChildAssociationRef contentChildAssocRef = createContentWithCategories(storeRef, rootNode); // Export/import - File acpFile = exportContent(contentChildAssocRef); + File acpFile = exportContent(contentChildAssocRef.getParentRef()); FileInfo importFolderFileInfo = importContent(acpFile, rootNode); // Check categories @@ -171,7 +175,7 @@ public class ExporterComponentTest extends BaseSpringTest ChildAssociationRef contentChildAssocRef = createContentWithCategories(storeRef, rootNode); // Export - File acpFile = exportContent(contentChildAssocRef); + File acpFile = exportContent(contentChildAssocRef.getParentRef()); // Import - destination store is different from export store. NodeRef destRootNode = nodeService.getRootNode(this.storeRef); FileInfo importFolderFileInfo = importContent(acpFile, destRootNode); @@ -184,6 +188,54 @@ public class ExporterComponentTest extends BaseSpringTest nodeService.getProperty(importedFileNode, ContentModel.PROP_CATEGORIES); assertEquals("No categories should have been imported for the content", 0, importedFileCategories.size()); } + + public void testMLText() throws Exception + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + NodeRef folderNodeRef = nodeService.createNode( + rootNode, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER).getChildRef(); + + FileInfo exportFolder = fileFolderService.create(folderNodeRef, "export", ContentModel.TYPE_FOLDER); + FileInfo content = fileFolderService.create(exportFolder.getNodeRef(), "file", ContentModel.TYPE_CONTENT); + MLText title = new MLText(); + title.addValue(Locale.ENGLISH, null); + title.addValue(Locale.FRENCH, "bonjour"); + nodeService.setProperty(content.getNodeRef(), ContentModel.PROP_TITLE, title); + nodeService.setProperty(content.getNodeRef(), ContentModel.PROP_NAME, "file"); + + FileInfo importFolder = fileFolderService.create(folderNodeRef, "import", ContentModel.TYPE_FOLDER); + + // export + File acpFile = exportContent(exportFolder.getNodeRef()); + + // import + FileInfo importFolderFileInfo = importContent(acpFile, importFolder.getNodeRef()); + assertNotNull(importFolderFileInfo); + NodeRef importedFileNode = fileFolderService.searchSimple(importFolderFileInfo.getNodeRef(), "file"); + assertNotNull("Couldn't find imported file: file", importedFileNode); + + Locale currentLocale = I18NUtil.getContentLocale(); + try + { + I18NUtil.setContentLocale(Locale.ENGLISH); + + String importedTitle = (String)nodeService.getProperty(importedFileNode, ContentModel.PROP_TITLE); + assertNull(importedTitle); + + I18NUtil.setContentLocale(Locale.FRENCH); + + importedTitle = (String)nodeService.getProperty(importedFileNode, ContentModel.PROP_TITLE); + assertNotNull(importedTitle); + assertEquals("bonjour", importedTitle); + } + finally + { + I18NUtil.setContentLocale(currentLocale); + } + } /** * @param contentChildAssocRef @@ -191,11 +243,11 @@ public class ExporterComponentTest extends BaseSpringTest * @throws FileNotFoundException * @throws IOException */ - private File exportContent(ChildAssociationRef contentChildAssocRef) + private File exportContent(NodeRef nodeRef) throws FileNotFoundException, IOException { TestProgress testProgress = new TestProgress(); - Location location = new Location(contentChildAssocRef.getParentRef()); + Location location = new Location(nodeRef); ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); parameters.setExportFrom(location); File acpFile = TempFileProvider.createTempFile("category-export-test", ACPExportPackageHandler.ACP_EXTENSION); @@ -446,7 +498,7 @@ public class ExporterComponentTest extends BaseSpringTest // System.out.println("TestProgress: end MLValue."); } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { // System.out.println("TestProgress: start MLValue for locale: " + locale); } diff --git a/source/java/org/alfresco/repo/exporter/URLExporter.java b/source/java/org/alfresco/repo/exporter/URLExporter.java index 9655623cc2..8b5ebad749 100644 --- a/source/java/org/alfresco/repo/exporter/URLExporter.java +++ b/source/java/org/alfresco/repo/exporter/URLExporter.java @@ -271,9 +271,9 @@ import org.springframework.extensions.surf.util.ParameterCheck; exporter.endReference(nodeRef); } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { - exporter.startValueMLText(nodeRef, locale); + exporter.startValueMLText(nodeRef, locale, isNull); } public void endValueMLText(NodeRef nodeRef) diff --git a/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java b/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java index 830b2340cc..f979084dfb 100644 --- a/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java +++ b/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java @@ -555,6 +555,8 @@ import org.xml.sax.helpers.AttributesImpl; } } + boolean isMLText = (dataTypeDef != null && dataTypeDef.getName().equals(DataTypeDefinition.MLTEXT)); + // convert node references to paths if (value instanceof NodeRef && referenceType.equals(ReferenceType.PATHREF)) { @@ -589,7 +591,11 @@ import org.xml.sax.helpers.AttributesImpl; { attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, DATATYPE_LOCALNAME, DATATYPE_QNAME.toPrefixString(), null, toPrefixString(valueDataType)); } - contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME), attrs); + if(!isMLText) + { + // only output for non MLTEXT values + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME), attrs); + } } // output value @@ -604,10 +610,11 @@ import org.xml.sax.helpers.AttributesImpl; } // output value wrapper if property data type is any - if (value == null || valueDataType != null || index != -1) - { - contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME)); - } + if ((value == null || valueDataType != null || index != -1) && !isMLText) + { + // only output for non MLTEXT values + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME)); + } } catch (SAXException e) { @@ -736,10 +743,14 @@ import org.xml.sax.helpers.AttributesImpl; } } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { AttributesImpl attrs = new AttributesImpl(); attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, LOCALE_LOCALNAME, LOCALE_QNAME.toPrefixString(), null, locale.toString()); + if(isNull) + { + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, ISNULL_LOCALNAME, ISNULL_QNAME.toPrefixString(), null, "true"); + } try { contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, MLVALUE_LOCALNAME, MLVALUE_QNAME.toPrefixString(), attrs); diff --git a/source/java/org/alfresco/repo/forms/processor/node/FieldUtils.java b/source/java/org/alfresco/repo/forms/processor/node/FieldUtils.java index c5866e8931..f5e4f13a78 100644 --- a/source/java/org/alfresco/repo/forms/processor/node/FieldUtils.java +++ b/source/java/org/alfresco/repo/forms/processor/node/FieldUtils.java @@ -27,6 +27,7 @@ import java.util.Map; import org.alfresco.repo.forms.Field; import org.alfresco.repo.forms.FieldGroup; import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.namespace.NamespaceService; @@ -54,9 +55,10 @@ public class FieldUtils PropertyDefinition propDef, Object value, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - PropertyFieldProcessor processor = new PropertyFieldProcessor(namespaceService, null); + PropertyFieldProcessor processor = new PropertyFieldProcessor(namespaceService, dictionaryService); return processor.makeField(propDef, value, group); } @@ -71,9 +73,10 @@ public class FieldUtils public static List makePropertyFields( Collection propDefs, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - return makePropertyFields(propDefs, null, group, namespaceService); + return makePropertyFields(propDefs, null, group, namespaceService, dictionaryService); } /** @@ -87,9 +90,10 @@ public class FieldUtils public static List makePropertyFields( Map propDefAndValue, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - return makePropertyFields(propDefAndValue.keySet(), propDefAndValue, group, namespaceService); + return makePropertyFields(propDefAndValue.keySet(), propDefAndValue, group, namespaceService, dictionaryService); } /** @@ -105,9 +109,10 @@ public class FieldUtils Collection propDefs, Map values, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - PropertyFieldProcessor processor = new PropertyFieldProcessor(namespaceService, null); + PropertyFieldProcessor processor = new PropertyFieldProcessor(namespaceService, dictionaryService); ArrayList fields = new ArrayList(propDefs.size()); for (PropertyDefinition propDef : propDefs) { @@ -131,9 +136,10 @@ public class FieldUtils AssociationDefinition assocDef, Object value, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - AssociationFieldProcessor processor = new AssociationFieldProcessor(namespaceService, null); + AssociationFieldProcessor processor = new AssociationFieldProcessor(namespaceService, dictionaryService); return processor.makeField(assocDef, value, group); } @@ -148,9 +154,10 @@ public class FieldUtils public static List makeAssociationFields( Collection assocDefs, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - return makeAssociationFields(assocDefs, null, group, namespaceService); + return makeAssociationFields(assocDefs, null, group, namespaceService, dictionaryService); } /** @@ -164,9 +171,10 @@ public class FieldUtils public static List makeAssociationFields( Map assocDefAndValue, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - return makeAssociationFields(assocDefAndValue.keySet(), assocDefAndValue, group, namespaceService); + return makeAssociationFields(assocDefAndValue.keySet(), assocDefAndValue, group, namespaceService, dictionaryService); } /** @@ -182,9 +190,10 @@ public class FieldUtils Collection assocDefs, Map values, FieldGroup group, - NamespaceService namespaceService) + NamespaceService namespaceService, + DictionaryService dictionaryService) { - AssociationFieldProcessor processor = new AssociationFieldProcessor(namespaceService, null); + AssociationFieldProcessor processor = new AssociationFieldProcessor(namespaceService, dictionaryService); ArrayList fields = new ArrayList(assocDefs.size()); for (AssociationDefinition propDef : assocDefs) { diff --git a/source/java/org/alfresco/repo/forms/processor/node/MockClassAttributeDefinition.java b/source/java/org/alfresco/repo/forms/processor/node/MockClassAttributeDefinition.java index 440d07a407..bfbf52ee43 100644 --- a/source/java/org/alfresco/repo/forms/processor/node/MockClassAttributeDefinition.java +++ b/source/java/org/alfresco/repo/forms/processor/node/MockClassAttributeDefinition.java @@ -32,6 +32,7 @@ import org.alfresco.service.cmr.dictionary.ConstraintDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.ModelDefinition; import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; import org.alfresco.service.namespace.QName; /** @@ -167,7 +168,7 @@ public class MockClassAttributeDefinition implements PropertyDefinition, Associa } @Override - public String getDescription() + public String getDescription(MessageLookup messageLookup) { return description; } @@ -191,7 +192,7 @@ public class MockClassAttributeDefinition implements PropertyDefinition, Associa } @Override - public String getTitle() + public String getTitle(MessageLookup messageLookup) { return title; } diff --git a/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java b/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java index 6ee2f4bf9c..cdec616fd2 100644 --- a/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java +++ b/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java @@ -236,7 +236,7 @@ public class PropertyFieldProcessor extends QNameFieldProcessor { String attribName = fieldDef.getName(); fieldDef.setGroup(group); - String title = attribDef.getTitle(); + String title = attribDef.getTitle(dictionaryService); title = title == null ? attribName : title; fieldDef.setLabel(title); - fieldDef.setDescription(attribDef.getDescription()); + fieldDef.setDescription(attribDef.getDescription(dictionaryService)); fieldDef.setProtectedField(attribDef.isProtected()); // define the data key name and set diff --git a/source/java/org/alfresco/repo/i18n/MessageService.java b/source/java/org/alfresco/repo/i18n/MessageService.java index 880aff8cef..f2203b719b 100644 --- a/source/java/org/alfresco/repo/i18n/MessageService.java +++ b/source/java/org/alfresco/repo/i18n/MessageService.java @@ -24,13 +24,14 @@ import java.util.ResourceBundle; import java.util.Set; import org.alfresco.repo.tenant.TenantDeployer; +import org.alfresco.service.cmr.i18n.MessageLookup; import org.alfresco.service.cmr.repository.StoreRef; /** * Utility class providing methods to access the Locale of the current thread and to get * Localised strings. These strings may be loaded from resource bundles deployed in the Repository. */ -public interface MessageService extends TenantDeployer +public interface MessageService extends TenantDeployer, MessageLookup { /** * Set the locale for the current thread. @@ -97,42 +98,6 @@ public interface MessageService extends TenantDeployer */ public void registerResourceBundle(String bundleBasePath); - /** - * Get message from registered resource bundle. - * - * @param messageKey message key - * @return localised message string, null if not found - */ - public String getMessage(String messageKey); - - /** - * Get a localised message string - * - * @param messageKey the message key - * @param locale override the current locale - * @return the localised message string, null if not found - */ - public String getMessage(final String messageKey, final Locale locale); - - /** - * Get a localised message string, parameterized using standard MessageFormatter. - * - * @param messageKey message key - * @param params format parameters - * @return the localised string, null if not found - */ - public String getMessage(String messageKey, Object ... params); - - /** - * Get a localised message string, parameterized using standard MessageFormatter. - * - * @param messageKey the message key - * @param locale override current locale - * @param params the localised message string - * @return the localised string, null if not found - */ - public String getMessage(String messageKey, Locale locale, Object ... params); - /** * Unregister a resource bundle *

diff --git a/source/java/org/alfresco/repo/importer/view/ViewParser.java b/source/java/org/alfresco/repo/importer/view/ViewParser.java index 124e52fb2a..405718bc35 100644 --- a/source/java/org/alfresco/repo/importer/view/ViewParser.java +++ b/source/java/org/alfresco/repo/importer/view/ViewParser.java @@ -718,10 +718,12 @@ public class ViewParser implements Parser isMLProperty = false; locale = xpp.getAttributeValue(NamespaceService.REPOSITORY_VIEW_1_0_URI, VIEW_LOCALE_ATTR); + Boolean isNull = Boolean.valueOf(xpp.getAttributeValue(NamespaceService.REPOSITORY_VIEW_1_0_URI, VIEW_ISNULL_ATTR)); + String decoratedValue = isNull ? null : ""; eventType = xpp.next(); if (eventType == XmlPullParser.TEXT) { - value = xpp.getText(); + decoratedValue = xpp.getText(); eventType = xpp.next(); } if (eventType == XmlPullParser.END_TAG) @@ -730,7 +732,7 @@ public class ViewParser implements Parser { logger.debug("Found ML entry: " + locale + "=" + value); } - mlText.addValue(DefaultTypeConverter.INSTANCE.convert(Locale.class, locale), value); + mlText.addValue(DefaultTypeConverter.INSTANCE.convert(Locale.class, locale), decoratedValue); eventType = xpp.next(); if (eventType == XmlPullParser.TEXT) diff --git a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java index 3056a4dd84..ec5e701d2a 100644 --- a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java +++ b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java @@ -994,8 +994,20 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor } - // Take advantage of the fact that the authority name is on the child association public boolean isAuthorityContained(NodeRef authorityNodeRef, String authorityToFind) + { + // Look up the desired authority using case sensitivity rules appropriate for the authority type + NodeRef authorityToFindRef = getAuthorityOrNull(authorityToFind); + if (authorityToFindRef == null) + { + // No such authority so it won't be contained anywhere + return false; + } + // Now we can just search for the NodeRef + return isAuthorityContainedImpl(authorityNodeRef, authorityToFindRef); + } + + private boolean isAuthorityContainedImpl(NodeRef authorityNodeRef, NodeRef authorityToFindRef) { List cars = getCachedChildAuthorities(authorityNodeRef); if (cars == null) @@ -1008,10 +1020,10 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor // Loop over children recursively to find authorityToFind for (ChildAssociationRef car : cars) { - String authorityName = car.getQName().getLocalName(); - if (authorityToFind.equals(authorityName) - || AuthorityType.getAuthorityType(authorityName) != AuthorityType.USER - && isAuthorityContained(car.getChildRef(), authorityToFind)) + NodeRef childRef = car.getChildRef(); + if (authorityToFindRef.equals(childRef) + || AuthorityType.getAuthorityType(car.getQName().getLocalName()) != AuthorityType.USER + && isAuthorityContainedImpl(childRef, authorityToFindRef)) { return true; } diff --git a/source/java/org/alfresco/repo/security/authority/script/Authority.java b/source/java/org/alfresco/repo/security/authority/script/Authority.java index 54ab7cedba..5f255b98e7 100644 --- a/source/java/org/alfresco/repo/security/authority/script/Authority.java +++ b/source/java/org/alfresco/repo/security/authority/script/Authority.java @@ -21,6 +21,7 @@ package org.alfresco.repo.security.authority.script; import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Set; public interface Authority { @@ -29,6 +30,7 @@ public interface Authority public String getShortName(); public String getFullName(); public String getDisplayName(); + public Set getZones(); /** diff --git a/source/java/org/alfresco/repo/security/authority/script/ScriptGroup.java b/source/java/org/alfresco/repo/security/authority/script/ScriptGroup.java index 113c224afc..93472a78ce 100644 --- a/source/java/org/alfresco/repo/security/authority/script/ScriptGroup.java +++ b/source/java/org/alfresco/repo/security/authority/script/ScriptGroup.java @@ -616,4 +616,17 @@ public class ScriptGroup implements Authority, Serializable return groups; } + + /** + * Gets all the zones of this group + * + * @return The name of the zones of this group. + * + * @since 4.1.3 + */ + @Override + public Set getZones() + { + return authorityService.getAuthorityZones(fullName); + } } diff --git a/source/java/org/alfresco/repo/security/authority/script/ScriptUser.java b/source/java/org/alfresco/repo/security/authority/script/ScriptUser.java index c5bcefc6b0..33a7bc6069 100644 --- a/source/java/org/alfresco/repo/security/authority/script/ScriptUser.java +++ b/source/java/org/alfresco/repo/security/authority/script/ScriptUser.java @@ -19,6 +19,7 @@ package org.alfresco.repo.security.authority.script; import java.io.Serializable; +import java.util.Set; import org.alfresco.model.ContentModel; import org.alfresco.repo.jscript.ScriptNode; @@ -125,4 +126,17 @@ public class ScriptUser implements Authority, Serializable { return new ScriptNode(getPersonNodeRef(), serviceRegistry, this.scope); } + + /** + * Gets all the zones of this user + * + * @return The name of the zones of this user. + * + * @since 4.1.3 + */ + @Override + public Set getZones() + { + return authorityService.getAuthorityZones(fullName); + } } diff --git a/source/java/org/alfresco/repo/site/script/Site.java b/source/java/org/alfresco/repo/site/script/Site.java index 2c7f4e5f1d..fef07e8991 100644 --- a/source/java/org/alfresco/repo/site/script/Site.java +++ b/source/java/org/alfresco/repo/site/script/Site.java @@ -721,7 +721,7 @@ public class Site implements Serializable if (propDef != null) { type = propDef.getDataType().getName().toString(); - title = propDef.getTitle(); + title = propDef.getTitle(this.serviceRegistry.getDictionaryService()); } // create the custom property and add to the map diff --git a/source/java/org/alfresco/repo/thumbnail/FailedThumbnailSourceAspect.java b/source/java/org/alfresco/repo/thumbnail/FailedThumbnailSourceAspect.java index daab686b60..3276149792 100644 --- a/source/java/org/alfresco/repo/thumbnail/FailedThumbnailSourceAspect.java +++ b/source/java/org/alfresco/repo/thumbnail/FailedThumbnailSourceAspect.java @@ -109,6 +109,12 @@ public class FailedThumbnailSourceAspect implements NodeServicePolicies.OnDelete @Override public void onDeleteNode(ChildAssociationRef childAssocRef, boolean isNodeArchived) { + if (!nodeService.exists(childAssocRef.getParentRef())) + { + // We are in the process of deleting the parent. + return; + } + // When a failedThumbnail node has been deleted, we should check if there are any other // failedThumbnail peer nodes left. // If there are not, then we can remove the failedThumbnailSource aspect. diff --git a/source/java/org/alfresco/repo/version/Version2ServiceImpl.java b/source/java/org/alfresco/repo/version/Version2ServiceImpl.java index d694503cdb..d0f44c5f1a 100644 --- a/source/java/org/alfresco/repo/version/Version2ServiceImpl.java +++ b/source/java/org/alfresco/repo/version/Version2ServiceImpl.java @@ -1141,7 +1141,8 @@ public class Version2ServiceImpl extends VersionServiceImpl implements VersionSe // Add/remove the child nodes List children = new ArrayList(this.nodeService.getChildAssocs(nodeRef)); - for (ChildAssociationRef versionedChild : this.nodeService.getChildAssocs(versionNodeRef)) + List versionedChildren = this.nodeService.getChildAssocs(versionNodeRef); + for (ChildAssociationRef versionedChild : versionedChildren) { if (children.contains(versionedChild) == false) { @@ -1150,8 +1151,13 @@ public class Version2ServiceImpl extends VersionServiceImpl implements VersionSe // The node was a primary child of the parent, but that is no longer the case. Despite this // the node still exits so this means it has been moved. // The best thing to do in this situation will be to re-add the node as a child, but it will not - // be a primary child. - this.nodeService.addChild(nodeRef, versionedChild.getChildRef(), versionedChild.getTypeQName(), versionedChild.getQName()); + // be a primary child + String childRefName = (String) this.nodeService.getProperty(versionedChild.getChildRef(), ContentModel.PROP_NAME); + NodeRef childAssocOnVersionNode = this.nodeService.getChildByName(nodeRef, versionedChild.getTypeQName(), childRefName); + if (childAssocOnVersionNode == null ) + { + this.nodeService.addChild(nodeRef, versionedChild.getChildRef(), versionedChild.getTypeQName(), versionedChild.getQName()); + } } else { diff --git a/source/java/org/alfresco/repo/version/VersionServiceImplTest.java b/source/java/org/alfresco/repo/version/VersionServiceImplTest.java index ad1e3d6319..5a8c4a4618 100644 --- a/source/java/org/alfresco/repo/version/VersionServiceImplTest.java +++ b/source/java/org/alfresco/repo/version/VersionServiceImplTest.java @@ -30,6 +30,8 @@ import java.util.Set; import org.alfresco.model.ApplicationModel; import org.alfresco.model.ContentModel; +import org.alfresco.model.ForumModel; +import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.jscript.ScriptNode; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -41,6 +43,7 @@ import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; @@ -490,6 +493,97 @@ public class VersionServiceImplTest extends BaseVersionStoreTest //assertEquals("0.1", nodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL)); } + + /** + * Test reverting a node that has comments, see ALF-13129 + */ + public void testRevertWithComments() + { + NodeRef versionableNode = createNewVersionableNode(); + + this.dbNodeService.setProperty(versionableNode, PROP_1, "I am before version"); + Version version1 = createVersion(versionableNode); + this.dbNodeService.setProperty(versionableNode, PROP_1, "I am after version 1"); + + VersionHistory vh = this.versionService.getVersionHistory(versionableNode); + assertNotNull(vh); + assertEquals(1, vh.getAllVersions().size()); + + // Create a new version + Version version2 = createVersion(versionableNode); + + //Test a revert with no comments + this.versionService.revert(versionableNode, version1); + assertEquals("I am before version", this.dbNodeService.getProperty(versionableNode, PROP_1)); + + createComment(versionableNode, "my comment", "Do great work", false); + assertTrue(nodeService.hasAspect(versionableNode, ForumModel.ASPECT_DISCUSSABLE)); + assertEquals(1, this.dbNodeService.getProperty(versionableNode, ForumModel.PROP_COMMENT_COUNT)); + + // Create a new version + this.dbNodeService.setProperty(versionableNode, PROP_1, "I am version 3"); + Version version3 = createVersion(versionableNode); + this.dbNodeService.setProperty(versionableNode, PROP_1, "I am after version 3"); + + createComment(versionableNode, "v3", "Great version", false); + assertEquals(2, this.dbNodeService.getProperty(versionableNode, ForumModel.PROP_COMMENT_COUNT)); + + //Revert to a version that has comments. + this.versionService.revert(versionableNode, version3); + assertTrue(nodeService.hasAspect(versionableNode, ForumModel.ASPECT_DISCUSSABLE)); + assertEquals("I am version 3", this.dbNodeService.getProperty(versionableNode, PROP_1)); + + } + + /** + * This method was taken from the CommmentServiceImpl on the cloud branch + * + * TODO: When this is merged to HEAD, please remove this method and use the one in CommmentServiceImpl + */ + private NodeRef createComment(final NodeRef discussableNode, String title, String comment, boolean suppressRollups) + { + if(comment == null) + { + throw new IllegalArgumentException("Must provide a non-null comment"); + } + + // There is no CommentService, so we have to create the node structure by hand. + // This is what happens within e.g. comment.put.json.js when comments are submitted via the REST API. + if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE)) + { + nodeService.addAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE, null); + } + if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP) && !suppressRollups) + { + nodeService.addAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP, null); + } + // Forum node is created automatically by DiscussableAspect behaviour. + NodeRef forumNode = nodeService.getChildAssocs(discussableNode, ForumModel.ASSOC_DISCUSSION, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion")).get(0).getChildRef(); + + final List existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments")); + NodeRef topicNode = null; + if (existingTopics.isEmpty()) + { + Map props = new HashMap(1, 1.0f); + props.put(ContentModel.PROP_NAME, "Comments"); + topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"), ForumModel.TYPE_TOPIC, props).getChildRef(); + } + else + { + topicNode = existingTopics.get(0).getChildRef(); + } + + NodeRef postNode = nodeService.createNode(topicNode, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, ForumModel.TYPE_POST).getChildRef(); + nodeService.setProperty(postNode, ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null)); + nodeService.setProperty(postNode, ContentModel.PROP_TITLE, title); + ContentWriter writer = contentService.getWriter(postNode, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_HTML); + writer.setEncoding("UTF-8"); + writer.putContent(comment); + + return postNode; + } + /** * Test reverting from Share */ diff --git a/source/java/org/alfresco/repo/workflow/WorkflowObjectFactory.java b/source/java/org/alfresco/repo/workflow/WorkflowObjectFactory.java index e499d792da..1a7f815fcb 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowObjectFactory.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowObjectFactory.java @@ -225,8 +225,8 @@ public class WorkflowObjectFactory String processKey = getProcessKey(defName) + ".task." + name; TypeDefinition metadata = taskDef.getMetadata(); - String title = getLabel(processKey, TITLE_LABEL, metadata.getTitle(), defaultTitle, name); - String description = getLabel(processKey, DESC_LABEL, metadata.getDescription(), defaultDescription, title); + String title = getLabel(processKey, TITLE_LABEL, metadata.getTitle(dictionaryService), defaultTitle, name); + String description = getLabel(processKey, DESC_LABEL, metadata.getDescription(dictionaryService), defaultDescription, title); return new WorkflowTask(actualId, taskDef, name, title, description, state, path, properties); @@ -262,7 +262,7 @@ public class WorkflowObjectFactory public String getTaskTitle(TypeDefinition typeDefinition, String defName, String defaultTitle, String name) { String displayId = getProcessKey(defName) + ".task." + name; - return getLabel(displayId, TITLE_LABEL, defaultTitle, typeDefinition.getTitle(), name); + return getLabel(displayId, TITLE_LABEL, defaultTitle, typeDefinition.getTitle(dictionaryService), name); } public String getTaskDescription(TypeDefinition typeDefinition, String defName, String defaultDescription, String title) diff --git a/source/java/org/alfresco/service/cmr/view/Exporter.java b/source/java/org/alfresco/service/cmr/view/Exporter.java index 089f273c5f..20599b51e2 100644 --- a/source/java/org/alfresco/service/cmr/view/Exporter.java +++ b/source/java/org/alfresco/service/cmr/view/Exporter.java @@ -179,7 +179,7 @@ public interface Exporter * @param nodeRef the node reference * @param locale */ - public void startValueMLText(NodeRef nodeRef, Locale locale); + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull); /** * End export MLText diff --git a/source/java/org/alfresco/tools/Export.java b/source/java/org/alfresco/tools/Export.java index c4a45ea031..3197573129 100644 --- a/source/java/org/alfresco/tools/Export.java +++ b/source/java/org/alfresco/tools/Export.java @@ -645,7 +645,7 @@ public final class Export extends Tool { } - public void startValueMLText(NodeRef nodeRef, Locale locale) + public void startValueMLText(NodeRef nodeRef, Locale locale, boolean isNull) { } }