diff --git a/config/alfresco/action-services-context.xml b/config/alfresco/action-services-context.xml index 249ebbebb0..6ae0233d68 100644 --- a/config/alfresco/action-services-context.xml +++ b/config/alfresco/action-services-context.xml @@ -420,12 +420,6 @@ - - - - - - @@ -493,6 +487,12 @@ + + + false + + + diff --git a/config/alfresco/cache-context.xml b/config/alfresco/cache-context.xml index 9c7f937189..c5c80a225d 100644 --- a/config/alfresco/cache-context.xml +++ b/config/alfresco/cache-context.xml @@ -323,13 +323,13 @@ - + - + @@ -339,7 +339,7 @@ - + diff --git a/config/alfresco/cmis-api-context.xml b/config/alfresco/cmis-api-context.xml index 568a337583..a432110c34 100644 --- a/config/alfresco/cmis-api-context.xml +++ b/config/alfresco/cmis-api-context.xml @@ -117,11 +117,6 @@ - - - - - diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml index 0a4dbec18e..881888fdc7 100644 --- a/config/alfresco/dao/dao-context.xml +++ b/config/alfresco/dao/dao-context.xml @@ -287,8 +287,6 @@ - - diff --git a/config/alfresco/messages/patch-service_ru.properties b/config/alfresco/messages/patch-service_ru.properties index cd07c4913d..a13321dbbb 100755 --- a/config/alfresco/messages/patch-service_ru.properties +++ b/config/alfresco/messages/patch-service_ru.properties @@ -488,4 +488,5 @@ patch.increaseColumnSizeActiviti.description=ALF-14983 : Upgrade scripts to incr patch.removeColumnActiviti.description=ALF-16038 : DB2: Upgrade script to remove ALFUSER.ACT_HI_ACTINST.OWNER_ patch.upgradeToActiviti5-10.description=Upgraded Activiti tables to 5.10 version -patch.addActivtiIndexHistoricActivity.description=Additional index for activiti on historic activity (PROC_INST_ID_ and ACTIVITY_ID_) \ No newline at end of file +patch.addActivtiIndexHistoricActivity.description=Additional index for activiti on historic activity (PROC_INST_ID_ and ACTIVITY_ID_) +patch.renameConstraintActiviti.description=ALF-15828 : DB2: Upgrade script to rename ACT_HI_PROCINST.PROC_INST_ID_ index \ No newline at end of file diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index b63443dc13..8c34c8b220 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -2484,6 +2484,12 @@ 0 5010 5011 + + + + + + diff --git a/config/alfresco/rule-services-context.xml b/config/alfresco/rule-services-context.xml index 8e3bb6cecd..02bfa6faa4 100644 --- a/config/alfresco/rule-services-context.xml +++ b/config/alfresco/rule-services-context.xml @@ -74,7 +74,6 @@ - @@ -115,6 +114,9 @@ + + ${policy.content.update.ignoreEmpty} + @@ -153,16 +155,14 @@ - - - false - - - - + true + + + ${policy.content.update.ignoreEmpty} + diff --git a/config/alfresco/subsystems/imap/default/imap-server-context.xml b/config/alfresco/subsystems/imap/default/imap-server-context.xml index c1fdf8b9ad..af0a1bb00d 100644 --- a/config/alfresco/subsystems/imap/default/imap-server-context.xml +++ b/config/alfresco/subsystems/imap/default/imap-server-context.xml @@ -115,6 +115,9 @@ + + + diff --git a/config/alfresco/tx-cache-context.xml b/config/alfresco/tx-cache-context.xml index 13db975ea7..0f7081154b 100644 --- a/config/alfresco/tx-cache-context.xml +++ b/config/alfresco/tx-cache-context.xml @@ -428,6 +428,7 @@ + @@ -507,51 +508,6 @@ - - - - - - - - org.alfresco.compiledModelsTransactionalCache - - - - - - - - - - - - - - - org.alfresco.prefixesTransactionalCache - - - - - - - - - - - - - - - org.alfresco.webScriptsRegistryTransactionalCache - - - - - - - diff --git a/source/java/org/alfresco/cmis/changelog/CMISChangeLogDataExtractor.java b/source/java/org/alfresco/cmis/changelog/CMISChangeLogDataExtractor.java deleted file mode 100644 index fbd3a17acc..0000000000 --- a/source/java/org/alfresco/cmis/changelog/CMISChangeLogDataExtractor.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2005-2010 Alfresco Software Limited. - * - * This file is part of Alfresco - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ -package org.alfresco.cmis.changelog; - -import java.io.Serializable; -import java.util.HashMap; - -import org.alfresco.cmis.CMISDictionaryModel; -import org.alfresco.cmis.CMISInvalidArgumentException; -import org.alfresco.cmis.CMISServices; -import org.alfresco.cmis.CMISTypeDefinition; -import org.alfresco.cmis.CMISTypeId; -import org.alfresco.repo.audit.extractor.AbstractDataExtractor; -import org.alfresco.service.cmr.model.FileInfo; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.NodeRef; - -/** - * An extractor that allows to filter data using the following rule: - * Audit records should only be created for items in the CMIS domain model. - * - * @author Stas Sokolovsky - */ -public class CMISChangeLogDataExtractor extends AbstractDataExtractor -{ - private CMISServices cmisService; - - public static final String KEY_NODE_REF = "nodeRef"; - public static final String KEY_OBJECT_ID = "objectId"; - - /** - * Extracts relevant node refs and Ids from auditing data - * - * @see org.alfresco.repo.audit.extractor.DataExtractor.extractData(java.io.Serializable) - */ - public Serializable extractData(Serializable value) throws Throwable - { - NodeRef nodeRef = getNodeRef(value); - HashMap result = new HashMap(5); - result.put(KEY_NODE_REF, nodeRef); - // Support version nodes by recording the object ID - result.put(KEY_OBJECT_ID, cmisService.getProperty(nodeRef, CMISDictionaryModel.PROP_OBJECT_ID)); - return result; - } - - /** - * @return Returns true if items in the CMIS domain model - * @see org.alfresco.repo.audit.extractor.DataExtractor.isSupported(java.io.Serializable) - */ - public boolean isSupported(Serializable data) - { - if (data != null) - { - NodeRef nodeRef = getNodeRef(data); - if (nodeRef != null) - { - try - { - CMISTypeDefinition typeDef = cmisService.getTypeDefinition(nodeRef); - if (typeDef != null) - { - CMISTypeId typeId = typeDef.getBaseType().getTypeId(); - return typeId.equals(CMISDictionaryModel.DOCUMENT_TYPE_ID) || typeId.equals(CMISDictionaryModel.FOLDER_TYPE_ID); - } - } - catch (CMISInvalidArgumentException e) - { - // Ignore and return false - } - } - } - return false; - } - - /** - * Gets the NodeRef from auditing data - * - * @param data audit data - * @return Node Reference - */ - private NodeRef getNodeRef(Serializable data) - { - NodeRef nodeRef = null; - if (data instanceof ChildAssociationRef) - { - nodeRef = ((ChildAssociationRef) data).getChildRef(); - } - if (data instanceof FileInfo) - { - nodeRef = ((FileInfo) data).getNodeRef(); - } - else if (data instanceof NodeRef) - { - nodeRef = (NodeRef) data; - } - return nodeRef; - } - - /** - * Set the CMIS service - * - * @param cmisService CMIS service - */ - public void setCmisService(CMISServices cmisService) - { - this.cmisService = cmisService; - } -} diff --git a/source/java/org/alfresco/cmis/changelog/CMISChangeLogServiceImpl.java b/source/java/org/alfresco/cmis/changelog/CMISChangeLogServiceImpl.java index d6dfd0def8..5524ab90d8 100644 --- a/source/java/org/alfresco/cmis/changelog/CMISChangeLogServiceImpl.java +++ b/source/java/org/alfresco/cmis/changelog/CMISChangeLogServiceImpl.java @@ -34,6 +34,7 @@ import org.alfresco.cmis.CMISChangeLogService; import org.alfresco.cmis.CMISChangeType; import org.alfresco.cmis.CMISInvalidArgumentException; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.opencmis.CMISChangeLogDataExtractor; import org.alfresco.service.cmr.audit.AuditQueryParameters; import org.alfresco.service.cmr.audit.AuditService; import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; diff --git a/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java b/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java index d724d437f7..372b0c48b4 100644 --- a/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java +++ b/source/java/org/alfresco/opencmis/AlfrescoCmisServiceImpl.java @@ -248,12 +248,17 @@ public class AlfrescoCmisServiceImpl extends AbstractCmisService implements Alfr protected CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef) { - CMISNodeInfoImpl result = connector.createNodeInfo(nodeRef); + return createNodeInfo(nodeRef, null); + } + + protected CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef, VersionHistory versionHistory) + { + CMISNodeInfoImpl result = connector.createNodeInfo(nodeRef, versionHistory); nodeInfoMap.put(result.getObjectId(), result); return result; } - + protected CMISNodeInfo createNodeInfo(AssociationRef assocRef) { CMISNodeInfoImpl result = connector.createNodeInfo(assocRef); @@ -2004,7 +2009,7 @@ public class AlfrescoCmisServiceImpl extends AbstractCmisService implements Alfr // convert the version history for (Version version : versionHistory.getAllVersions()) { - CMISNodeInfo versionInfo = createNodeInfo(version.getFrozenStateNodeRef()); + CMISNodeInfo versionInfo = createNodeInfo(version.getFrozenStateNodeRef(), versionHistory); result.add( connector.createCMISObject( diff --git a/source/java/org/alfresco/opencmis/CMISChangeLogDataExtractor.java b/source/java/org/alfresco/opencmis/CMISChangeLogDataExtractor.java index 5579c56277..e97919a38d 100644 --- a/source/java/org/alfresco/opencmis/CMISChangeLogDataExtractor.java +++ b/source/java/org/alfresco/opencmis/CMISChangeLogDataExtractor.java @@ -26,6 +26,7 @@ import org.alfresco.opencmis.dictionary.CMISPropertyAccessor; import org.alfresco.opencmis.dictionary.TypeDefinitionWrapper; import org.alfresco.repo.audit.extractor.AbstractDataExtractor; import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.QName; @@ -100,10 +101,15 @@ public class CMISChangeLogDataExtractor extends AbstractDataExtractor private NodeRef getNodeRef(Serializable data) { NodeRef nodeRef = null; - if (data instanceof FileInfo) + if (data instanceof ChildAssociationRef) + { + nodeRef = ((ChildAssociationRef) data).getChildRef(); + } + else if (data instanceof FileInfo) { nodeRef = ((FileInfo) data).getNodeRef(); - } else if (data instanceof NodeRef) + } + else if (data instanceof NodeRef) { nodeRef = (NodeRef) data; } diff --git a/source/java/org/alfresco/opencmis/CMISConnector.java b/source/java/org/alfresco/opencmis/CMISConnector.java index cfcb8bbfe6..237831909a 100644 --- a/source/java/org/alfresco/opencmis/CMISConnector.java +++ b/source/java/org/alfresco/opencmis/CMISConnector.java @@ -104,6 +104,7 @@ import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.site.SiteInfo; import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.version.VersionHistory; import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.cmr.version.VersionType; import org.alfresco.service.descriptor.Descriptor; @@ -844,7 +845,15 @@ public class CMISConnector implements ApplicationContextAware, ApplicationListen */ public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef) { - return new CMISNodeInfoImpl(this, nodeRef); + return createNodeInfo(nodeRef, null); + } + + /** + * Creates an object info object. + */ + public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef, VersionHistory versionHistory) + { + return new CMISNodeInfoImpl(this, nodeRef, versionHistory); } /** diff --git a/source/java/org/alfresco/opencmis/CMISNodeInfoImpl.java b/source/java/org/alfresco/opencmis/CMISNodeInfoImpl.java index 529421db4c..19e158a13b 100644 --- a/source/java/org/alfresco/opencmis/CMISNodeInfoImpl.java +++ b/source/java/org/alfresco/opencmis/CMISNodeInfoImpl.java @@ -94,6 +94,15 @@ public class CMISNodeInfoImpl implements CMISNodeInfo analyseObjectId(); } + public CMISNodeInfoImpl(CMISConnector connector, NodeRef nodeRef, VersionHistory versionHistory) + { + this.connector = connector; + this.nodeRef = nodeRef; + this.versionHistory = versionHistory; + + analyseNodeRef(); + } + public CMISNodeInfoImpl(CMISConnector connector, NodeRef nodeRef) { this.connector = connector; @@ -115,10 +124,10 @@ public class CMISNodeInfoImpl implements CMISNodeInfo return objecVariant != CMISObjectVariant.VERSION; } - protected void analyseVersionNode(NodeRef nodeRef) + protected void analyseVersionNode() { // check version - versionHistory = connector.getVersionService().getVersionHistory(nodeRef); + versionHistory = getVersionHistory(); if (versionHistory == null) { objecVariant = CMISObjectVariant.CURRENT_VERSION; @@ -301,7 +310,7 @@ public class CMISNodeInfoImpl implements CMISNodeInfo { // the version label refers to a specific non-head version, find the nodeRef // of the version node from the version history - versionHistory = connector.getVersionService().getVersionHistory(nodeRef); + versionHistory = getVersionHistory(); if (versionHistory == null) { // unexpected null versionHistory, assume not versioned @@ -420,7 +429,7 @@ public class CMISNodeInfoImpl implements CMISNodeInfo // check version if(connector.getVersionService().isAVersion(nodeRef)) { - analyseVersionNode(nodeRef); + analyseVersionNode(); } else { diff --git a/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java index c54ed8c11f..eda401308a 100644 --- a/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java +++ b/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java @@ -18,49 +18,36 @@ */ package org.alfresco.repo.action.executer; -import java.util.ArrayList; import java.util.List; +import org.alfresco.model.ContentModel; import org.alfresco.repo.action.ParameterDefinitionImpl; -import org.alfresco.repo.content.transform.ContentTransformer; -import org.alfresco.repo.content.transform.TransformerDebug; import org.alfresco.repo.content.transform.magick.ImageTransformationOptions; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ParameterDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.NoTransformerException; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TransformationOptions; /** - * Transfor action executer + * Transform action executer. Modified original code to use any transformer rather than just + * ImageMagick. The original, allowed additional command line options to be supplied (added for + * backward compatibility in 15/04/2008). Now removed. Did not find any internal usage (22/11/2012) but it + * is possible that there may be some in Javascript calling ScriptNode or custom code using + * the context passed to the ImageRenderingEngine. That said, it will almost always be ImagMagick + * that does the transform so it will still be used. These command line options were to overcome issues with + * ImageMagick. Other transformers will just ignore them. * * @author Roy Wetherall */ public class ImageTransformActionExecuter extends TransformActionExecuter { - private TransformerDebug transformerDebug; - /** * Action constants */ public static final String NAME = "transform-image"; public static final String PARAM_CONVERT_COMMAND = "convert-command"; - private ContentTransformer imageMagickContentTransformer; - - /** - * Set the image magick content transformer - * - * @param imageMagickContentTransformer - * the conten transformer - */ - public void setImageMagickContentTransformer(ContentTransformer imageMagickContentTransformer) - { - this.imageMagickContentTransformer = imageMagickContentTransformer; - } - /** * Add parameter definitions */ @@ -71,48 +58,17 @@ public class ImageTransformActionExecuter extends TransformActionExecuter paramList.add(new ParameterDefinitionImpl(PARAM_CONVERT_COMMAND, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_CONVERT_COMMAND))); } - public void setTransformerDebug(TransformerDebug transformerDebug) + @Override + protected TransformationOptions newTransformationOptions(Action ruleAction, NodeRef sourceNodeRef) { - this.transformerDebug = transformerDebug; - } - - /** - * @see org.alfresco.repo.action.executer.TransformActionExecuter#doTransform(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter) - */ - protected void doTransform(Action ruleAction, - NodeRef sourceNodeRef, ContentReader contentReader, - NodeRef destinationNodeRef, ContentWriter contentWriter) - { - // Try and transform the content + ImageTransformationOptions options = new ImageTransformationOptions(); + options.setSourceNodeRef(sourceNodeRef); + options.setSourceContentProperty(ContentModel.PROP_NAME); + options.setTargetContentProperty(ContentModel.PROP_NAME); + String convertCommand = (String) ruleAction.getParameterValue(PARAM_CONVERT_COMMAND); - // create some options for the transform - ImageTransformationOptions imageOptions = new ImageTransformationOptions(); - imageOptions.setCommandOptions(convertCommand); - imageOptions.setSourceNodeRef(sourceNodeRef); + options.setCommandOptions(convertCommand); - // check if the transformer is going to work, i.e. is available - String sourceMimetype = contentReader.getMimetype(); - String targetMimetype = contentWriter.getMimetype(); - long sourceSize = contentReader.getSize(); - try - { - transformerDebug.pushAvailable(contentReader.getContentUrl(), sourceMimetype, targetMimetype, imageOptions); - List transformers = new ArrayList(1); - if (imageMagickContentTransformer.isTransformable(sourceMimetype, contentReader.getSize(), targetMimetype, imageOptions)) - { - transformers.add(imageMagickContentTransformer); - } - transformerDebug.availableTransformers(transformers, sourceSize, "ImageTransformActionExecuter.doTransform(...)"); - if (transformers.size() == 0) - { - throw new NoTransformerException(sourceMimetype, targetMimetype); - } - - imageMagickContentTransformer.transform(contentReader, contentWriter, imageOptions); - } - finally - { - transformerDebug.popAvailable(); - } + return options; } } diff --git a/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java b/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java index dcea0fcd1c..8ad0c89753 100644 --- a/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java +++ b/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -39,8 +39,8 @@ import org.alfresco.repo.template.I18NMessageMethod; import org.alfresco.repo.template.TemplateNode; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ParameterDefinition; @@ -301,6 +301,13 @@ public class MailActionExecuter extends ActionExecuterAbstractBase final Action ruleAction, final NodeRef actionedUponNodeRef) { + MimeMessageHelper message = null; + if (!testMode && validNodeRefIfPresent(actionedUponNodeRef)) + { + message = prepareEmail(ruleAction, actionedUponNodeRef); + } + final MimeMessageHelper finalMessage = message; + if (sendAfterCommit(ruleAction)) { AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() @@ -314,9 +321,9 @@ public class MailActionExecuter extends ActionExecuterAbstractBase @Override public Void execute() throws Throwable { - if (validNodeRefIfPresent(actionedUponNodeRef)) + if (finalMessage != null) { - prepareAndSendEmail(ruleAction, actionedUponNodeRef); + sendEmail(ruleAction, actionedUponNodeRef, finalMessage); } return null; } @@ -328,7 +335,7 @@ public class MailActionExecuter extends ActionExecuterAbstractBase { if (validNodeRefIfPresent(actionedUponNodeRef)) { - prepareAndSendEmail(ruleAction, actionedUponNodeRef); + sendEmail(ruleAction, actionedUponNodeRef, finalMessage); } } } @@ -356,9 +363,14 @@ public class MailActionExecuter extends ActionExecuterAbstractBase return sendAfterCommit == null ? false : sendAfterCommit.booleanValue(); } - private void prepareAndSendEmail(final Action ruleAction, final NodeRef actionedUponNodeRef) + public MimeMessageHelper prepareEmail(final Action ruleAction, final NodeRef actionedUponNodeRef) { - // Create the mime mail message + // Create the mime mail message. + // Hack: using an array here to get around the fact that inner classes aren't closures. + // The MimeMessagePreparator.prepare() signature does not allow us to return a value and yet + // we can't set a result on a bare, non-final object reference due to Java language restrictions. + final MimeMessageHelper[] messageRef = new MimeMessageHelper[1]; + MimeMessagePreparator mailPreparer = new MimeMessagePreparator() { @SuppressWarnings("unchecked") @@ -369,7 +381,7 @@ public class MailActionExecuter extends ActionExecuterAbstractBase logger.debug(ruleAction.getParameterValues()); } - MimeMessageHelper message = new MimeMessageHelper(mimeMessage); + messageRef[0] = new MimeMessageHelper(mimeMessage); // set header encoding if one has been supplied if (headerEncoding != null && headerEncoding.length() != 0) @@ -381,7 +393,7 @@ public class MailActionExecuter extends ActionExecuterAbstractBase String to = (String)ruleAction.getParameterValue(PARAM_TO); if (to != null && to.length() != 0) { - message.setTo(to); + messageRef[0].setTo(to); } else { @@ -449,7 +461,7 @@ public class MailActionExecuter extends ActionExecuterAbstractBase if(recipients.size() > 0) { - message.setTo(recipients.toArray(new String[recipients.size()])); + messageRef[0].setTo(recipients.toArray(new String[recipients.size()])); } else { @@ -487,7 +499,7 @@ public class MailActionExecuter extends ActionExecuterAbstractBase { logger.debug("from specified as a parameter, from:" + from); } - message.setFrom(from); + messageRef[0].setFrom(from); } else { @@ -504,12 +516,12 @@ public class MailActionExecuter extends ActionExecuterAbstractBase { logger.debug("looked up email address for :" + fromPerson + " email from " + fromActualUser); } - message.setFrom(fromActualUser); + messageRef[0].setFrom(fromActualUser); } else { // from system or user does not have email address - message.setFrom(fromDefaultAddress); + messageRef[0].setFrom(fromDefaultAddress); } } @@ -521,14 +533,14 @@ public class MailActionExecuter extends ActionExecuterAbstractBase logger.debug("from not enabled - sending from default address:" + fromDefaultAddress); } // from is not enabled. - message.setFrom(fromDefaultAddress); + messageRef[0].setFrom(fromDefaultAddress); } // set subject line - message.setSubject((String)ruleAction.getParameterValue(PARAM_SUBJECT)); + messageRef[0].setSubject((String)ruleAction.getParameterValue(PARAM_SUBJECT)); // See if an email template has been specified String text = null; @@ -587,24 +599,41 @@ public class MailActionExecuter extends ActionExecuterAbstractBase if (text != null) { - message.setText(text, isHTML); + messageRef[0].setText(text, isHTML); } } }; + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try + { + mailPreparer.prepare(mimeMessage); + } catch (Exception e) + { + // We're forced to catch java.lang.Exception here. Urgh. + if (logger.isInfoEnabled()) + { + logger.warn("Unable to prepare mail message. Skipping.", e); + } + } + return messageRef[0]; + } + + private void sendEmail(final Action ruleAction, final NodeRef actionedUponNodeRef, MimeMessageHelper preparedMessage) + { try { // Send the message unless we are in "testMode" - if(!testMode) + if(!testMode && preparedMessage != null) { - javaMailSender.send(mailPreparer); + javaMailSender.send(preparedMessage.getMimeMessage()); } - else + else if (validNodeRefIfPresent(actionedUponNodeRef)) { try { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); - mailPreparer.prepare(mimeMessage); + prepareEmail(ruleAction, actionedUponNodeRef); lastTestMessage = mimeMessage; } catch(Exception e) { System.err.println(e); diff --git a/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java b/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java index 669580f5a8..588b71fb14 100644 --- a/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java +++ b/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java @@ -81,6 +81,11 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase private CopyService copyService; private MimetypeService mimetypeService; + /** + * Properties (needed to avoid changing method signatures) + */ + protected TransformationOptions options; + /** * Set the mime type service */ @@ -172,8 +177,7 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase throw new RuleServiceException(CONTENT_READER_NOT_FOUND_MESSAGE); } - TransformationOptions options = new TransformationOptions(); - options.setSourceNodeRef(actionedUponNodeRef); + options = newTransformationOptions(ruleAction, actionedUponNodeRef); if (null == contentService.getTransformer(contentReader.getContentUrl(), contentReader.getMimetype(), contentReader.getSize(), mimeType, options)) { throw new RuleServiceException(String.format(TRANSFORMER_NOT_EXISTS_MESSAGE_PATTERN, contentReader.getMimetype(), mimeType)); @@ -243,7 +247,6 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase } } - boolean newCopy = false; if (copyNodeRef == null) { // Copy the content node @@ -253,11 +256,7 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase destinationAssocTypeQName, destinationAssocQName, false); - newCopy = true; - } - - if (newCopy == true) - { + // Adjust the name of the copy nodeService.setProperty(copyNodeRef, ContentModel.PROP_NAME, newName); String originalTitle = (String)nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_TITLE); @@ -282,6 +281,7 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase // TODO: Check failure patterns for actions. try { + options.setTargetNodeRef(copyNodeRef); doTransform(ruleAction, actionedUponNodeRef, contentReader, copyNodeRef, contentWriter); } catch(NoTransformerException e) @@ -296,6 +296,11 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase throw new RuleServiceException(TRANSFORMING_ERROR_MESSAGE + e.getMessage()); } } + } + + protected TransformationOptions newTransformationOptions(Action ruleAction, NodeRef sourceNodeRef) + { + return new TransformationOptions(sourceNodeRef, ContentModel.PROP_NAME, null, ContentModel.PROP_NAME); } /** @@ -306,10 +311,6 @@ public class TransformActionExecuter extends ActionExecuterAbstractBase NodeRef destinationNodeRef, ContentWriter contentWriter) { - // Transformation options - TransformationOptions options = new TransformationOptions( - sourceNodeRef, ContentModel.PROP_NAME, destinationNodeRef, ContentModel.PROP_NAME); - // transform - will throw NoTransformerException if there are no transformers this.contentService.transform(contentReader, contentWriter, options); } diff --git a/source/java/org/alfresco/repo/activities/feed/ErrorProneActionExecutor.java b/source/java/org/alfresco/repo/activities/feed/ErrorProneActionExecutor.java new file mode 100644 index 0000000000..1290a6c7c6 --- /dev/null +++ b/source/java/org/alfresco/repo/activities/feed/ErrorProneActionExecutor.java @@ -0,0 +1,97 @@ +package org.alfresco.repo.activities.feed; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; +import org.alfresco.repo.action.executer.TestModeable; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +public class ErrorProneActionExecutor extends ActionExecuterAbstractBase + implements InitializingBean, TestModeable +{ + private static Log logger = LogFactory.getLog(ErrorProneActionExecutor.class); + + public static final String PARAM_FAILING_PERSON_NODEREF = "failingPersonNodeRef"; + public static final String PARAM_PERSON_NODEREF = "personNodeRef"; + public static final String PARAM_USERNAME = "userName"; + + public static final String NAME = "errorProneActionExecutor"; + + // count of number of successful notifications + private AtomicInteger numSuccessful = new AtomicInteger(); + + // count of number of failed notifications + private AtomicInteger numFailed = new AtomicInteger(); + + public int getNumSuccess() + { + return numSuccessful.get(); + } + + public int getNumFailed() + { + return numFailed.get(); + } + + /** + * Send an email message + * + * @throws AlfrescoRuntimeExeption + */ + @Override + protected void executeImpl( + final Action ruleAction, + final NodeRef actionedUponNodeRef) + { + NodeRef failingPersonNodeRef = (NodeRef)ruleAction.getParameterValue(PARAM_FAILING_PERSON_NODEREF); + NodeRef personNodeRef = (NodeRef)ruleAction.getParameterValue(PARAM_PERSON_NODEREF); + String userName = (String)ruleAction.getParameterValue(PARAM_USERNAME); + + System.out.println("userName = " + userName); + + if(personNodeRef.equals(failingPersonNodeRef)) + { + numFailed.incrementAndGet(); + throw new AlfrescoRuntimeException(""); + } + + numSuccessful.incrementAndGet(); + } + + /** + * Add the parameter definitions + */ + @Override + protected void addParameterDefinitions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_FAILING_PERSON_NODEREF, DataTypeDefinition.NODE_REF, true, "Failing Person NodeRef")); + paramList.add(new ParameterDefinitionImpl(PARAM_PERSON_NODEREF, DataTypeDefinition.NODE_REF, true, "Person NodeRef")); + paramList.add(new ParameterDefinitionImpl(PARAM_USERNAME, DataTypeDefinition.TEXT, true, "Username")); + } + + @Override + public boolean isTestMode() + { + return true; + } + + @Override + public void setTestMode(boolean testMode) + { + } + + @Override + public void afterPropertiesSet() throws Exception + { + + } +} diff --git a/source/java/org/alfresco/repo/activities/feed/ErrorProneUserNotifier.java b/source/java/org/alfresco/repo/activities/feed/ErrorProneUserNotifier.java new file mode 100644 index 0000000000..7feb812d46 --- /dev/null +++ b/source/java/org/alfresco/repo/activities/feed/ErrorProneUserNotifier.java @@ -0,0 +1,94 @@ +package org.alfresco.repo.activities.feed; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.executer.MailActionExecuter; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +public class ErrorProneUserNotifier extends AbstractUserNotifier +{ +// private AtomicInteger numSuccessful = new AtomicInteger(); +// private AtomicInteger numFailed = new AtomicInteger(); + + private NodeRef failingPersonNodeRef; + private ActionService actionService; + + public ErrorProneUserNotifier(NodeRef failingPersonNodeRef) + { + this.failingPersonNodeRef = failingPersonNodeRef; + } + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + @Override + protected boolean skipUser(NodeRef personNodeRef) + { + return false; + } + + @Override + protected Long getFeedId(NodeRef personNodeRef) + { + Map personProps = nodeService.getProperties(personNodeRef); + + // where did we get up to ? + Long emailFeedDBID = (Long)personProps.get(ContentModel.PROP_EMAIL_FEED_ID); + if (emailFeedDBID != null) + { + // increment min feed id + emailFeedDBID++; + } + else + { + emailFeedDBID = -1L; + } + + return emailFeedDBID; + } + +// public int getNumSuccess() +// { +// return numSuccessful.get(); +// } +// +// public int getNumFailed() +// { +// return numFailed.get(); +// } + + @Override + protected void notifyUser(NodeRef personNodeRef, String subjectText, + Map model, NodeRef templateNodeRef) + { +// super.notifyUser(personNodeRef, subjectText, model, templateNodeRef); + + String userName = (String)nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); + + Action action = actionService.createAction(ErrorProneActionExecutor.NAME); + + action.setParameterValue(ErrorProneActionExecutor.PARAM_FAILING_PERSON_NODEREF, failingPersonNodeRef); + action.setParameterValue(ErrorProneActionExecutor.PARAM_PERSON_NODEREF, personNodeRef); + action.setParameterValue(ErrorProneActionExecutor.PARAM_USERNAME, userName); + + actionService.executeAction(action, null); +// String userName = (String)nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); +// +// System.out.println("userName = " + userName); +// +// if(personNodeRef.equals(failingPersonNodeRef)) +// { +// numFailed.incrementAndGet(); +// throw new AlfrescoRuntimeException(""); +// } +// +// numSuccessful.incrementAndGet(); + } +} diff --git a/source/java/org/alfresco/repo/activities/feed/FeedNotifierImpl.java b/source/java/org/alfresco/repo/activities/feed/FeedNotifierImpl.java index 68a3d26d14..4055120a9a 100644 --- a/source/java/org/alfresco/repo/activities/feed/FeedNotifierImpl.java +++ b/source/java/org/alfresco/repo/activities/feed/FeedNotifierImpl.java @@ -17,6 +17,7 @@ import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.admin.RepoAdminService; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.repository.InvalidNodeRefException; @@ -88,7 +89,7 @@ public class FeedNotifierImpl implements FeedNotifier, ApplicationContextAware this.batchSize = batchSize; } - public void setUserNotifier(UserNotifier userNotifier) + public void setUserNotifier(UserNotifier userNotifier) { this.userNotifier = userNotifier; } @@ -165,6 +166,11 @@ public class FeedNotifierImpl implements FeedNotifier, ApplicationContextAware } String lockToken = getLock(LOCK_TTL); + if (lockToken == null) + { + logger.info("Can't get lock. Assume multiple feed notifiers..."); + return; + } try { @@ -281,6 +287,21 @@ public class FeedNotifierImpl implements FeedNotifier, ApplicationContextAware public void process(final PersonInfo person) throws Throwable { + final RetryingTransactionHelper txHelper = transactionService.getRetryingTransactionHelper(); + txHelper.setMaxRetries(0); + + txHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + processInternal(person); + return null; + } + }, false, true); + } + + private void processInternal(final PersonInfo person) throws Throwable + { final NodeRef personNodeRef = person.getNodeRef(); try @@ -306,7 +327,7 @@ public class FeedNotifierImpl implements FeedNotifier, ApplicationContextAware // skip this person - eg. no longer exists ? logger.warn("Skip feed notification for user ("+personNodeRef+"): " + inre.getMessage()); } - } + } }; // grab people for the batch processor in chunks of size batchSize diff --git a/source/java/org/alfresco/repo/activities/feed/FeedNotifierTest.java b/source/java/org/alfresco/repo/activities/feed/FeedNotifierTest.java new file mode 100644 index 0000000000..d5ccb0ec60 --- /dev/null +++ b/source/java/org/alfresco/repo/activities/feed/FeedNotifierTest.java @@ -0,0 +1,243 @@ +package org.alfresco.repo.activities.feed; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.activities.post.lookup.PostLookup; +import org.alfresco.repo.domain.activities.ActivityPostDAO; +import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.admin.RepoAdminService; +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.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.quartz.Scheduler; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.JobDetailBean; + +/** + * Feed notifier tests. + * + * @author steveglover + * + */ +public class FeedNotifierTest +{ + private static ApplicationContext ctx = null; + + private PersonService personService; + private NodeService nodeService; + private NamespaceService namespaceService; + private SiteService siteService; + private ActivityService activityService; + private RepoAdminService repoAdminService; + private FeedNotifierImpl feedNotifier; + private TransactionService transactionService; + private PostLookup postLookup; + private FeedGenerator feedGenerator; + private FileFolderService fileFolderService; + private SubscriptionService subscriptionService; + private ErrorProneActionExecutor errorProneActionExecutor; + private ActivityPostDAO postDAO; + private ActionService actionService; + + private ErrorProneUserNotifier userNotifier; + + private NodeRef failingPersonNodeRef; + private NodeRef personNodeRef; + private String userName1 = "user1." + GUID.generate(); + private String userName2 = "user2." + GUID.generate(); + + @BeforeClass + public static void init() + { + ApplicationContextHelper.setUseLazyLoading(false); + ApplicationContextHelper.setNoAutoStart(true); + + ctx = ApplicationContextHelper.getApplicationContext(); + } + + @Before + public void before() throws Exception + { + ChildApplicationContextFactory activitiesFeed = (ChildApplicationContextFactory)ctx.getBean("ActivitiesFeed"); + ApplicationContext activitiesFeedCtx = activitiesFeed.getApplicationContext(); + this.feedNotifier = (FeedNotifierImpl)activitiesFeedCtx.getBean("feedNotifier"); + this.activityService = (ActivityService)activitiesFeedCtx.getBean("activityService"); + this.postLookup = (PostLookup)activitiesFeedCtx.getBean("postLookup"); + this.feedGenerator = (FeedGenerator)activitiesFeedCtx.getBean("feedGenerator"); + + Scheduler scheduler = (Scheduler)ctx.getBean("schedulerFactory"); + + JobDetailBean feedGeneratorJobDetail = (JobDetailBean)activitiesFeedCtx.getBean("feedGeneratorJobDetail"); + JobDetailBean postLookupJobDetail = (JobDetailBean)activitiesFeedCtx.getBean("postLookupJobDetail"); + JobDetailBean feedCleanerJobDetail = (JobDetailBean)activitiesFeedCtx.getBean("feedCleanerJobDetail"); + JobDetailBean postCleanerJobDetail = (JobDetailBean)activitiesFeedCtx.getBean("postCleanerJobDetail"); + JobDetailBean feedNotifierJobDetail = (JobDetailBean)activitiesFeedCtx.getBean("feedNotifierJobDetail"); + + // Pause activities jobs so that we aren't competing with their scheduled versions + scheduler.pauseJob(feedGeneratorJobDetail.getName(), feedGeneratorJobDetail.getGroup()); + scheduler.pauseJob(postLookupJobDetail.getName(), postLookupJobDetail.getGroup()); + scheduler.pauseJob(feedCleanerJobDetail.getName(), feedCleanerJobDetail.getGroup()); + scheduler.pauseJob(postCleanerJobDetail.getName(), postCleanerJobDetail.getGroup()); + scheduler.pauseJob(feedNotifierJobDetail.getName(), feedNotifierJobDetail.getGroup()); + + this.personService = (PersonService)ctx.getBean("personService"); + this.nodeService = (NodeService)ctx.getBean("nodeService"); + this.namespaceService = (NamespaceService)ctx.getBean("namespaceService"); + this.siteService = (SiteService)ctx.getBean("siteService"); + this.repoAdminService = (RepoAdminService)ctx.getBean("repoAdminService"); + this.transactionService = (TransactionService)ctx.getBean("transactionService"); + this.postDAO = (ActivityPostDAO)ctx.getBean("postDAO"); + this.fileFolderService = (FileFolderService)ctx.getBean("fileFolderService"); + this.subscriptionService = (SubscriptionService)ctx.getBean("SubscriptionService"); + this.errorProneActionExecutor = (ErrorProneActionExecutor)ctx.getBean("errorProneActionExecutor"); + this.actionService = (ActionService)ctx.getBean("ActionService"); + + // create some users + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("synthetic-access") + public Void execute() throws Throwable + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // create person properties + PropertyMap personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, userName1); + personProps.put(ContentModel.PROP_FIRSTNAME, userName1); + personProps.put(ContentModel.PROP_LASTNAME, userName1); + personProps.put(ContentModel.PROP_EMAIL, userName1+"@email.com"); + personNodeRef = personService.createPerson(personProps); + + personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, userName2); + personProps.put(ContentModel.PROP_FIRSTNAME, userName2); + personProps.put(ContentModel.PROP_LASTNAME, userName2); + personProps.put(ContentModel.PROP_EMAIL, userName2+"@email.com"); + failingPersonNodeRef = personService.createPerson(personProps); + + AuthenticationUtil.popAuthentication(); + + return null; + } + }, false, true); + + // and some activities for those users + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("synthetic-access") + public Void execute() throws Throwable + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(userName1); + + NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + NodeRef workingRootNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.ALFRESCO_URI, "working root"), + ContentModel.TYPE_FOLDER).getChildRef(); + FileInfo file1 = fileFolderService.create(workingRootNodeRef, GUID.generate(), ContentModel.TYPE_CONTENT); + + // ensure at least 3 activities + JSONObject activityData = new JSONObject(); + activityData.put("title", GUID.generate()); + activityData.put("nodeRef", file1.getNodeRef()); + activityService.postActivity("org.alfresco.documentlibrary.file-added", null, "documentlibrary", activityData.toString(), userName1); + + AuthenticationUtil.popAuthentication(); + + return null; + } + }, false, true); + + // one of the users follows the other user (so that we can test that notification failures for one of the users does not + // affect other users) + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("synthetic-access") + public Void execute() throws Throwable + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(userName2); + + subscriptionService.follow(userName2, userName1); + + AuthenticationUtil.popAuthentication(); + + return null; + } + }, false, true); + + generateActivities(); + + // use our own user notifier for testing purposes + this.userNotifier = new ErrorProneUserNotifier(failingPersonNodeRef); + userNotifier.setNodeService(nodeService); + userNotifier.setNamespaceService(namespaceService); + userNotifier.setSiteService(siteService); + userNotifier.setActivityService(activityService); + userNotifier.setRepoAdminService(repoAdminService); + userNotifier.setActionService(actionService); + feedNotifier.setUserNotifier(userNotifier); + } + + private void generateActivities() throws Exception + { + // generate the activities + postLookup.execute(); + + Long maxSequence = postDAO.getMaxActivitySeq(); + while(maxSequence != null) + { + feedGenerator.execute(); + + maxSequence = postDAO.getMaxActivitySeq(); + } + } + + /** + * ALF-16155 test + */ + @Test + public void testFailedNotifications() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // execute the notifier, counting the number of successful and unsuccessful notifications + assertEquals(0, errorProneActionExecutor.getNumSuccess()); + assertEquals(0, errorProneActionExecutor.getNumFailed()); + feedNotifier.execute(1); + assertEquals(1, errorProneActionExecutor.getNumFailed()); // should be a single notification failure + assertTrue(errorProneActionExecutor.getNumSuccess() > 0); // others should have gone through ok + int numSuccessfulNotifications = errorProneActionExecutor.getNumSuccess(); + + // execute the notifier again, checking that there are no more notifications + feedNotifier.execute(1); + // should still be failing + assertEquals(2, errorProneActionExecutor.getNumFailed()); + // there should not be any more because they have been processed already + assertEquals(numSuccessfulNotifications, errorProneActionExecutor.getNumSuccess()); + } +} diff --git a/source/java/org/alfresco/repo/cache/TransactionalCache.java b/source/java/org/alfresco/repo/cache/TransactionalCache.java index 534ca57d57..a3761e55f8 100644 --- a/source/java/org/alfresco/repo/cache/TransactionalCache.java +++ b/source/java/org/alfresco/repo/cache/TransactionalCache.java @@ -19,6 +19,7 @@ package org.alfresco.repo.cache; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; @@ -266,10 +267,52 @@ public class TransactionalCache * * @param noSharedCacheRead true to avoid reading from the shared cache for the transaction */ + @SuppressWarnings("unchecked") public void setDisableSharedCacheReadForTransaction(boolean noSharedCacheRead) { TransactionData txnData = getTransactionData(); - txnData.noSharedCacheRead = noSharedCacheRead; + + // If we are switching on noSharedCacheRead mode, convert all existing reads and updates to avoid 'consistent + // read' behaviour giving us a potentially out of date node already accessed + if (noSharedCacheRead && !txnData.noSharedCacheRead) + { + txnData.noSharedCacheRead = noSharedCacheRead; + String currentCacheRegion = TenantUtil.getCurrentDomain(); + for (Map.Entry> entry : new ArrayList>>( + txnData.updatedItemsCache.entrySet())) + { + Serializable cacheKey = entry.getKey(); + K key = null; + if (cacheKey instanceof CacheRegionKey) + { + CacheRegionKey cacheRegionKey = (CacheRegionKey) cacheKey; + if (currentCacheRegion.equals(cacheRegionKey.getCacheRegion())) + { + key = (K) cacheRegionKey.getCacheKey(); + } + } + else + { + key = (K) cacheKey; + } + + if (key != null) + { + CacheBucket bucket = entry.getValue(); + // Simply 'forget' reads + if (bucket instanceof ReadCacheBucket) + { + txnData.updatedItemsCache.remove(cacheKey); + } + // Convert updates to removes + else if (bucket instanceof UpdateCacheBucket) + { + remove(key); + } + // Leave new entries alone - they can't have come from the shared cache + } + } + } } /** diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java index 98bdc6838b..2f8577d8cc 100644 --- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java @@ -4235,6 +4235,54 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO } } + @Override + public Set getCachedAncestors(List nodeIds) + { + // First, make sure 'level 1' nodes and their parents are in the cache + cacheNodesById(nodeIds); + for (Long nodeId : nodeIds) + { + // Filter out deleted nodes + if (exists(nodeId)) + { + getParentAssocsCached(nodeId); + } + } + // Now recurse on all ancestors in the cache + Set ancestors = new TreeSet(); + for (Long nodeId : nodeIds) + { + findCachedAncestors(nodeId, ancestors); + } + return ancestors; + } + + /** + * Uses the node and parent assocs cache content to recursively find the set of currently cached ancestor node IDs + */ + private void findCachedAncestors(Long nodeId, Set ancestors) + { + if (!ancestors.add(nodeId)) + { + return; // Already visited + } + Node node = nodesCache.getValue(nodeId); + if (node == null) + { + return; // Not in cache yet - will load in due course + } + Pair cacheKey = new Pair(nodeId, node.getTransaction().getChangeTxnId()); + ParentAssocsInfo value = parentAssocsCache.get(cacheKey); + if (value == null) + { + return; // Not in cache yet - will load in due course + } + for (ChildAssocEntity childAssoc : value.getParentAssocs().values()) + { + findCachedAncestors(childAssoc.getParentNode().getId(), ancestors); + } + } + @Override public void cacheNodesById(List nodeIds) { diff --git a/source/java/org/alfresco/repo/domain/permissions/AbstractAclCrudDAOImpl.java b/source/java/org/alfresco/repo/domain/permissions/AbstractAclCrudDAOImpl.java index 1ea9317d2b..cc612183eb 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AbstractAclCrudDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/permissions/AbstractAclCrudDAOImpl.java @@ -26,6 +26,7 @@ import java.util.Map; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.cache.TransactionalCache; import org.alfresco.repo.cache.lookup.EntityLookupCache; import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAO; import org.alfresco.repo.domain.CrcHelper; @@ -94,6 +95,12 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO */ private EntityLookupCache aclEntityCache; + /** + * Backing transactional cache to allow read-through requests to be honoured + */ + private TransactionalCache aclEntityTransactionalCache; + + /** * Cache for the Authority entity:
* KEY: ID (Authority)
@@ -115,12 +122,13 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO * * @param aclEntityCache the cache of IDs to AclEntities */ - public void setAclEntityCache(SimpleCache aclEntityCache) + public void setAclEntityCache(TransactionalCache aclEntityCache) { this.aclEntityCache = new EntityLookupCache( aclEntityCache, CACHE_REGION_ACL, aclEntityDaoCallback); + this.aclEntityTransactionalCache = aclEntityCache; } /** @@ -200,6 +208,12 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO return entityPair.getSecond(); } + @Override + public void setCheckAclConsistency() + { + aclEntityTransactionalCache.setDisableSharedCacheReadForTransaction(true); + } + public AclUpdateEntity getAclForUpdate(long id) { AclEntity acl = getAclImpl(id); diff --git a/source/java/org/alfresco/repo/domain/permissions/AclCrudDAO.java b/source/java/org/alfresco/repo/domain/permissions/AclCrudDAO.java index b68a6ab7bc..bc61c9bb8b 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AclCrudDAO.java +++ b/source/java/org/alfresco/repo/domain/permissions/AclCrudDAO.java @@ -45,6 +45,12 @@ import org.alfresco.util.Pair; */ public interface AclCrudDAO { + /** + * Transaction-scope setting to make the DAO guarantee the validity of all caches: some cache data will be reloaded; + * some cache data will be considered safe. + */ + public void setCheckAclConsistency(); + // // Access Control List (ACL) // diff --git a/source/java/org/alfresco/repo/domain/permissions/AclDAO.java b/source/java/org/alfresco/repo/domain/permissions/AclDAO.java index b455c7eafc..ce8b5453ad 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AclDAO.java +++ b/source/java/org/alfresco/repo/domain/permissions/AclDAO.java @@ -34,6 +34,12 @@ import org.alfresco.repo.security.permissions.impl.AclChange; */ public interface AclDAO { + /** + * Transaction-scope setting to make the DAO guarantee the validity of all caches: some cache data will be reloaded; + * some cache data will be considered safe. + */ + public void setCheckAclConsistency(); + /** * Get an ACL (including entries) */ diff --git a/source/java/org/alfresco/repo/domain/permissions/AclDAOImpl.java b/source/java/org/alfresco/repo/domain/permissions/AclDAOImpl.java index f0127c2cdb..40b9acae39 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AclDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/permissions/AclDAOImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -21,6 +21,8 @@ package org.alfresco.repo.domain.permissions; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -70,11 +72,8 @@ public class AclDAOImpl implements AclDAO private AclCrudDAO aclCrudDAO; private NodeDAO nodeDAO; private TenantService tenantService; - private SimpleCache aclCache; - private SimpleCache> readersCache; + private SimpleCache aclCache; - private SimpleCache> readersDeniedCache; - private enum WriteMode { /** @@ -132,27 +131,11 @@ public class AclDAOImpl implements AclDAO * * @param aclCache */ - public void setAclCache(SimpleCache aclCache) + public void setAclCache(SimpleCache aclCache) { this.aclCache = aclCache; } - /** - * @param readersCache the readersCache to set - */ - public void setReadersCache(SimpleCache> readersCache) - { - this.readersCache = readersCache; - } - - /** - * @param readersDeniedCache the readersDeniedCache to set - */ - public void setReadersDeniedCache(SimpleCache> readersDeniedCache) - { - this.readersDeniedCache = readersDeniedCache; - } - /** * {@inheritDoc} */ @@ -348,8 +331,8 @@ public class AclDAOImpl implements AclDAO } getWritable(created, toInherit, excluded, toAdd, toInherit, false, changes, WriteMode.CREATE_AND_INHERIT); - - return createdAcl; + // Fetch an up-to-date version + return getAcl(created); } private void getWritable( @@ -442,9 +425,6 @@ public class AclDAOImpl implements AclDAO AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id); if (!acl.isLatest()) { - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()); } @@ -493,9 +473,6 @@ public class AclDAOImpl implements AclDAO } acl.setAclChangeSetId(getCurrentChangeSetId()); aclCrudDAO.updateAcl(acl); - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()); } else if ((acl.getAclChangeSetId() == getCurrentChangeSetId()) && (!requiresVersion) && (!acl.getRequiresVersion())) @@ -533,9 +510,6 @@ public class AclDAOImpl implements AclDAO acl.setInheritsFrom(inheritsFrom); } aclCrudDAO.updateAcl(acl); - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()); } else @@ -623,9 +597,6 @@ public class AclDAOImpl implements AclDAO acl.setLatest(Boolean.FALSE); acl.setRequiresVersion(Boolean.FALSE); aclCrudDAO.updateAcl(acl); - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); return new AclChangeImpl(id, created, acl.getAclType(), newAcl.getAclType()); } } @@ -751,13 +722,13 @@ public class AclDAOImpl implements AclDAO @Override public List deleteAccessControlEntries(final String authority) { - List acls = new ArrayList(); + List aclChanges = new LinkedList(); // get authority Authority authEntity = aclCrudDAO.getAuthority(authority); if (authEntity == null) { - return acls; + return aclChanges; } List aces = new ArrayList(); @@ -767,6 +738,7 @@ public class AclDAOImpl implements AclDAO boolean leaveAuthority = false; if (members.size() > 0) { + Set acls = new HashSet(members.size() * 2); List membersToDelete = new ArrayList(members.size()); // fix up members and extract acls and aces @@ -817,12 +789,9 @@ public class AclDAOImpl implements AclDAO if (!hasAnotherTenantNodes) { - aclCache.remove(aclId); - readersCache.remove(aclId); - readersDeniedCache.remove(aclId); - - Acl list = aclCrudDAO.getAcl(aclId); - acls.add(new AclChangeImpl(aclId, aclId, list.getAclType(), list.getAclType())); + AclUpdateEntity list = aclCrudDAO.getAclForUpdate(aclId); + aclChanges.add(new AclChangeImpl(aclId, aclId, list.getAclType(), list.getAclType())); + acls.add(list); membersToDelete.add(aclMemberId); aces.add((Long)aceId); } @@ -830,6 +799,13 @@ public class AclDAOImpl implements AclDAO // delete list of acl members aclCrudDAO.deleteAclMembers(membersToDelete); + + // Remember to 'touch' all affected ACLs + for (AclUpdateEntity acl : acls) + { + acl.setAclChangeSetId(getCurrentChangeSetId()); + aclCrudDAO.updateAcl(acl); + } } if (!leaveAuthority) @@ -859,7 +835,7 @@ public class AclDAOImpl implements AclDAO } } - return acls; + return aclChanges; } /** @@ -874,10 +850,6 @@ public class AclDAOImpl implements AclDAO // delete acl members & acl aclCrudDAO.deleteAclMembersByAcl(aclId); aclCrudDAO.deleteAcl(aclId); - - aclCache.remove(aclId); - readersCache.remove(aclId); - readersDeniedCache.remove(aclId); } if (dbAcl.getAclType() == ACLType.SHARED) { @@ -893,10 +865,6 @@ public class AclDAOImpl implements AclDAO // delete acl members & acl aclCrudDAO.deleteAclMembersByAcl(aclId); aclCrudDAO.deleteAcl(aclId); - - aclCache.remove(aclId); - readersCache.remove(aclId); - readersDeniedCache.remove(aclId); } } else @@ -1008,10 +976,6 @@ public class AclDAOImpl implements AclDAO aclCrudDAO.deleteAcl(acl.getId()); } - // remove the deleted acl from the cache - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); acls.add(new AclChangeImpl(id, null, acl.getAclType(), null)); return acls; } @@ -1075,37 +1039,31 @@ public class AclDAOImpl implements AclDAO return aclCrudDAO.getAcl(id); } + @Override + public void setCheckAclConsistency() + { + aclCrudDAO.setCheckAclConsistency(); + } + /** * {@inheritDoc} */ @Override public AccessControlList getAccessControlList(Long id) { - AccessControlList acl = aclCache.get(id); - if (acl == null) - { - acl = getAccessControlListImpl(id); - aclCache.put(id, acl); - } - else - { - // System.out.println("Used cache for "+id); - } - return acl; - } - - /** - * @return the access control list - */ - private AccessControlList getAccessControlListImpl(final Long id) - { - SimpleAccessControlList acl = new SimpleAccessControlList(); + // Used the cached properties as our cache key AccessControlListProperties properties = getAccessControlListProperties(id); if (properties == null) { return null; } + AccessControlList aclCached = aclCache.get((Serializable)properties); + if (aclCached != null) + { + return aclCached; + } + SimpleAccessControlList acl = new SimpleAccessControlList(); acl.setProperties(properties); List> results = aclCrudDAO.getAcesAndAuthoritiesByAcl(id); @@ -1143,6 +1101,9 @@ public class AclDAOImpl implements AclDAO Collections.sort(entries); acl.setEntries(entries); + + // Cache it for next time + aclCache.put((Serializable)properties, acl); return acl; } @@ -1153,7 +1114,6 @@ public class AclDAOImpl implements AclDAO @Override public Long getInheritedAccessControlList(Long id) { - aclCache.remove(id); AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id); if (acl.getAclType() == ACLType.OLD) { @@ -1355,9 +1315,6 @@ public class AclDAOImpl implements AclDAO acl.setInherits(Boolean.TRUE); acl.setAclChangeSetId(getCurrentChangeSetId()); aclCrudDAO.updateAcl(acl); - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); changes.add(new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType())); return changes; case SHARED: @@ -1394,7 +1351,6 @@ public class AclDAOImpl implements AclDAO @Override public List disableInheritance(Long id, boolean setInheritedOnAcl) { - aclCache.remove(id); AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id); List changes = new ArrayList(1); switch (acl.getAclType()) @@ -1406,9 +1362,6 @@ public class AclDAOImpl implements AclDAO acl.setInherits(Boolean.FALSE); acl.setAclChangeSetId(getCurrentChangeSetId()); aclCrudDAO.updateAcl(acl); - aclCache.remove(id); - readersCache.remove(id); - readersDeniedCache.remove(id); changes.add(new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType())); return changes; case SHARED: @@ -1442,9 +1395,6 @@ public class AclDAOImpl implements AclDAO aclToCopy.setRequiresVersion(true); aclToCopy.setAclChangeSetId(getCurrentChangeSetId()); aclCrudDAO.updateAcl(aclToCopy); - aclCache.remove(toCopy); - readersCache.remove(toCopy); - readersDeniedCache.remove(toCopy); inheritedId = getInheritedAccessControlList(toCopy); if ((inheritedId != null) && (!inheritedId.equals(toCopy))) { @@ -1452,9 +1402,6 @@ public class AclDAOImpl implements AclDAO inheritedAcl.setRequiresVersion(true); inheritedAcl.setAclChangeSetId(getCurrentChangeSetId()); aclCrudDAO.updateAcl(inheritedAcl); - aclCache.remove(inheritedId); - readersCache.remove(inheritedId); - readersDeniedCache.remove(inheritedId); } return toCopy; case REDIRECT: diff --git a/source/java/org/alfresco/repo/domain/permissions/AclEntity.java b/source/java/org/alfresco/repo/domain/permissions/AclEntity.java index 437851c1ce..b947e13a15 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AclEntity.java +++ b/source/java/org/alfresco/repo/domain/permissions/AclEntity.java @@ -199,9 +199,13 @@ public class AclEntity implements Acl, Serializable @Override public int hashCode() { - return (id == null ? 0 : id.hashCode()); + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; } - + @Override public boolean equals(Object obj) { @@ -211,8 +215,9 @@ public class AclEntity implements Acl, Serializable } else if (obj instanceof AclEntity) { - AclEntity that = (AclEntity)obj; - return (EqualsHelper.nullSafeEquals(this.id, that.id)); + AclEntity that = (AclEntity) obj; + return EqualsHelper.nullSafeEquals(this.id, that.id) + && EqualsHelper.nullSafeEquals(this.version, that.version); } else { diff --git a/source/java/org/alfresco/repo/forms/processor/node/ContentModelFormProcessor.java b/source/java/org/alfresco/repo/forms/processor/node/ContentModelFormProcessor.java index c10d4f2e05..da1485cd75 100644 --- a/source/java/org/alfresco/repo/forms/processor/node/ContentModelFormProcessor.java +++ b/source/java/org/alfresco/repo/forms/processor/node/ContentModelFormProcessor.java @@ -48,6 +48,7 @@ import org.alfresco.repo.forms.processor.FilteredFormProcessor; import org.alfresco.repo.forms.processor.FormCreationData; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.ConstraintDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -502,21 +503,36 @@ public abstract class ContentModelFormProcessor extends // ensure that the association being persisted is defined in the model AssociationDefinition assocDef = assocDefs.get(fullQName); - // TODO: if the association is not defined on the node, check for the association - // in all models, however, the source of an association can be critical so we - // can't just look up the association in the model regardless. We need to - // either check the source class of the node and the assoc def match or we - // check that the association was defined as part of an aspect (where by it's - // nature can have any source type) - + // If the association is not defined on this node i.e. it is not defined for this node's content + // type and is not defined for any of the aspects that have already been applied to this node, + // then assocDef here will be null. This is because assocDef is passed in to this method, its value + // having been obtained from dictionaryService.getAnonymousType() + // + // However if the association type is defined on an aspect which has not yet been applied to this node + // then setting a value for this association should lead to the automatic application of the aspect defining it. if (assocDef == null) { - if (getLogger().isWarnEnabled()) - { - getLogger().warn("Ignoring field '" + fieldName + "' as an association definition can not be found"); - } + // Is it defined on any other type? + AssociationDefinition assocDefFromDictionary = this.dictionaryService.getAssociation(fullQName); - return; + // If the association is defined on any *aspect* type... + if (assocDefFromDictionary != null && assocDefFromDictionary.getSourceClass().isAspect()) + { + // ... then it should be safe to proceed with applying the association value. + assocDef = assocDefFromDictionary; + } + else + { + // ... else it is either undefined in the dictionary service or is defined on a concrete + // content type which is inconsistent with the node's type. We'll ignore it, which is what + // has been done with unhandled associations up to this point. + if (getLogger().isWarnEnabled()) + { + getLogger().warn("Ignoring field '" + fieldName + "' as a valid association definition can not be found"); + } + + return; + } } String value = (String) fieldData.getValue(); diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index dd1d3bd28a..1aa8f6b162 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java @@ -344,7 +344,9 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab } else { - serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, null); + String fileName = (String) serviceRegistry.getNodeService().getProperty(sourceMessageFileInfo.getNodeRef(), ContentModel.PROP_NAME); + String newFileName = imapService.generateUniqueFilename(destFolderNodeRef, fileName); + serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, newFileName); } } diff --git a/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java b/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java index 9c4b75b59c..3deb1d0521 100644 --- a/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java +++ b/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java @@ -71,6 +71,7 @@ public class AttachmentsExtractor private FileFolderService fileFolderService; private NodeService nodeService; + private ImapService imapService; private ServiceRegistry serviceRegistry; private RepositoryFolderConfigBean attachmentsFolder; private NodeRef attachmentsFolderRef; @@ -84,6 +85,11 @@ public class AttachmentsExtractor public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; + } + + public void setImapService(ImapService imapService) + { + this.imapService = imapService; } public void setAttachmentsFolder(RepositoryFolderConfigBean attachmentsFolder) @@ -193,23 +199,11 @@ public class AttachmentsExtractor } else { - String name = fileName; - String ext = ""; - if (fileName.lastIndexOf(".") != -1) - { - int index = fileName.lastIndexOf("."); - name = fileName.substring(0, index); - ext = fileName.substring(index); - - } - - int copyNum = 0; - do - { - copyNum++; - } while (fileFolderService.searchSimple(attachmentsFolderRef, name + " (" + copyNum + ")" + ext) != null); - - FileInfo createdFile = fileFolderService.create(attachmentsFolderRef, name + " (" + copyNum + ")" + ext, ContentModel.TYPE_CONTENT); + + + String newFileName = imapService.generateUniqueFilename(attachmentsFolderRef, fileName); + + FileInfo createdFile = fileFolderService.create(attachmentsFolderRef, newFileName, ContentModel.TYPE_CONTENT); nodeService.createAssociation(messageFile, createdFile.getNodeRef(), ImapModel.ASSOC_IMAP_ATTACHMENT); attachmentFile = createdFile.getNodeRef(); diff --git a/source/java/org/alfresco/repo/imap/ImapService.java b/source/java/org/alfresco/repo/imap/ImapService.java index 30ebddc27a..1296e4a85d 100644 --- a/source/java/org/alfresco/repo/imap/ImapService.java +++ b/source/java/org/alfresco/repo/imap/ImapService.java @@ -315,6 +315,8 @@ public interface ImapService */ public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException; + public String generateUniqueFilename(NodeRef destFolderNodeRef, String fileName); + static class FolderStatus { public final int messageCount; diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index e19368dd91..65afa7cdfc 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -2107,6 +2107,29 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol attachmentsExtractor.extractAttachments(messageRef, originalMessage); } + public String generateUniqueFilename(NodeRef destFolderNodeRef, String fileName) + { + if(fileFolderService.searchSimple(destFolderNodeRef, fileName) != null) + { + String name = fileName; + String ext = ""; + if (fileName.lastIndexOf(".") != -1) + { + int index = fileName.lastIndexOf("."); + name = fileName.substring(0, index); + ext = fileName.substring(index); + } + int copyNum = 0; + do + { + copyNum++; + } while (fileFolderService.searchSimple(destFolderNodeRef, name + " (" + copyNum + ")" + ext) != null); + fileName = name + " (" + copyNum + ")" + ext; + } + + return fileName; + } + static class CacheItem { private Date modified; diff --git a/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java index dd66696e19..14c2c936e1 100644 --- a/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java +++ b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java @@ -442,22 +442,44 @@ public class MultilingualContentServiceImpl implements MultilingualContentServic * @param translationNodeRef a translation */ private void unmakeTranslationSimple(NodeRef translationNodeRef) - { - if (nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION)) + { + try { - nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION); - nodeService.addAspect(translationNodeRef, ContentModel.ASPECT_TEMPORARY, null); + this.policyBehaviourFilter.disableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER); + if (nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION)) + { + nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION); + nodeService.addAspect(translationNodeRef, ContentModel.ASPECT_TEMPORARY, null); + } + else + { + if (nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT)) + { + nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT); + } + } + List assocRefs = nodeService.getParentAssocs( + translationNodeRef, + ContentModel.ASSOC_MULTILINGUAL_CHILD, + RegexQNamePattern.MATCH_ALL); + if (assocRefs.size() != 1) + { + throw new AlfrescoRuntimeException( + "Unable to remove ASSOC_MULTILINGUAL_CHILD on : " + translationNodeRef.toString()); + } } - else + finally { - nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT); + this.policyBehaviourFilter.enableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER); } } /** @inheritDoc */ public void unmakeTranslation(NodeRef translationNodeRef) { - if (isPivotTranslation(translationNodeRef)) + if ((nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION) || + nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT)) && + isPivotTranslation(translationNodeRef)) { NodeRef containerNodeRef = getMLContainer(translationNodeRef, true); // We have not cleaned up all other translations diff --git a/source/java/org/alfresco/repo/model/ml/tools/MultilingualContentServiceImplTest.java b/source/java/org/alfresco/repo/model/ml/tools/MultilingualContentServiceImplTest.java index b70b5ef3c4..6c89d8804d 100644 --- a/source/java/org/alfresco/repo/model/ml/tools/MultilingualContentServiceImplTest.java +++ b/source/java/org/alfresco/repo/model/ml/tools/MultilingualContentServiceImplTest.java @@ -164,6 +164,59 @@ public class MultilingualContentServiceImplTest extends AbstractMultilingualTest assertEquals("Pivot node ref not correct", frenchContentNodeRef, multilingualContentService.getPivotTranslation(mlContainerNodeRef)); } + /** + * Testing unmakeTranslation + * @throws Exception + */ + public void testUnmakeTranslation() throws Exception + { + NodeRef frenchContentNodeRef = createContent(); + multilingualContentService.makeTranslation(frenchContentNodeRef, Locale.FRENCH); + NodeRef mlContainerNodeRef = multilingualContentService.getTranslationContainer(frenchContentNodeRef); + + NodeRef englishContentNodeRef = createContent(); + multilingualContentService.addTranslation(englishContentNodeRef, frenchContentNodeRef, Locale.ENGLISH); + + NodeRef germanContentNodeRef = createContent(); + multilingualContentService.addTranslation(germanContentNodeRef, frenchContentNodeRef, Locale.GERMAN); + + multilingualContentService.unmakeTranslation(englishContentNodeRef); + multilingualContentService.unmakeTranslation(germanContentNodeRef); + //multilingualContentService.unmakeTranslation(frenchContentNodeRef); + + //calling unmakeTranslation() dissolves the multiligual document. + //none of the documents composing the multilingual document + assertTrue("The document should not be multilingual!", + !multilingualContentService.isTranslation(englishContentNodeRef) && + !multilingualContentService.isTranslation(germanContentNodeRef)); + } + + /** + * Testing unmakeTranslation() on pivot + * @throws Exception + */ + public void testUnmakeTranslationOnPivot() throws Exception + { + NodeRef frenchContentNodeRef = createContent(); + multilingualContentService.makeTranslation(frenchContentNodeRef, Locale.FRENCH); + NodeRef mlContainerNodeRef = multilingualContentService.getTranslationContainer(frenchContentNodeRef); + + NodeRef englishContentNodeRef = createContent(); + multilingualContentService.addTranslation(englishContentNodeRef, frenchContentNodeRef, Locale.ENGLISH); + + NodeRef germanContentNodeRef = createContent(); + multilingualContentService.addTranslation(germanContentNodeRef, frenchContentNodeRef, Locale.GERMAN); + + multilingualContentService.unmakeTranslation(frenchContentNodeRef); + + //calling unmakeTranslation() dissolves the multiligual document. + //none of the documents composing the multilingual document + assertTrue("The document should not be multilingual!", + !multilingualContentService.isTranslation(frenchContentNodeRef) && + !multilingualContentService.isTranslation(englishContentNodeRef) && + !multilingualContentService.isTranslation(germanContentNodeRef)); + } + @SuppressWarnings("unused") public void testCreateEmptyTranslation() throws Exception { diff --git a/source/java/org/alfresco/repo/node/NodeBulkLoader.java b/source/java/org/alfresco/repo/node/NodeBulkLoader.java index a63a0f1272..449bb3d398 100644 --- a/source/java/org/alfresco/repo/node/NodeBulkLoader.java +++ b/source/java/org/alfresco/repo/node/NodeBulkLoader.java @@ -19,6 +19,7 @@ package org.alfresco.repo.node; import java.util.List; +import java.util.Set; import org.alfresco.service.cmr.repository.NodeRef; @@ -32,6 +33,15 @@ import org.alfresco.service.cmr.repository.NodeRef; */ public interface NodeBulkLoader { + /** + * Gets the current set of cached ancestors of the given list of nodes. + * + * @param nodeIds + * a list of node IDs to visit + * @return the current set of cached ancestors of the given list of nodes, including the nodes themselves. + */ + public Set getCachedAncestors(List nodeIds); + /** * Transaction-scope setting to make the Node loader to guarantee the validity of all * caches: some cache data will be reloaded; some cache data will be considered safe. diff --git a/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java index 3461ab368e..df91241144 100644 --- a/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java +++ b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java @@ -929,14 +929,13 @@ public class RuleServiceCoverageTest extends TestCase .getBean("OutboundSMTP")).getApplicationContext().getBean("mail"); mailService.setTestMode(true); mailService.clearLastTestMessage(); - - NodeRef contentNodeRef = this.nodeService.createNode( + + this.nodeService.createNode( this.nodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName(TEST_NAMESPACE, "children"), ContentModel.TYPE_CONTENT, getContentProperties()).getChildRef(); - addContentToNode(contentNodeRef); // An email should appear in the recipients email // System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); @@ -1664,9 +1663,53 @@ public class RuleServiceCoverageTest extends TestCase ContentModel.ASSOC_CHILDREN, QName.createQName(TEST_NAMESPACE, "children"), ContentModel.TYPE_CONTENT).getChildRef(); + assertTrue(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); + addContentToNode(contentNodeRef); + assertTrue(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // ALF-14744 / MNT-187: Create a content node - this time with 'empty content' in the same transaction + contentNodeRef = this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef contentNodeRef = RuleServiceCoverageTest.this.nodeService.createNode( + RuleServiceCoverageTest.this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT).getChildRef(); + ContentWriter contentWriter = RuleServiceCoverageTest.this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.setEncoding("UTF-8"); + contentWriter.putContent(""); + return contentNodeRef; + } + }); assertFalse(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); addContentToNode(contentNodeRef); assertTrue(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // ALF-14744 / MNT-187: Create a content node - this time with the 'no content' aspect in the same transaction + contentNodeRef = this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef contentNodeRef = RuleServiceCoverageTest.this.nodeService.createNode( + RuleServiceCoverageTest.this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT).getChildRef(); + RuleServiceCoverageTest.this.nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_NO_CONTENT, null); + return contentNodeRef; + } + }); + assertFalse(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); + addContentToNode(contentNodeRef); + assertFalse(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); + nodeService.removeAspect(contentNodeRef, ContentModel.ASPECT_NO_CONTENT); + assertTrue(this.nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE)); // Create a node to be moved NodeRef moveNode = this.nodeService.createNode( diff --git a/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java b/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java index cd530ae9d8..4357f3c59d 100644 --- a/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java +++ b/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java @@ -83,31 +83,32 @@ public class RuleTypeImplTest extends BaseSpringTest public void testMockInboundRuleType() { - NodeRef nodeRef = this.nodeService.createNode( - this.rootNodeRef, - ContentModel.ASSOC_CHILDREN, - ContentModel.ASSOC_CHILDREN, - ContentModel.TYPE_CONTENT).getChildRef(); - NodeRef nodeRef2 = this.nodeService.createNode( - this.rootNodeRef, - ContentModel.ASSOC_CHILDREN, - ContentModel.ASSOC_CHILDREN, - ContentModel.TYPE_CONTAINER).getChildRef(); - List triggers = new ArrayList(2); triggers.add((RuleTrigger)this.applicationContext.getBean("on-content-create-trigger")); - triggers.add((RuleTrigger)this.applicationContext.getBean("on-content-update-trigger")); + triggers.add((RuleTrigger)this.applicationContext.getBean("on-create-node-trigger")); triggers.add((RuleTrigger)this.applicationContext.getBean("on-create-child-association-trigger")); ExtendedRuleType ruleType = new ExtendedRuleType(triggers); assertFalse(ruleType.rulesTriggered); + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT).getChildRef(); + // Update some content in order to trigger the rule type ContentWriter contentWriter = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); contentWriter.putContent("any old content"); assertTrue(ruleType.rulesTriggered); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + // Reset ruleType.rulesTriggered = false; assertFalse(ruleType.rulesTriggered); diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java index 4c8bdf4fc8..6bf75532dc 100644 --- a/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java +++ b/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java @@ -26,11 +26,6 @@ import org.alfresco.repo.policy.Behaviour.NotificationFrequency; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.rule.RuntimeRuleService; import org.alfresco.repo.transaction.TransactionalResourceHelper; -import org.alfresco.service.cmr.dictionary.AspectDefinition; -import org.alfresco.service.cmr.dictionary.ClassDefinition; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.dictionary.PropertyDefinition; -import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.NamespaceService; @@ -112,26 +107,6 @@ public class CreateNodeRuleTrigger extends RuleTriggerAbstractBase new JavaBehaviour(this, "onRemoveAspect", NotificationFrequency.EVERY_EVENT)); } - - /** - * Return true if provided classDef has property that has propertyType type - */ - private boolean hasPropertyOfType(ClassDefinition classDef, QName propertyType) - { - if (classDef != null) - { - for (PropertyDefinition propertyDef : classDef.getProperties().values()) - { - if (propertyDef.getDataType().getName().equals(propertyType) && !propertyDef.isMultiValued()) - { - return true; - } - } - } - - return false; - } - /** * {@inheritDoc} */ @@ -148,27 +123,6 @@ public class CreateNodeRuleTrigger extends RuleTriggerAbstractBase Set newNodeRefSet = TransactionalResourceHelper.getSet(RULE_TRIGGER_NEW_NODES); newNodeRefSet.add(nodeRef); - // If the node's type or aspects have a single-valued content property, don't fire the trigger, as it would be - // handled by on-content-create-trigger, according to its settings for empty content - - // Check node type's properties - QName nodeType = nodeService.getType(nodeRef); - TypeDefinition typeDefinition = dictionaryService.getType(nodeType); - if (hasPropertyOfType(typeDefinition, DataTypeDefinition.CONTENT)) - { - return; - } - - // Check node aspects' properties - for (QName aspectQName : nodeService.getAspects(nodeRef)) - { - AspectDefinition aspectDefinition = dictionaryService.getAspect(aspectQName); - if (hasPropertyOfType(aspectDefinition, DataTypeDefinition.CONTENT)) - { - return; - } - } - if (nodeRef != null && nodeService.exists(nodeRef) == true && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT) == false) diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/OnContentUpdateRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/OnContentUpdateRuleTrigger.java deleted file mode 100644 index 371a393a38..0000000000 --- a/source/java/org/alfresco/repo/rule/ruletrigger/OnContentUpdateRuleTrigger.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2005-2012 Alfresco Software Limited. - * - * This file is part of Alfresco - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ -package org.alfresco.repo.rule.ruletrigger; - -import java.util.List; -import java.util.Set; - -import org.alfresco.model.ContentModel; -import org.alfresco.repo.content.ContentServicePolicies; -import org.alfresco.repo.policy.JavaBehaviour; -import org.alfresco.repo.transaction.TransactionalResourceHelper; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.namespace.QName; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * @author Roy Wetherall - */ -public class OnContentUpdateRuleTrigger extends RuleTriggerAbstractBase - implements ContentServicePolicies.OnContentPropertyUpdatePolicy -{ - /** - * The logger - */ - private static Log logger = LogFactory.getLog(OnContentUpdateRuleTrigger.class); - - /** True trigger on new content, false otherwise */ - private boolean onNewContent = false; - - /** True trigger parent rules, false otherwier */ - private boolean triggerParentRules = true; - - /** - * If set to true the trigger will fire on new content, otherwise it will fire on content update - * - * @param onNewContent indicates whether to fire on content create or update - */ - public void setOnNewContent(boolean onNewContent) - { - this.onNewContent = onNewContent; - } - - /** - * Indicates whether the parent rules should be triggered or the rules on the node itself - * - * @param triggerParentRules true trigger parent rules, false otherwise - */ - public void setTriggerParentRules(boolean triggerParentRules) - { - this.triggerParentRules = triggerParentRules; - } - - /* - * @see org.alfresco.repo.rule.ruletrigger.RuleTrigger#registerRuleTrigger() - */ - public void registerRuleTrigger() - { - // Bind behaviour - this.policyComponent.bindClassBehaviour( - ContentServicePolicies.OnContentPropertyUpdatePolicy.QNAME, - this, - new JavaBehaviour(this, "onContentPropertyUpdate")); - } - - - - /** - * @see org.alfresco.repo.content.ContentServicePolicies.OnContentPropertyUpdatePolicy#onContentPropertyUpdate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.cmr.repository.ContentData, org.alfresco.service.cmr.repository.ContentData) - */ - @Override - public void onContentPropertyUpdate(NodeRef nodeRef, QName propertyQName, ContentData beforeValue, - ContentData afterValue) - { - // Break out early if rules are not enabled - if (!areRulesEnabled()) - { - return; - } - - // Check the new content and make sure that we do indeed want to trigger the rule - if (propertyQName.equals(ContentModel.PROP_PREFERENCE_VALUES)) - { - return; - } - - // Check for new content - boolean newContent = beforeValue == null && afterValue != null; - - // Check the new content and make sure that we do indeed want to trigger the rule - if (newContent) - { - if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT)) - { - return; - } - - // Note: Don't use the ContentService.getReader() because we don't need access to the content - if (!ContentData.hasContent(afterValue)) - { - return; - } - } - // An update, but double check for content created in this transaction - else - { - Set newNodeRefSet = TransactionalResourceHelper.getSet(RULE_TRIGGER_NEW_NODES); - if (newNodeRefSet.contains(nodeRef)) - { - if (logger.isDebugEnabled()) - { - logger.debug("Receiving content property update for node created in transaction: " + nodeRef); - } - return; - } - } - - // Trigger the rules in the appropriate way - if (newContent == this.onNewContent) - { - if (triggerParentRules == true) - { - if (logger.isDebugEnabled() == true) - { - logger.debug("OnContentUpdate rule triggered fired for content; nodeId=" + nodeRef.getId() + "; newContent=" + newContent); - } - - List parentsAssocRefs = this.nodeService.getParentAssocs(nodeRef); - for (ChildAssociationRef parentAssocRef : parentsAssocRefs) - { - triggerRules(parentAssocRef.getParentRef(), nodeRef); - } - } - else - { - triggerRules(nodeRef, nodeRef); - } - } - } -} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/OnPropertyUpdateRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/OnPropertyUpdateRuleTrigger.java index 4ca79f884e..326f0bf13f 100644 --- a/source/java/org/alfresco/repo/rule/ruletrigger/OnPropertyUpdateRuleTrigger.java +++ b/source/java/org/alfresco/repo/rule/ruletrigger/OnPropertyUpdateRuleTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -19,7 +19,7 @@ package org.alfresco.repo.rule.ruletrigger; import java.io.Serializable; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,10 +27,12 @@ import java.util.Set; import org.alfresco.model.ContentModel; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.rule.RuntimeRuleService; import org.alfresco.repo.transaction.TransactionalResourceHelper; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; @@ -51,10 +53,39 @@ public class OnPropertyUpdateRuleTrigger extends RuleTriggerAbstractBase */ private static Log logger = LogFactory.getLog(OnPropertyUpdateRuleTrigger.class); - /** True trigger parent rules, false otherwier */ + /** True trigger on new content, false otherwise */ + private boolean onNewContent = false; + + /** Should we consider zero byte content to be the same as no content? */ + private boolean ignoreEmptyContent = true; + + /** True trigger parent rules, false otherwise */ private boolean triggerParentRules = true; + /** Runtime rule service */ + private RuntimeRuleService runtimeRuleService; + + /** + * If set to true the trigger will fire on new content, otherwise it will fire on content update + * + * @param onNewContent indicates whether to fire on content create or update + */ + public void setOnNewContent(boolean onNewContent) + { + this.onNewContent = onNewContent; + } + /** + * If set to true, then we consider zero byte content to be equivalent to no content. + * + * @param ignoreEmptyContent + */ + public void setIgnoreEmptyContent(boolean ignoreEmptyContent) + { + this.ignoreEmptyContent = ignoreEmptyContent; + } + + /** * Indicates whether the parent rules should be triggered or the rules on the node itself * * @param triggerParentRules true trigger parent rules, false otherwise @@ -64,6 +95,14 @@ public class OnPropertyUpdateRuleTrigger extends RuleTriggerAbstractBase this.triggerParentRules = triggerParentRules; } + /** + * Set the rule service + */ + public void setRuntimeRuleService(RuntimeRuleService runtimeRuleService) + { + this.runtimeRuleService = runtimeRuleService; + } + /* * @see org.alfresco.repo.rule.ruletrigger.RuleTrigger#registerRuleTrigger() */ @@ -76,48 +115,95 @@ public class OnPropertyUpdateRuleTrigger extends RuleTriggerAbstractBase new JavaBehaviour(this, "onUpdateProperties")); } - private boolean havePropertiesBeenModified(NodeRef nodeRef, Map before, Map after) + private boolean havePropertiesBeenModified(NodeRef nodeRef, Map before, Map after, boolean newNode, boolean newContentOnly) { - List remainder = new ArrayList(after.keySet()); - List modifiedProperties = new ArrayList(); - for (QName name : before.keySet()) + if (newContentOnly && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT)) { - if (after.containsKey(name) == true) - { - Serializable beforeValue = before.get(name); - Serializable afterValue = after.get(name); - if (EqualsHelper.nullSafeEquals(beforeValue, afterValue) != true) - { - // The property has been changed - modifiedProperties.add(name); - } - - // Remove the property from the remainder list - remainder.remove(name); - } - } - - // Add any properties now remaining whose values have been added for the first time - if (remainder.size() != 0) - { - modifiedProperties.addAll(remainder); - } - - // Filter out the protected and content type properties from the list of modified properties - for (QName propertyName : new ArrayList(modifiedProperties)) - { - PropertyDefinition propertyDefinition = this.dictionaryService.getProperty(propertyName); - if (propertyDefinition != null) - { - if (propertyDefinition.isProtected() == true || propertyDefinition.getDataType().getName().equals(DataTypeDefinition.CONTENT) == true) - { - // Remove the protected property from the list - modifiedProperties.remove(propertyName); - } - } + return false; } - - return (modifiedProperties.isEmpty() == false); + + Set keys = new HashSet(after.keySet()); + keys.addAll(before.keySet()); + + // Compare all properties, ignoring protected properties and giving special treatment to content properties + boolean nonNullContentProperties = false; + boolean newContentProperties = false; + boolean nonNewModifiedContentProperties = false; + boolean modifiedNonContentProperties = false; + for (QName name : keys) + { + // Skip rule firing on this content property for performance reasons + if (name.equals(ContentModel.PROP_PREFERENCE_VALUES)) + { + continue; + } + Serializable beforeValue = before.get(name); + Serializable afterValue = after.get(name); + PropertyDefinition propertyDefinition = this.dictionaryService.getProperty(name); + if (propertyDefinition == null) + { + if (!EqualsHelper.nullSafeEquals(beforeValue, afterValue)) + { + modifiedNonContentProperties = true; + } + } + // Ignore protected properties + else if (!propertyDefinition.isProtected()) + { + if (propertyDefinition.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + // Remember whether the property was populated, regardless of the ignore setting + if (afterValue != null) + { + nonNullContentProperties = true; + } + if (this.ignoreEmptyContent) + { + ContentData beforeContent = (ContentData) before.get(name); + ContentData afterContent = (ContentData) after.get(name); + if (!ContentData.hasContent(beforeContent) || beforeContent.getSize() == 0) + { + beforeValue = null; + } + if (!ContentData.hasContent(afterContent) || afterContent.getSize() == 0) + { + afterValue = null; + } + } + if (newNode) + { + if (afterValue != null) + { + newContentProperties = true; + } + } + else if (!EqualsHelper.nullSafeEquals(beforeValue, afterValue)) + { + if (beforeValue == null) + { + newContentProperties = true; + } + else + { + nonNewModifiedContentProperties = true; + } + } + } + else if (!EqualsHelper.nullSafeEquals(beforeValue, afterValue)) + { + modifiedNonContentProperties = true; + } + } + } + + if (newContentOnly) + { + return (newNode && !nonNullContentProperties ) || newContentProperties; + } + else + { + return modifiedNonContentProperties || nonNewModifiedContentProperties; + } } /** @@ -133,47 +219,87 @@ public class OnPropertyUpdateRuleTrigger extends RuleTriggerAbstractBase // Do not fire if the node has been created in this transaction Set newNodeRefSet = TransactionalResourceHelper.getSet(RULE_TRIGGER_NEW_NODES); boolean wasCreatedInTxn = newNodeRefSet.contains(nodeRef); - if (logger.isDebugEnabled() && wasCreatedInTxn) + if (wasCreatedInTxn) { - logger.debug("Receiving property update for node created in transaction: " + nodeRef); - } - - // Only try and trigger the rules if a non protected property has been modified - if (!wasCreatedInTxn && - before.size() != 0 && // ALF-4846: Do not trigger for newly created nodes - havePropertiesBeenModified(nodeRef, before, after) == true) - { - // Keep track of name changes explicitly. This prevents the later association change from - // triggering 'inbound' rules - if (!EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_NAME), after.get(ContentModel.PROP_NAME))) + if (logger.isDebugEnabled()) { - // Name has changed - Set renamedNodeRefSet = TransactionalResourceHelper.getSet(RULE_TRIGGER_RENAMED_NODES); - renamedNodeRefSet.add(nodeRef); + logger.debug("Receiving property update for node created in transaction: " + nodeRef); } - if (triggerParentRules == true) - { - List parentsAssocRefs = this.nodeService.getParentAssocs(nodeRef); - for (ChildAssociationRef parentAssocRef : parentsAssocRefs) + + // A rule has already been fired for this new node, but now that we are aware of content properties, we may + // want to withhold it until later + if (this.onNewContent) + { + if (havePropertiesBeenModified(nodeRef, before, after, true, true)) { - triggerRules(parentAssocRef.getParentRef(), nodeRef); + // Possibly undo a previous cancellation in this transaction if (logger.isDebugEnabled() == true) { - logger.debug( - "OnPropertyUpdate rule triggered (parent); " + - "nodeRef=" + parentAssocRef.getParentRef()); + logger.debug("New node " + nodeRef.toString() + + " confirmed to have no content properties or to have new content so firing inbound rules."); } + triggerRules(nodeRef); + } + else + { + // Removes any rules that have already been triggered for that node + if (logger.isDebugEnabled() == true) + { + logger.debug("Removing the pending rules for the new node " + nodeRef.toString() + + " since there are no non-empty content properties."); + } + runtimeRuleService.removeRulePendingExecution(nodeRef); } } - else + } + else + { + // Only try and trigger the rules if a non protected property has been modified + if (!wasCreatedInTxn && + before.size() != 0 && // ALF-4846: Do not trigger for newly created nodes + havePropertiesBeenModified(nodeRef, before, after, false, this.onNewContent)) { - triggerRules(nodeRef, nodeRef); + // Keep track of name changes explicitly. This prevents the later association change from + // triggering 'inbound' rules + if (!EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_NAME), after.get(ContentModel.PROP_NAME))) + { + // Name has changed + Set renamedNodeRefSet = TransactionalResourceHelper.getSet(RULE_TRIGGER_RENAMED_NODES); + renamedNodeRefSet.add(nodeRef); + } + + triggerRules(nodeRef); + } + } + } + + /** + * @param nodeRef + */ + private void triggerRules(NodeRef nodeRef) + { + if (triggerParentRules == true) + { + List parentsAssocRefs = this.nodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef parentAssocRef : parentsAssocRefs) + { + triggerRules(parentAssocRef.getParentRef(), nodeRef); if (logger.isDebugEnabled() == true) { - logger.debug("OnPropertyUpdate rule triggered; nodeRef=" + nodeRef); + logger.debug( + "OnPropertyUpdate rule triggered (parent); " + + "nodeRef=" + parentAssocRef.getParentRef()); } } } + else + { + triggerRules(nodeRef, nodeRef); + if (logger.isDebugEnabled() == true) + { + logger.debug("OnPropertyUpdate rule triggered; nodeRef=" + nodeRef); + } + } } } diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java index 67f1f77346..63219a3463 100644 --- a/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java +++ b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java @@ -44,7 +44,7 @@ public class RuleTriggerTest extends BaseSpringTest private static final String ON_DELETE_CHILD_ASSOCIATION_TRIGGER = "on-delete-child-association-trigger"; private static final String ON_CREATE_ASSOCIATION_TRIGGER = "on-create-association-trigger"; private static final String ON_DELETE_ASSOCIATION_TRIGGER = "on-delete-association-trigger"; - private static final String ON_CONTENT_UPDATE_TRIGGER = "on-content-update-trigger"; + private static final String ON_PROPERTY_UPDATE_TRIGGER = "on-property-update-trigger"; private static final String ON_CONTENT_CREATE_TRIGGER = "on-content-create-trigger"; private NodeService nodeService; @@ -228,12 +228,22 @@ public class RuleTriggerTest extends BaseSpringTest public void testOnContentCreateTrigger() { + TestRuleType nodeCreate = createTestRuleType(ON_CREATE_NODE_TRIGGER); + assertFalse(nodeCreate.rulesTriggered); + NodeRef nodeRef = this.nodeService.createNode( this.rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_CONTENT).getChildRef(); + assertTrue(nodeCreate.rulesTriggered); + + // Terminate the transaction + setComplete(); + endTransaction(); + startNewTransaction(); + TestRuleType contentCreate = createTestRuleType(ON_CONTENT_CREATE_TRIGGER); assertFalse(contentCreate.rulesTriggered); @@ -260,14 +270,19 @@ public class RuleTriggerTest extends BaseSpringTest public void testOnContentUpdateTrigger() { - NodeRef nodeRef = this.nodeService.createNode( + TestRuleType nodeCreate = createTestRuleType(ON_CREATE_NODE_TRIGGER); + assertFalse(nodeCreate.rulesTriggered); + + NodeRef nodeRef = this.nodeService.createNode( this.rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_CONTENT).getChildRef(); + + assertTrue(nodeCreate.rulesTriggered); TestRuleType contentCreate = createTestRuleType(ON_CONTENT_CREATE_TRIGGER); - TestRuleType contentUpdate = createTestRuleType(ON_CONTENT_UPDATE_TRIGGER); + TestRuleType contentUpdate = createTestRuleType(ON_PROPERTY_UPDATE_TRIGGER); assertFalse(contentCreate.rulesTriggered); assertFalse(contentUpdate.rulesTriggered); @@ -290,7 +305,7 @@ public class RuleTriggerTest extends BaseSpringTest contentWriter2.putContent("more content some content"); // Check to see if the rule type has been triggered - assertFalse(contentCreate.rulesTriggered); + assertTrue(contentCreate.rulesTriggered); assertFalse( "Content update must not fire if the content was created in the same txn.", contentUpdate.rulesTriggered); @@ -298,6 +313,7 @@ public class RuleTriggerTest extends BaseSpringTest // Terminate the transaction setComplete(); endTransaction(); + contentCreate.rulesTriggered = false; // Try and trigger the type (again) ContentWriter contentWriter3 = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); diff --git a/source/java/org/alfresco/repo/search/impl/solr/SolrBackupClient.java b/source/java/org/alfresco/repo/search/impl/solr/SolrBackupClient.java index 46cba5df10..649951d866 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/SolrBackupClient.java +++ b/source/java/org/alfresco/repo/search/impl/solr/SolrBackupClient.java @@ -89,7 +89,7 @@ public class SolrBackupClient implements InitializingBean String lockToken = getLock(60000); if (lockToken == null) { - + return; } // Use a flag to keep track of the running job final AtomicBoolean running = new AtomicBoolean(true); diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java index 16b9161f80..c4f2d78a10 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java @@ -765,7 +765,7 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm * Key for a cache object is built from all the known Authorities (which can change dynamically so they must all be * used) the NodeRef ID and the permission reference itself. This gives a unique key for each permission test. */ - static Serializable generateKey(Set auths, NodeRef nodeRef, PermissionReference perm, CacheType type) + Serializable generateKey(Set auths, NodeRef nodeRef, PermissionReference perm, CacheType type) { LinkedHashSet key = new LinkedHashSet(); key.add(perm.toString()); @@ -779,6 +779,9 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm key.addAll(auths); } key.add(nodeRef); + // Ensure some concept of node version or transaction is included in the key so we can track without cache replication + NodeRef.Status nodeStatus = nodeService.getNodeStatus(nodeRef); + key.add(nodeStatus == null ? "null" : nodeStatus.getChangeTxnId()); key.add(type); return key; } @@ -1177,22 +1180,6 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm */ @Override public Set getReaders(Long aclId) - { - Set aclReaders = readersCache.get(aclId); - if (aclReaders == null) - { - aclReaders = buildReaders(aclId); - readersCache.put(aclId, aclReaders); - } - return aclReaders; - } - - /** - * Builds the set of authorities who can read the given ACL. No caching is done here. - * - * @return an unmodifiable set of authorities - */ - protected Set buildReaders(Long aclId) { AccessControlList acl = aclDaoComponent.getAccessControlList(aclId); if (acl == null) @@ -1200,6 +1187,12 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm return Collections.emptySet(); } + Set aclReaders = readersCache.get((Serializable)acl.getProperties()); + if (aclReaders != null) + { + return aclReaders; + } + HashSet assigned = new HashSet(); HashSet readers = new HashSet(); @@ -1217,23 +1210,30 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm } } - return Collections.unmodifiableSet(readers); + aclReaders = Collections.unmodifiableSet(readers); + readersCache.put((Serializable)acl.getProperties(), aclReaders); + return aclReaders; } /** * @param aclId * @return set of authorities with read permission on the ACL */ - protected Set buildReadersDenied(Long aclId) + private Set getReadersDenied(Long aclId) { - HashSet assigned = new HashSet(); - HashSet denied = new HashSet(); AccessControlList acl = aclDaoComponent.getAccessControlList(aclId); if (acl == null) + { + return Collections.emptySet(); + } + Set denied = readersDeniedCache.get(aclId); + if (denied != null) { return denied; } + denied = new HashSet(); + Set assigned = new HashSet(); for (AccessControlEntry ace : acl.getEntries()) { @@ -1248,6 +1248,8 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm denied.add(authority); } } + + readersDeniedCache.put((Serializable)acl.getProperties(), denied); return denied; } @@ -1261,12 +1263,7 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm if(anyDenyDenies) { - Set aclReadersDenied = readersDeniedCache.get(aclId); - if(aclReadersDenied == null) - { - aclReadersDenied = buildReadersDenied(aclId); - readersDeniedCache.put(aclId, aclReadersDenied); - } + Set aclReadersDenied = getReadersDenied(aclId); for(String auth : aclReadersDenied) { diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java index 3177774ca4..22a1d614ae 100644 --- a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -894,6 +894,19 @@ public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean // BatchProcessWorker that runs work as another user. private abstract class RunAsWorker extends BatchProcessWorkerAdaptor { + @Override + public void beforeProcess() throws Throwable + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(userName); + } + + @Override + public void afterProcess() throws Throwable + { + AuthenticationUtil.popAuthentication(); + } + final String userName; final String name; diff --git a/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java b/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java index 78702b488c..83aefb68f0 100644 --- a/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java +++ b/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java @@ -186,6 +186,9 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent { if(enabled) { + // We don't want the caches to lie and we may not be part of the cluster + aclDAO.setCheckAclConsistency(); + /* * This is an N+1 query that should, in theory, make use of cached ACL readers data. */ @@ -486,7 +489,6 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent private List preCacheNodes(NodeMetaDataParameters nodeMetaDataParameters) { - nodeDAO.setCheckNodeConsistency(); int maxResults = nodeMetaDataParameters.getMaxResults(); boolean isLimitSet = (maxResults != 0 && maxResults != Integer.MAX_VALUE); @@ -515,8 +517,13 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent nodeIds.add(nodeId); } } - // pre-cache nodes - nodeDAO.cacheNodesById(nodeIds); + + // Pre-evaluate ancestors so we can bulk load them + List ancestors = new ArrayList(nodeDAO.getCachedAncestors(nodeIds)); + // Ensure that we get fresh node references + nodeDAO.setCheckNodeConsistency(); + // bulk load nodes and their ancestors + nodeDAO.cacheNodesById(ancestors); return nodeIds; } @@ -554,10 +561,7 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent { return; } - - // Ensure that we get fresh node references - nodeDAO.setCheckNodeConsistency(); - + NodeMetaDataQueryRowHandler rowHandler = new NodeMetaDataQueryRowHandler(callback); boolean includeType = (resultFilter == null ? true : resultFilter.getIncludeType()); boolean includeProperties = (resultFilter == null ? true : resultFilter.getIncludeProperties());