Showing posts with label Alfresco. Show all posts
Showing posts with label Alfresco. Show all posts

Thursday, August 31, 2017

Alfresco 4 - Importing ACP with UUID Binding with existing "Import" functionality

One of my projects uses Alfresco 4.1, and I recently needed to import data from another environment and keep the folder and content node references intact while doing so.  These below articles were extremely helpful, but neither of them talked about overriding the existing "Import" functionality when inside Alfresco Explorer.

https://community.alfresco.com/thread/178902-acp-import-with-uuid-binding-specified
http://ecmarchitect.com/archives/2008/10/27/859


To preserve UUID binding during existing ACP import:
  1. Followed the steps from the first article above, creating a custom class  that extended the ImporterActionExecuter.  cherryshoe-alfresco-amp\src\main\java\com\cherryshoe\action\executer\CustomAcpImporterActionExecuter.java

    package com.cherryshoe.action.executer;
    
    import java.io.File;
    
    import org.alfresco.model.ContentModel;
    import org.alfresco.repo.action.executer.ImporterActionExecuter;
    import org.alfresco.repo.content.MimetypeMap;
    import org.alfresco.repo.importer.ACPImportPackageHandler;
    import org.alfresco.service.cmr.action.Action;
    import org.alfresco.service.cmr.repository.ContentReader;
    import org.alfresco.service.cmr.repository.ContentService;
    import org.alfresco.service.cmr.repository.NodeRef;
    import org.alfresco.service.cmr.repository.NodeService;
    import org.alfresco.service.cmr.view.ImporterBinding;
    import org.alfresco.service.cmr.view.ImporterService;
    import org.alfresco.service.cmr.view.Location;
    import org.alfresco.service.namespace.QName;
    import org.alfresco.util.TempFileProvider;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class CustomAcpImporterActionExecuter extends ImporterActionExecuter {
     private static final Log log = LogFactory.getLog(CustomAcpImporterActionExecuter.class);
     public static final String NAME = "custom-import";
     public static final String PARAM_ENCODING = "encoding";
     public static final String PARAM_DESTINATION_FOLDER = "destination";
     private static final String TEMP_FILE_PREFIX = "alf";
     private static final String TEMP_FILE_SUFFIX_ACP = ".acp";
     private ImporterBinding.UUID_BINDING uuidBinding;
     private ImporterService importerService;
     private NodeService nodeService;
     private ContentService contentService;
    
     public void setImporterService(ImporterService importerService) {
      this.importerService = importerService;
     }
    
     public void setNodeService(NodeService nodeService) {
      this.nodeService = nodeService;
     }
    
     public void setContentService(ContentService contentService) {
      this.contentService = contentService;
     }
    
     public CustomAcpImporterActionExecuter() {
      super();
      log.info(
        "*****************************CustomAcpImporterActionExecuter constructor*************************************");
     }
    
     /**
      * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef,
      *      org.alfresco.repo.ref.NodeRef)
      */
     @Override
     public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) {
      log.info(
        "************************actionedUponNodeRef:**********************************" + actionedUponNodeRef);
      if (this.nodeService.exists(actionedUponNodeRef) == true) {
       // The node being passed in should be an Alfresco content package
       ContentReader reader = this.contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT);
       if (reader != null) {
        NodeRef importDest = (NodeRef) ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER);
        log.info("*******************************importDest:" + importDest + " actionedUponNodeRef:"
          + actionedUponNodeRef);
        if (MimetypeMap.MIMETYPE_ACP.equals(reader.getMimetype())) {
         // perform an import of an Alfresco ACP file (special format
         // ZIP structure)
         File zipFile = null;
         try {
          // unfortunately a ZIP file can not be read directly
          // from an input stream so we have to create
          // a temporary file first
          zipFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX_ACP);
          reader.getContent(zipFile);
    
          ACPImportPackageHandler importHandler = new ACPImportPackageHandler(zipFile,
            (String) ruleAction.getParameterValue(PARAM_ENCODING));
    
          log.debug(
            "************************************Starting import*****************************************");
          this.importerService.importView(importHandler, new Location(importDest), getBinding(), null);
          log.debug(
            "************************************Stopping import*****************************************");
         } finally {
          // now the import is done, delete the temporary file
          if (zipFile != null) {
           zipFile.delete();
          }
         }
        }
       }
      }
     }
    
     private ImporterBinding getBinding() {
      ImporterBinding binding = new ImporterBinding() {
       public boolean allowReferenceWithinTransaction() {
        return false;
       }
    
       public QName[] getExcludedClasses() {
        return null;
       }
    
       public UUID_BINDING getUUIDBinding() {
        return uuidBinding;
       }
    
       public String getValue(String key) {
        return null;
       }
      };
    
      // Uses UPDATE_EXISTING as usage of REPLACE_EXISTING removes child nodes
      // if these are not sent over in the export.
      uuidBinding = ImporterBinding.UUID_BINDING.UPDATE_EXISTING;
      return binding;
     }
    }
    
  2. Copied the existing alfresco\WEB-INF\classes\alfresco\action-services-context.xml file to my maven alfresco sdk folder cherryshoe-alfresco-amp\src\main\amp\config\alfresco\extension folder and named it action-services-cherryshoe-override-context.xml.  I removed everything in the file except the spring bean with id "import", and updated the class attribute with the package location of my custom class com.cherryshoe.action.executer.CustomAcpImporterActionExecuter.  The extension folder gets loaded after the out-of-the-box alfresco folders, so this should be overriding the WEB-INF\classes\alfresco\action-services-context.xml with just this one bean to use our custom one (If you know the classloading order of resources, Spring does allow overriding beans of the same id).
    cherryshoe-alfresco-amp\src\main\amp\config\alfresco\extension\action-services-cherryshoe-override-context.xml

    <?xml version='1.0' encoding='UTF-8'?>
    <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
    
    <beans>
    
     <!-- The extension folder gets loaded after the OOB alfresco folders, so this should be overriding the 
     WEB-INF\classes\alfresco\action-services-context.xml with just this one bean to use our custom one -->
        <bean id="import" class="com.cherryshoe.action.executer.CustomAcpImporterActionExecuter" parent="action-executer">
            <property name="importerService">
                <ref bean="ImporterService"/>
            </property>
            <property name="nodeService">
                <ref bean="NodeService"></ref>
            </property>
            <property name="contentService">
                <ref bean="ContentService" />
            </property>
            <property name="fileFolderService">
                <ref bean="FileFolderService"/>
            </property>
        </bean>
        
    </beans>
    
  3. Updated cherryshoe-alfresco-amp\src\test\resources\alfresco\extension\custom-log4j.properties with:
    # custom import action executer
    log4j.logger.com.cherryshoe.action.executor=info

  4. After navigating to <servername>:<port>/alfresco -> Folder to import ACP -> Import ACP -> then searching no a known node reference UUID using the Node Browser, I can see that the node was binded with the original UUID!
    1. NOTE: Since ACP files can be very large, using this default behavior could hang/fail, since the ACP first has to be uploaded, then imported.  Instead you can write a javascript script, implement a rule on a folder to call that script, and import an ACP that already exists on the repository (this article was helpful and worked great for me).

Sunday, March 19, 2017

Bash script to call rest endpoint for each id in text file

I needed to write an admin utility script to delete documents from a content management system.  First, the list of document ids were retrieved from the application database; Second, the list of ids were used as input to a bash script to make REST calls to delete the documents.

Environment:
MySql 5.7.10
RHEL 7

STEPS:
  1. Retrieve list of ids from the database and save to uuids.txt, one id on each line. The ids were alfresco cmis unique ids (with unique nodeRef id, ;,  and version number), they look like "workspace://SpacesStore/aa3e765f-c628-44c2-bc66-17d622ca2210;1.0".  I needed documents created after a certain date, and only the unique id portion after the "SpacesStore/" and prior to the ";1.0" of the unique id.  Below is the MySQL select statement:

    select substring_index(substring_index(alf_node_id, 'SpacesStore/', -1),
                           ';1.0', 1), created_dt from cherryshoe_documents where created_dt >= '2017-03-18 00:00:00' order by created_dt desc;

  2. The bash script is below, it incorporates a curl call to a REST DELETE endpoint with basic authentication.

    delete.sh
    
    #!/usr/bin/bash
    # Calls a repository rest endpoint to delete node(s).
    # Takes in an input text file of one id per line.
    
    username=admin
    password=admin
    protocol=http
    #hostname can contain port
    hostname="localhost:8080"
    
    echo username=$username, password=$password, protocol=$protocol, hostname=$hostname
    
    if (( "$#" != 1 ))
    then
        echo "Usage Info: Enter in filename"
    exit 1
    fi
    
    filename="$1"
    
    echo "Starting Node Delete..."
    while read -r line
    do
        uuid="$line"
        endpoint=$protocol://$hostname/alfresco/s/api/node/workspace/SpacesStore/$uuid
        # contains a \r (CR) at the end (0d). Remove it with
        endpoint=${endpoint%$'\r'}
        echo $endpoint
    
        echo "#######START $uuid"
        curl -u $username:$password -i -X DELETE $endpoint
        echo "#######END $uuid"
    
    done < "$filename"
    
    echo "DONE"
    

  3. Sample uuids.txt file below:
    32231a66-649a-4cee-9a16-2c45b639fd94
    08b53382-e120-4100-a093-32b2a57b9bad
    6759904f-48bf-4e02-93e0-6ee9f9885df7
    9931f0e8-1b48-4c3c-a885-94ccf80866ea
    aedef20e-6831-4fdc-ad21-872af333d985

Wednesday, November 30, 2016

CMIS - adding new aspect removed existing aspects problem

I had a problem where there were existing custom aspects/attributes already applied to an existing object with a createObject api.

A post process needed to apply additional aspects (for migration purposes) to set the legacy object id information.  It ended up blowing away the existing aspect and properties!

Environment:
OSX El Capitan 10.11.6
java version "1.8.0_101"
Alfresco 5.1.0/ CMIS 1.1
Apache chemistry chemistry-opencmis-client-impl 0.14.0

Problem:
Existing post processing code was attempting to add the new migrated aspect to the properties map.  You can see it setting the “PropertyIds.SECONDARY_OBJECT_TYPE_IDS” property with the new type.  This is going to replace any “PropertyIds.SECONDARY_OBJECT_TYPE_IDS” that may have previously existed!

List<String> secondaryTypeProps = new ArrayList<String>();
secondaryTypeProps.add(CmisConstants.CST_ASPECT_MIGRATED_EVENT);

Map<String, Object> properties = new HashMap<String, Object>();
properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, secondaryTypeProps);
… add new custom properties

Solution:
Since you are completely replacing the "PropertyIds.SECONDARY_OBJECT_TYPE_IDS" property, you need to retrieve the existing value(s) first, and then add any new aspect(s) to it.

// Retrieve existing aspects
List<Object> aspects = obj.getProperty(PropertyIds.SECONDARY_OBJECT_TYPE_IDS).getValues();
aspects.add(CmisConstants.CST_ASPECT_MIGRATED_EVENT);

// Following adds the new aspect for migrate event aspect
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, aspects);
… add new custom properties

Saturday, June 13, 2015

Custom Alfresco MailActionExecuter to support CC, BCC, CC Lists, and BCC Lists

I needed to create a custom alfresco MailActionExecuter action so it could do several additional things that are not provided from the vanilla OOB alfresco version.  I did this case study with version 4.2.e, but double-checked in the enterpise 5.0.1 code base, that it does not have this support yet either.  Here's what I needed to do:
  • Support CC, BCC, CC Lists, and BCC Lists
  • Fix the problem of if there are X number of multiple recipients, then X number of emails are sent.  Why?  We only need one email sent for the X multiple recipients.  For example, if you had three recipients in your email list, 3 separate emails was sent.  We only need one email sent that will have a To/CC/BCC list that includes the three recipients.
NOTE: Grab the source code for the 4.2.e, 5.0.1, and the custom code described in the steps below.

Steps:
  1. Grap the source code for MailActionExecuter located in the alfresco-repository source jar, grab from either:
    • alfresco public maven or alfresco private maven (depending on community or enterprise version).  
      • I opted for this method and downloaded both alfresco-repository-4.2.e-sources.jar and alfresco-repository-5.0.1-sources.jar.
    • sdk source code zip download for your particular alfresco version
  2. Create a custom version of MailActionExecuter and put it in a custom package (i.e. com.cherryshoe.action.executer).  Changes in the MailActionExecuter are indicated by comments that start with "NEW CHERRYSHOE" that tell you what was changed and why. 
    package com.cherryshoe.action.executer;
    
    /*
     * These are NEW CHERRYSHOE updates for org.alfresco.repo.action.executer.MailActionExecuter.
     * The Alfresco MailActionExecuter has the following issues that will be fixed for the NEW CHERRYSHOE version:
     * 1.  If there are X multiple recipients, then X emails are sent.  We only need ONE email sent for the
     * X multiple recipients.
     * 2.  PARAM_CC, PARAM_CC_MANY, PARAM_BCC, and PARAM_BCC_MANY are not currently supported.  
     */
    
    import java.io.Serializable;
    import java.io.UnsupportedEncodingException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Locale;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.atomic.AtomicInteger;
    
    import javax.mail.MessagingException;
    import javax.mail.internet.InternetAddress;
    import javax.mail.internet.MimeMessage;
    
    import org.alfresco.error.AlfrescoRuntimeException;
    import org.alfresco.model.ContentModel;
    import org.alfresco.repo.action.ParameterDefinitionImpl;
    import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
    import org.alfresco.repo.action.executer.TestModeable;
    import org.alfresco.repo.admin.SysAdminParams;
    import org.alfresco.repo.security.authentication.AuthenticationUtil;
    import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
    import org.alfresco.repo.template.DateCompareMethod;
    import org.alfresco.repo.template.HasAspectMethod;
    import org.alfresco.repo.template.I18NMessageMethod;
    import org.alfresco.repo.template.TemplateNode;
    import org.alfresco.repo.tenant.TenantService;
    import org.alfresco.repo.tenant.TenantUtil;
    import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
    import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
    import org.alfresco.repo.transaction.RetryingTransactionHelper;
    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;
    import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
    import org.alfresco.service.cmr.preference.PreferenceService;
    import org.alfresco.service.cmr.repository.NodeRef;
    import org.alfresco.service.cmr.repository.NodeService;
    import org.alfresco.service.cmr.repository.TemplateImageResolver;
    import org.alfresco.service.cmr.repository.TemplateService;
    import org.alfresco.service.cmr.security.AuthenticationService;
    import org.alfresco.service.cmr.security.AuthorityService;
    import org.alfresco.service.cmr.security.AuthorityType;
    import org.alfresco.service.cmr.security.PersonService;
    import org.alfresco.util.Pair;
    import org.alfresco.util.UrlUtil;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.commons.validator.routines.EmailValidator;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.extensions.surf.util.I18NUtil;
    import org.springframework.mail.MailException;
    import org.springframework.mail.MailPreparationException;
    import org.springframework.mail.javamail.JavaMailSender;
    import org.springframework.mail.javamail.MimeMessageHelper;
    import org.springframework.mail.javamail.MimeMessagePreparator;
    import org.springframework.util.StringUtils;
    
    /**
     * Mail action executor implementation.
     * <p/>
     * <em>Note on executing this action as System:</em> it is allowed to execute {@link #NAME mail} actions as system.
     * However there is a limitation if you do so. Because the system user is not a normal user and specifically because
     * there is no corresponding {@link ContentModel#TYPE_PERSON cm:person} node for system, it is not possible to use
     * any reference to that person in the associated email template. Various email templates use a '{@link TemplateNode person}' object
     * in the FTL model to access things like first name, last name etc.
     * In the case of mail actions sent while running as system, none of these will be available.
     * 
     * @author Roy Wetherall
     */
    public class MailActionExecuter extends ActionExecuterAbstractBase
                                    implements InitializingBean, TestModeable
    {
        private static Log logger = LogFactory.getLog(MailActionExecuter.class);
        
        /**
         * Action executor constants
         */
        public static final String NAME = "mail";
        public static final String PARAM_LOCALE = "locale";
        public static final String PARAM_TO = "to";
        public static final String PARAM_CC = "cc"; // NEW CHERRYSHOE
        public static final String PARAM_BCC = "bcc"; // NEW CHERRYSHOE
        public static final String PARAM_TO_MANY = "to_many";
        public static final String PARAM_CC_MANY = "cc_many"; // NEW CHERRYSHOE
        public static final String PARAM_BCC_MANY = "bcc_many"; // NEW CHERRYSHOE
        public static final String PARAM_SUBJECT = "subject";
        public static final String PARAM_SUBJECT_PARAMS = "subjectParams";
        public static final String PARAM_TEXT = "text";
        public static final String PARAM_HTML = "html";
        public static final String PARAM_FROM = "from";
        public static final String PARAM_FROM_PERSONAL_NAME = "fromPersonalName";
        public static final String PARAM_TEMPLATE = "template";
        public static final String PARAM_TEMPLATE_MODEL = "template_model";
        public static final String PARAM_IGNORE_SEND_FAILURE = "ignore_send_failure";
        public static final String PARAM_SEND_AFTER_COMMIT = "send_after_commit";
       
        /**
         * From address
         */
        private static final String FROM_ADDRESS = "alfresco@alfresco.org";
        
        /**
         * The java mail sender
         */
        private JavaMailSender mailService;
        
        /**
         * The Template service
         */
        private TemplateService templateService;
        
        /**
         * The Person service
         */
        private PersonService personService;
        
        /**
         * The Authentication service
         */
        private AuthenticationService authService;
        
        /**
         * The Node Service
         */
        private NodeService nodeService;
        
        /**
         * The Authority Service
         */
        private AuthorityService authorityService;
        
        /**
         * The Service registry
         */
        private ServiceRegistry serviceRegistry;
        
        /**
         * System Administration parameters, including URL information
         */
        private SysAdminParams sysAdminParams;
        
        /**
         * The Preference Service
         */
        private PreferenceService preferenceService;
        
        /**
         * The Tenant Service
         */
        private TenantService tenantService;
        
        /**
         * Mail header encoding scheme
         */
        private String headerEncoding = null;
        
        /**
         * Default from address
         */
        private String fromDefaultAddress = null;
        
        /**
         * Is the from field enabled? Or must we always use the default address.
         */
        private boolean fromEnabled = true;
        
        
        private boolean sendTestMessage = false;
        private String testMessageTo = null;
        private String testMessageSubject = "Test message";
        private String testMessageText = "This is a test message.";
    
        private boolean validateAddresses = true;
        
        /**
         * Test mode prevents email messages from being sent.
         * It is used when unit testing when we don't actually want to send out email messages.
         * 
         * MER 20/11/2009 This is a quick and dirty fix. It should be replaced by being 
         * "mocked out" or some other better way of running the unit tests. 
         */
        private boolean testMode = false;
        private MimeMessage lastTestMessage;
    
        private TemplateImageResolver imageResolver;
    
        /**
         * @param javaMailSender    the java mail sender
         */
        public void setMailService(JavaMailSender javaMailSender) 
        {
            this.mailService = javaMailSender;
        }
        
        /**
         * @param templateService   the TemplateService
         */
        public void setTemplateService(TemplateService templateService)
        {
            this.templateService = templateService;
        }
        
        /**
         * @param personService     the PersonService
         */
        public void setPersonService(PersonService personService)
        {
            this.personService = personService;
        }
        
        public void setPreferenceService(PreferenceService preferenceService)
        {
            this.preferenceService = preferenceService;
        }
        
        /**
         * @param authService       the AuthenticationService
         */
        public void setAuthenticationService(AuthenticationService authService)
        {
            this.authService = authService;
        }
        
        /**
         * @param serviceRegistry   the ServiceRegistry
         */
        public void setServiceRegistry(ServiceRegistry serviceRegistry)
        {
            this.serviceRegistry = serviceRegistry;
        }
        
        /**
         * @param authorityService  the AuthorityService
         */
        public void setAuthorityService(AuthorityService authorityService)
        {
            this.authorityService = authorityService;
        }
        
        /**
         * @param nodeService       the NodeService to set.
         */
        public void setNodeService(NodeService nodeService)
        {
            this.nodeService = nodeService;
        }
        
        /**
         * @param tenantService       the TenantService to set.
         */
        public void setTenantService(TenantService tenantService)
        {
            this.tenantService = tenantService;
        }
        
        /**
         * @param headerEncoding     The mail header encoding to set.
         */
        public void setHeaderEncoding(String headerEncoding)
        {
            this.headerEncoding = headerEncoding;
        }
        
        /**
         * @param fromAddress   The default mail address.
         */
        public void setFromAddress(String fromAddress)
        {
            this.fromDefaultAddress = fromAddress;
        }
    
        public void setSysAdminParams(SysAdminParams sysAdminParams)
        {
            this.sysAdminParams = sysAdminParams;
        }
        
        public void setImageResolver(TemplateImageResolver imageResolver)
        {
            this.imageResolver = imageResolver;
        }
        
        public void setTestMessageTo(String testMessageTo)
        {
            this.testMessageTo = testMessageTo;
        }
        
        public String getTestMessageTo()
        {
            return testMessageTo;
        }
        
        public void setTestMessageSubject(String testMessageSubject)
        {
            this.testMessageSubject = testMessageSubject;
        }
        
        public void setTestMessageText(String testMessageText)
        {
            this.testMessageText = testMessageText;
        }
    
        public void setSendTestMessage(boolean sendTestMessage)
        {
            this.sendTestMessage = sendTestMessage;
        }
        
        /**
         * This stores an email address which, if it is set, overrides ALL email recipients sent from
         * this class. It is intended for dev/test usage only !!
         */
        private String testModeRecipient;
    
        /**
         * Send a test message
         * 
         * @return true, message sent 
         * @throws AlfrescoRuntimeException 
         */
        public boolean sendTestMessage() 
        {
            if(testMessageTo == null || testMessageTo.length() == 0)
            {
                throw new AlfrescoRuntimeException("email.outbound.err.test.no.to");
            }
            if(testMessageSubject == null || testMessageSubject.length() == 0)
            {
                throw new AlfrescoRuntimeException("email.outbound.err.test.no.subject");
            }
            if(testMessageText == null || testMessageText.length() == 0)
            {
                throw new AlfrescoRuntimeException("email.outbound.err.test.no.text");
            }
            Map<String, Serializable> params = new HashMap<String, Serializable>();
            params.put(PARAM_TO, testMessageTo);
            params.put(PARAM_SUBJECT, testMessageSubject);
            params.put(PARAM_TEXT, testMessageText);
            
            Action ruleAction = serviceRegistry.getActionService().createAction(NAME, params);
            
            MimeMessageHelper message = prepareEmail(ruleAction, null,
                    new Pair<String, Locale>(testMessageTo, getLocaleForUser(testMessageTo)), getFrom(ruleAction));
            try
            {
                mailService.send(message.getMimeMessage());
                onSend();
            }
            catch (MailException me)
            {
                onFail();
                StringBuffer txt = new StringBuffer();
                
                txt.append(me.getClass().getName() + ", " + me.getMessage());
                
                Throwable cause = me.getCause();
                while (cause != null)
                {
                    txt.append(", ");
                    txt.append(cause.getClass().getName() + ", " + cause.getMessage());
                    cause = cause.getCause();
                }
                
                Object[] args = {testMessageTo, txt.toString()};
                throw new AlfrescoRuntimeException("email.outbound.err.send.failed", args, me);
            }
            
            return true;
        }
        
        public void setTestModeRecipient(String testModeRecipient)
        {
            this.testModeRecipient = testModeRecipient;
        }
    
        public void setValidateAddresses(boolean validateAddresses)
        {
            this.validateAddresses = validateAddresses;
        }
    
        
        @Override
        public void init()
        {
            if(logger.isDebugEnabled())
            {
                logger.debug("Init called, testMessageTo=" + testMessageTo);
            }
            
            numberSuccessfulSends.set(0);
            numberFailedSends.set(0);
            
            super.init();
            if (sendTestMessage && testMessageTo != null)
            {
                AuthenticationUtil.runAs(new RunAsWork<Object>()
                {
                    public Object doWork() throws Exception
                    {
                        Map<String, Serializable> params = new HashMap<String, Serializable>();
                        params.put(PARAM_TO, testMessageTo);
                        params.put(PARAM_SUBJECT, testMessageSubject);
                        params.put(PARAM_TEXT, testMessageText);
    
                        Action ruleAction = serviceRegistry.getActionService().createAction(NAME, params);
                        executeImpl(ruleAction, null);
                        return null;
                    }
                }, AuthenticationUtil.getSystemUserName());
            }
        }
    
        /**
         * Initialise bean
         */
        public void afterPropertiesSet() throws Exception
        {
            if (fromDefaultAddress == null || fromDefaultAddress.length() == 0)
            {
                fromDefaultAddress = FROM_ADDRESS;
            }
            
        }
        
        /**
         * Send an email message
         * 
         * @throws AlfrescoRuntimeExeption
         */
        @Override
        protected void executeImpl(
                final Action ruleAction,
                final NodeRef actionedUponNodeRef) 
        {
           
            //Prepare our messages before the commit, in-case of deletes
            MimeMessageHelper[] messages = null; 
            if (validNodeRefIfPresent(actionedUponNodeRef))
            {
            messages = prepareEmails(ruleAction, actionedUponNodeRef);
            }
            final MimeMessageHelper[] finalMessages = messages;
            
            //Send out messages
            if(finalMessages!=null){
                if (sendAfterCommit(ruleAction))
                {
                    AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter()
                    {
                        @Override
                        public void afterCommit()
                        {
                            RetryingTransactionHelper helper = serviceRegistry.getTransactionService().getRetryingTransactionHelper();
                            helper.doInTransaction(new RetryingTransactionCallback<Void>()
                            {
                                @Override
                                public Void execute() throws Throwable
                                {
                                    for (MimeMessageHelper message : finalMessages) {
                                        sendEmail(ruleAction, message);
                                    }
                                    
                                    return null;
                                }
                            }, false, true);
                        }
                    });
                }
                else 
                {
                        for (MimeMessageHelper message : finalMessages) {
                            sendEmail(ruleAction, message);
                            break; // NEW CHERRYSHOE The code before it got here should be preventing multiple messages to be created, but just in case, only call sendEmail once
                        }
                }
            }
    
        }
        
        
        private boolean validNodeRefIfPresent(NodeRef actionedUponNodeRef)
        {
            if (actionedUponNodeRef == null)
            {
                // We must expect that null might be passed in (ALF-11625)
                // since the mail action might not relate to a specific nodeRef.
                return true;
            }
            else
            {
                // Only try and send the email if the actioned upon node reference still exists
                // (i.e. if one has been specified it must be valid)
                return nodeService.exists(actionedUponNodeRef);
            }
        }
        
        private boolean sendAfterCommit(Action action)
        {
            Boolean sendAfterCommit = (Boolean) action.getParameterValue(PARAM_SEND_AFTER_COMMIT);
            return sendAfterCommit == null ? false : sendAfterCommit.booleanValue();
        }
        
        /*
         * NEW CHERRYSHOE: Alfresco OOB code here is setting the number of messages to send determined by how many recipients there are total. 
         * That seems totally wrong, as you should just have ONE email sent period no matter how many recipients, with the recipients properly set in the ONE email message.
         * The getRecipients has been modified to return after at least one recipient is determined.  Also break after the loop is finished even though collections is size of 1.
         */
        private MimeMessageHelper[] prepareEmails(final Action ruleAction, final NodeRef actionedUponNodeRef)
        {
            List<Pair<String, Locale>> recipients = getRecipients(ruleAction);
            
            Pair<InternetAddress, Locale> from = getFrom(ruleAction);
            
            if (logger.isDebugEnabled())
            {
                logger.debug("From: address=" + from.getFirst() + " ,locale=" + from.getSecond());
            }
            
            MimeMessageHelper[] messages = new MimeMessageHelper[recipients.size()];
            int recipientIndex = 0;
            for (Pair<String, Locale> recipient : recipients)
            {
                if (logger.isDebugEnabled())
                {
                    logger.debug("Recipient: address=" + recipient.getFirst() + " ,locale=" + recipient.getSecond());
                }
                
                messages[recipientIndex] = prepareEmail(ruleAction, actionedUponNodeRef, recipient, from);
                recipientIndex++;
                
                // NEW CHERRYSHOE only call prepareEmail once
                break;
            }
            
            return messages;
        }
        
        public MimeMessageHelper prepareEmail(final Action ruleAction , final NodeRef actionedUponNodeRef, final Pair<String, Locale> recipient, final Pair<InternetAddress, Locale> sender)
        {
            // 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")
                public void prepare(MimeMessage mimeMessage) throws MessagingException
                {
                    if (logger.isDebugEnabled())
                    {
                       logger.debug(ruleAction.getParameterValues());
                    }
                    
                    messageRef[0] = new MimeMessageHelper(mimeMessage);
                    
                    // set header encoding if one has been supplied
                    if (headerEncoding != null && headerEncoding.length() != 0)
                    {
                        mimeMessage.setHeader("Content-Transfer-Encoding", headerEncoding);
                    }
                    
                    // set recipient
                    String to = (String)ruleAction.getParameterValue(PARAM_TO);
                    String cc = (String)ruleAction.getParameterValue(PARAM_CC); // NEW CHERRYSHOE
                    String bcc = (String)ruleAction.getParameterValue(PARAM_BCC); // NEW CHERRYSHOE
                    // NEW CHERRYSHOE use apache string utils to check for is blank
                    if (!org.apache.commons.lang.StringUtils.isBlank(to) || !org.apache.commons.lang.StringUtils.isBlank(cc) || !org.apache.commons.lang.StringUtils.isBlank(bcc))
                    {
                        if (!org.apache.commons.lang.StringUtils.isBlank(to)) {
                            messageRef[0].setTo(to);
                        }
                        if (!org.apache.commons.lang.StringUtils.isBlank(cc)) {
                            messageRef[0].setCc(cc);
                        }
                        if (!org.apache.commons.lang.StringUtils.isBlank(bcc)) {
                            messageRef[0].setBcc(bcc);
                        }
                        
                        // Note: there is no validation on the username to check that it actually is an email address.
                        // TODO Fix this.
                    }
                    else
                    {
                        // NEW CHERRYSHOE added CC and BCC support.  For TO, CC, and BCC, respectively, 
                        // created new method setUpManyRecipients to set up multiple recipients
                        // *****TO*****
                        List<String> toRecipients = null;
                        toRecipients = setUpManyRecipients(ruleAction, PARAM_TO_MANY);
                        if(toRecipients.size() > 0)
                        {
                            messageRef[0].setTo(toRecipients.toArray(new String[toRecipients.size()]));
                        }
    
                        // *****CC*****                  
                        List<String> ccRecipients = null;
                        ccRecipients = setUpManyRecipients(ruleAction, PARAM_CC_MANY);
                        if(ccRecipients.size() > 0)
                        {
                            messageRef[0].setCc(ccRecipients.toArray(new String[ccRecipients.size()]));     
                        }
                        
                        // *****BCC*****
                        List<String> bccRecipients = null;
                        bccRecipients = setUpManyRecipients(ruleAction, PARAM_BCC_MANY);
                        if(bccRecipients.size() > 0)
                        {
                            messageRef[0].setBcc(bccRecipients.toArray(new String[bccRecipients.size()]));
                        }
    
                    }
                    
                    // from person - not to be performed for the "admin" or "system" users
                    NodeRef fromPerson = null;
                    
                    final String currentUserName = authService.getCurrentUserName();
                    
                    final List<String> usersNotToBeUsedInFromField = Arrays.asList(new String[] {AuthenticationUtil.getAdminUserName(),
                                                                                                 AuthenticationUtil.getSystemUserName(),
                                                                                                 AuthenticationUtil.getGuestUserName()});
                    if ( !usersNotToBeUsedInFromField.contains(currentUserName))
                    {
                        fromPerson = personService.getPerson(currentUserName);
                    }
                    
                    if(isFromEnabled())
                    {   
                        // Use the FROM parameter in preference to calculating values.
                        String from = (String)ruleAction.getParameterValue(PARAM_FROM);
                        if (from != null && from.length() > 0)
                        {
                            if(logger.isDebugEnabled())
                            {
                                logger.debug("from specified as a parameter, from:" + from);
                            }
                            
                            // Check whether or not to use a personal name for the email (will be RFC 2047 encoded)
                            String fromPersonalName = (String)ruleAction.getParameterValue(PARAM_FROM_PERSONAL_NAME);
                            if(fromPersonalName != null && fromPersonalName.length() > 0) 
                            {
                                try
                                {
                                    messageRef[0].setFrom(from, fromPersonalName);
                                }
                                catch (UnsupportedEncodingException error)
                                {
                                    // Uses the JVM's default encoding, can never be unsupported. Just in case, revert to simple email
                                    messageRef[0].setFrom(from);
                                }
                            }
                            else
                            {
                                messageRef[0].setFrom(from);
                            }
                        }
                        else
                        {
                            // set the from address from the current user
                            String fromActualUser = null;
                            if (fromPerson != null)
                            {
                                fromActualUser = (String) nodeService.getProperty(fromPerson, ContentModel.PROP_EMAIL);
                            }
                        
                            if (fromActualUser != null && fromActualUser.length() != 0)
                            {
                                if(logger.isDebugEnabled())
                                {
                                    logger.debug("looked up email address for :" + fromPerson + " email from " + fromActualUser);
                                }
                                messageRef[0].setFrom(fromActualUser);
                            }
                            else
                            {
                                // from system or user does not have email address
                                messageRef[0].setFrom(fromDefaultAddress);
                            }
                        }
    
                    }
                    else
                    {
                        if(logger.isDebugEnabled())
                        {
                            logger.debug("from not enabled - sending from default address:" + fromDefaultAddress);
                        }
                        // from is not enabled.
                        messageRef[0].setFrom(fromDefaultAddress);
                    }
                    
    
    
                    
                    // set subject line
                    messageRef[0].setSubject((String)ruleAction.getParameterValue(PARAM_SUBJECT));
                    
                    if ((testModeRecipient != null) && (testModeRecipient.length() > 0) && (! testModeRecipient.equals("${dev.email.recipient.address}")))
                    {
                        // If we have an override for the email recipient, we'll send the email to that address instead.
                        // We'll prefix the subject with the original recipient, but leave the email message unchanged in every other way.
                        messageRef[0].setTo(testModeRecipient);
                        
                        String emailRecipient = (String)ruleAction.getParameterValue(PARAM_TO);
                        if (emailRecipient == null)
                        {
                           Object obj = ruleAction.getParameterValue(PARAM_TO_MANY);
                           if (obj != null)
                           {
                               emailRecipient = obj.toString();
                           }
                        }
                        
                        String recipientPrefixedSubject = "(" + emailRecipient + ") " + (String)ruleAction.getParameterValue(PARAM_SUBJECT);
                        
                        messageRef[0].setSubject(recipientPrefixedSubject);
                    }
                    
                    
                    // See if an email template has been specified
                    String text = null;
                    
                    // templateRef: either a nodeRef or classpath (see ClasspathRepoTemplateLoader)
                    Serializable ref = ruleAction.getParameterValue(PARAM_TEMPLATE);
                    String templateRef = (ref instanceof NodeRef ? ((NodeRef)ref).toString() : (String)ref);
                    if (templateRef != null)
                    {
                        Map<String, Object> suppliedModel = null;
                        if(ruleAction.getParameterValue(PARAM_TEMPLATE_MODEL) != null)
                        {
                            Object m = ruleAction.getParameterValue(PARAM_TEMPLATE_MODEL);
                            if(m instanceof Map)
                            {
                                suppliedModel = (Map<String, Object>)m;
                            }
                            else
                            {
                                logger.warn("Skipping unsupported email template model parameters of type "
                                        + m.getClass().getName() + " : " + m.toString());
                            }
                        }
                        
                        // build the email template model
                        Map<String, Object> model = createEmailTemplateModel(actionedUponNodeRef, suppliedModel, fromPerson);
    
                        // Determine the locale to use to send the email.
                        Locale locale = recipient.getSecond();
                        if (locale == null)
                        {
                            locale = (Locale)ruleAction.getParameterValue(PARAM_LOCALE);
                        }
                        if (locale == null)
                        {
                            locale = sender.getSecond();
                        }
                        
                        // set subject line
                        String subject = (String)ruleAction.getParameterValue(PARAM_SUBJECT);
                        Object[] subjectParams = (Object[])ruleAction.getParameterValue(PARAM_SUBJECT_PARAMS);
                        String localizedSubject = getLocalizedSubject(subject, subjectParams, locale);
                        if (locale == null)
                        {
                            // process the template against the model
                            text = templateService.processTemplate("freemarker", templateRef, model);
                        }
                        else
                        {
                            // process the template against the model
                            text = templateService.processTemplate("freemarker", templateRef, model, locale);
                        }
                        if ((testModeRecipient != null) && (testModeRecipient.length() > 0) && (! testModeRecipient.equals("${dev.email.recipient.address}")))
                        {
                            // If we have an override for the email recipient, we'll send the email to that address instead.
                            // We'll prefix the subject with the original recipient, but leave the email message unchanged in every other way.
                            messageRef[0].setTo(testModeRecipient);
                            
                            String emailRecipient = recipient.getFirst();
                            
                            String recipientPrefixedSubject = "(" + emailRecipient + ") " + localizedSubject;
                            
                            messageRef[0].setSubject(recipientPrefixedSubject);
                        }
                        else 
                        {
                            messageRef[0].setTo(recipient.getFirst());
                            messageRef[0].setSubject(localizedSubject);
                        }
                    }
                    
                    // set the text body of the message
                    
                    boolean isHTML = false;
                    if (text == null)
                    {
                        text = (String)ruleAction.getParameterValue(PARAM_TEXT);
                    }
                    
                    if (text != null)
                    {
                        if (isHTML(text))
                        {
                            isHTML = true;
                        }
                    }
                    else
                    {
                        text = (String)ruleAction.getParameterValue(PARAM_HTML);
                        if (text != null)
                        {
                            // assume HTML
                            isHTML = true;
                        }
                    }
                    
                    if (text != null)
                    {
                        messageRef[0].setText(text, isHTML);
                    }
                    
                }
            };
            MimeMessage mimeMessage = mailService.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];
        }
        
        /*
         * NEW CHERRYSHOE: Figures out how many recipients there are for a particular paramMany type.
         * Since we only need maximum one recipient, then we will break out when we have that.
         */
        private List<Pair<String, Locale>> getManyRecipients(final Action ruleAction, String paramMany) 
        {
            List<Pair<String, Locale>> recipients = new LinkedList<Pair<String,Locale>>();
            
            // see if multiple recipients have been supplied - as a list of authorities
            Serializable authoritiesValue = ruleAction.getParameterValue(paramMany);
            List<String> authorities = null;
            if (authoritiesValue != null)
            {
                if (authoritiesValue instanceof String)
                {
                    authorities = new ArrayList<String>(1);
                    authorities.add((String)authoritiesValue);
                }
                else
                {
                    authorities = (List<String>)authoritiesValue;
                }
            }
            
            if (authorities != null && authorities.size() != 0)
            {
                for (String authority : authorities)
                {
                    AuthorityType authType = AuthorityType.getAuthorityType(authority);
                    if (authType.equals(AuthorityType.USER))
                    {
                        // Formerly, this code checked personExists(auth) but we now support emailing addresses who are not yet Alfresco users.
                        // Check the user name to be a valid email and we don't need to log an error in this case
                        // ALF-19231
                        // Validate the email, allowing for local email addresses
                        if (authority != null && authority.length() != 0)
                        {
                            if (personExists(authority))
                            {
                                EmailValidator emailValidator = EmailValidator.getInstance(true);
                                if (validateAddresses && emailValidator.isValid(authority))
                                {
                                    Locale locale = getLocaleForUser(authority);
                                    recipients.add(new Pair<String, Locale>(authority, locale));
                                    break; // only need one maximum recipient
                                }
                                else
                                {
                                    String address = getPersonEmail(authority);
                                    if (address != null && address.length() != 0 && validateAddress(address))
                                    {
                                        Locale locale = getLocaleForUser(authority);
                                        recipients.add(new Pair<String, Locale>(address, locale));
                                        break; // only need one maximum recipient
                                    }
                                }
                            }
                            else
                            {
                                recipients.add(new Pair<String, Locale>(authority, null));
                                break; // only need one maximum recipient
                            }
                        }
                    }
                    else if (authType.equals(AuthorityType.GROUP) || authType.equals(AuthorityType.EVERYONE))
                    {
                        // NEW CHERRYSHOE
                        // Notify all members of the group
                        Set<String> users;
                        if (authType.equals(AuthorityType.GROUP))
                        {        
                            users = authorityService.getContainedAuthorities(AuthorityType.USER, authority, false);
                        }
                        else
                        {
                            users = authorityService.getAllAuthorities(AuthorityType.USER);
                        }
                        
                        for (String userAuth : users)
                        {
                            if (personExists(userAuth))
                            {
                                // Check the user name to be a valid email and we don't need to log an error in this case
                                // ALF-19231
                                // Validate the email, allowing for local email addresses
                                EmailValidator emailValidator = EmailValidator.getInstance(true);
                                if (validateAddresses && emailValidator.isValid(userAuth))
                                {
                                    if (userAuth != null && userAuth.length() != 0)
                                    {
                                        Locale locale = getLocaleForUser(userAuth);
                                        recipients.add(new Pair<String, Locale>(userAuth, locale));
                                        break; // only need one maximum recipient
                                    }
                                }
                                else
                                {
                                    String address = getPersonEmail(userAuth);
                                    if (address != null && address.length() != 0 && validateAddress(address))
                                    {
                                        Locale locale = getLocaleForUser(userAuth);
                                        recipients.add(new Pair<String, Locale>(address, locale));
                                        break; // only need one maximum recipient
                                    }
                                }
                            }
                            else
                            {
                                recipients.add(new Pair<String, Locale>(authority, null));
                                break; // only need one maximum recipient
                            }
                        }
                    }
                }
                if(recipients.size() <= 0)
                {
                    // All recipients were invalid
                    throw new MailPreparationException(
                            "All recipients for the mail action were invalid"
                    );
                }
            }
            else
            {
                // No recipients have been specified, change to info only don't throw an error
                logger.info(
                        "No recipient has been specified for the mail action in getManyRecipients for paramMany[" + paramMany + "]"
                );
            }
            
            return recipients;
        }
        
        /*
         * NEW CHERRYSHOE Sets up the PARAM_X_MANY recipients based on the "param" parameter.
         */
        private List<String> setUpManyRecipients(final Action ruleAction, String paramMany)
        throws MailPreparationException {
            
            // see if multiple to recipients have been supplied - as a list of authorities
            Serializable authoritiesValue = ruleAction.getParameterValue(paramMany);
            List<String> authorities = null;
            if (authoritiesValue != null)
            {
                if (authoritiesValue instanceof String)
                {
                    authorities = new ArrayList<String>(1);
                    authorities.add((String)authoritiesValue);
                }
                else
                {
                    authorities = (List<String>)authoritiesValue;
                }
            }
            
            List<String> recipients = new ArrayList<String>(authorities.size());
            if (authorities != null && authorities.size() != 0)
            {
                if (logger.isTraceEnabled()) { logger.trace(authorities.size() + " recipient(s) for mail"); }
                
                for (String authority : authorities)
                {
                    final AuthorityType authType = AuthorityType.getAuthorityType(authority);
                    
                    if (logger.isTraceEnabled()) { logger.trace(" authority type: " + authType); }
                    
                    if (authType.equals(AuthorityType.USER))
                    {
                        if (personService.personExists(authority) == true)
                        {
                            NodeRef person = personService.getPerson(authority);
                            String address = (String)nodeService.getProperty(person, ContentModel.PROP_EMAIL);
                            if (address != null && address.length() != 0 && validateAddress(address))
                            {
                                if (logger.isTraceEnabled()) { logger.trace("Recipient (person) exists in Alfresco with known email."); }
                                recipients.add(address);
                            }
                            else
                            {
                                if (logger.isTraceEnabled()) { logger.trace("Recipient (person) exists in Alfresco without known email."); }
                                // If the username looks like an email address, we'll use that.
                                if (validateAddress(authority)) { recipients.add(authority); }
                            }
                        }
                        else
                        {
                            if (logger.isTraceEnabled()) { logger.trace("Recipient does not exist in Alfresco."); }
                            if (validateAddress(authority)) { recipients.add(authority); }
                        }
                    }
                    else if (authType.equals(AuthorityType.GROUP) || authType.equals(AuthorityType.EVERYONE))
                    {
                        if (logger.isTraceEnabled()) { logger.trace("Recipient is a group..."); }
                        // Notify all members of the group
                        Set<String> users;
                        if (authType.equals(AuthorityType.GROUP))
                        {        
                            users = authorityService.getContainedAuthorities(AuthorityType.USER, authority, false);
                        }
                        else
                        {
                            users = authorityService.getAllAuthorities(AuthorityType.USER);
                        }
                        
                        for (String userAuth : users)
                        {
                            if (personService.personExists(userAuth) == true)
                            {
                                NodeRef person = personService.getPerson(userAuth);
                                String address = (String)nodeService.getProperty(person, ContentModel.PROP_EMAIL);
                                if (address != null && address.length() != 0)
                                {
                                    recipients.add(address);
                                    if (logger.isTraceEnabled()) { logger.trace("   Group member email is known."); }
                                }
                                else
                                {
                                    if (logger.isTraceEnabled()) { logger.trace("   Group member email not known."); }
                                    if (validateAddress(authority)) { recipients.add(userAuth); }
                                }
                            }
                            else
                            {
                                if (logger.isTraceEnabled()) { logger.trace("   Group member person not found"); }
                                if (validateAddress(authority)) { recipients.add(userAuth); }
                            }
                        }
                    }
                }
                
                if (logger.isTraceEnabled()) { logger.trace(recipients.size() + " valid recipient(s)."); }
              
            }
            else
            {
                // No recipients have been specified, change to info only
                logger.info(
                        "No recipient has been specified for the mail action in setUpManyRecipients for paramMany[" + paramMany + "]"
                );
            }
            return recipients;
        }
        
        private void sendEmail(final Action ruleAction, MimeMessageHelper preparedMessage)
        {
    
            try
            {
                // Send the message unless we are in "testMode"
                if (!testMode)
                {
                    mailService.send(preparedMessage.getMimeMessage());
                    onSend();
                }
                else
                {
                    lastTestMessage = preparedMessage.getMimeMessage();
                }
            }
            catch (MailException e)
            {
                onFail();
                String to = (String)ruleAction.getParameterValue(PARAM_TO);
                if (to == null)
                {
                   Object obj = ruleAction.getParameterValue(PARAM_TO_MANY);
                   if (obj != null)
                   {
                      to = obj.toString();
                   }
                }
                
                // always log the failure
                logger.error("Failed to send email to " + to, e);
                
                // optionally ignore the throwing of the exception
                Boolean ignoreError = (Boolean)ruleAction.getParameterValue(PARAM_IGNORE_SEND_FAILURE);
                if (ignoreError == null || ignoreError.booleanValue() == false)
                {
                    Object[] args = {to, e.toString()};
                    throw new AlfrescoRuntimeException("email.outbound.err.send.failed", args, e);
                }   
            }
        }
        
        /**
         * Attempt to localize the subject, using the subject parameter as the message key.
         * 
         * @param subject Message key for subject lookup
         * @param params Parameters for the message
         * @param locale Locale to use
         * @return The localized message, or subject if the message format could not be found
         */
        private String getLocalizedSubject(String subject, Object[] params, Locale locale)
        {
            String localizedSubject = null;
            if (locale == null)
            {
                localizedSubject = I18NUtil.getMessage(subject, params);
            }
            else 
            {
                localizedSubject = I18NUtil.getMessage(subject, locale, params);
            }
            
            if (localizedSubject == null)
            {
                return subject;
            }
            else
            {
                return localizedSubject;
            }
            
        }
        
        /**
         * 
         * @param ruleAction
         * @return
         */
        private Pair<InternetAddress, Locale> getFrom(Action ruleAction)
        {
            try 
            {
                InternetAddress address;
                Locale locale = null;
                // from person
                String fromPersonName = null;
                
                if (! authService.isCurrentUserTheSystemUser())
                {
                    String currentUserName = authService.getCurrentUserName();
                    if (currentUserName != null && personExists(currentUserName))
                    {
                        fromPersonName = currentUserName;
                        locale = getLocaleForUser(fromPersonName);
                    }
                }
                        
                if(isFromEnabled())
                {   
                    // Use the FROM parameter in preference to calculating values.
                    String from = (String)ruleAction.getParameterValue(PARAM_FROM);
                    if (from != null && from.length() > 0)
                    {
                        if(logger.isDebugEnabled())
                        {
                            logger.debug("from specified as a parameter, from:" + from);
                        }
                    
                        // Check whether or not to use a personal name for the email (will be RFC 2047 encoded)
                        String fromPersonalName = (String)ruleAction.getParameterValue(PARAM_FROM_PERSONAL_NAME);
                        if(fromPersonalName != null && fromPersonalName.length() > 0) 
                        {
                            try
                            {
                                address = new InternetAddress(from, fromPersonalName);
                            }
                            catch (UnsupportedEncodingException error)
                            {
                                address = new InternetAddress(from);
                            }
                        }
                        else
                        {
                            address = new InternetAddress(from);
                        }
                        if (locale == null)
                        {
                            if (personExists(from))
                            {
                                locale = getLocaleForUser(from);
                            }
                        }
                    }
                    else
                    {
                        // FROM enabled but not not specified                
                        String fromActualUser = fromPersonName;
                        if (fromPersonName != null)
                        {
                             NodeRef fromPerson = getPerson(fromPersonName);
                             fromActualUser = (String) nodeService.getProperty(fromPerson, ContentModel.PROP_EMAIL);
                        }
                  
                        if (fromActualUser != null && fromActualUser.length() != 0)
                        {
                            if(logger.isDebugEnabled())
                            {
                                logger.debug("looked up email address for :" + fromPersonName + " email from " + fromActualUser);
                            }
                            address = new InternetAddress(fromActualUser);
                        }
                        else
                        {
                            // from system or user does not have email address
                            address = new InternetAddress(fromDefaultAddress);
                        }
                    }
                }
                else
                {
                    if(logger.isDebugEnabled())
                    {
                        logger.debug("from not enabled - sending from default address:" + fromDefaultAddress);
                    }
                    // from is not enabled.
                    address = new InternetAddress(fromDefaultAddress);
                }
                
                return new Pair<InternetAddress, Locale>(address, locale);
            }
            catch (MessagingException ex)
            {
                throw new AlfrescoRuntimeException("Failed to resolve sender mail address");
            }
        }
        
        /*
         * NEW CHERRYSHOE: Since we only need maximum one recipient, then we will break out when we have that.
         */
        @SuppressWarnings("unchecked")
        private List<Pair<String, Locale>> getRecipients(Action ruleAction) 
        {
            
            List<Pair<String, Locale>> recipients = new LinkedList<Pair<String,Locale>>();
            
            // set single recipient(s)
            String to = (String)ruleAction.getParameterValue(PARAM_TO);
            String cc = (String)ruleAction.getParameterValue(PARAM_CC); // NEW CHERRYSHOE
            String bcc = (String)ruleAction.getParameterValue(PARAM_BCC); // NEW CHERRYSHOE
            // NEW CHERRYSHOE use apache string utils to check for is blank
            if (!org.apache.commons.lang.StringUtils.isBlank(to) || !org.apache.commons.lang.StringUtils.isBlank(cc) || !org.apache.commons.lang.StringUtils.isBlank(bcc))
            {
                if (!org.apache.commons.lang.StringUtils.isBlank(to))
                {
                    Locale locale = null;
                    if (personExists(to))
                    {
                        locale = getLocaleForUser(to);
                    }
                    recipients.add(new Pair<String, Locale>(to, locale));
                    return recipients; // NEW CHERRYSHOE only need one maximum recipient
                } 
                if (!org.apache.commons.lang.StringUtils.isBlank(cc)) 
                {
                    Locale locale = null;
                    if (personExists(cc))
                    {
                        locale = getLocaleForUser(cc);
                    }
                    recipients.add(new Pair<String, Locale>(cc, locale));
                    return recipients; // NEW CHERRYSHOE only need one maximum recipient
                } 
                if (!org.apache.commons.lang.StringUtils.isBlank(bcc)) {
                    Locale locale = null;
                    if (personExists(bcc))
                    {
                        locale = getLocaleForUser(bcc);
                    }
                    recipients.add(new Pair<String, Locale>(bcc, locale));
                    return recipients; // NEW CHERRYSHOE only need one maximum recipient
                }
            }
            else
            {
                // NEW CHERRYSHOE figure out the manyRecipients, broke out alfresco code into
                // new getManyRecipients method
                recipients.addAll(getManyRecipients(ruleAction, PARAM_TO_MANY));
                if (recipients.size() > 0) return recipients; // NEW CHERRYSHOE only need one maximum recipient
                recipients.addAll(getManyRecipients(ruleAction, PARAM_CC_MANY));
                if (recipients.size() > 0) return recipients; // NEW CHERRYSHOE only need one maximum recipient
                recipients.addAll(getManyRecipients(ruleAction, PARAM_BCC_MANY));
            }
            return recipients;
        }
        
        public boolean personExists(final String user)
        {
            boolean exists = false;
            String domain = tenantService.getPrimaryDomain(user); // get primary tenant 
            if (domain != null) 
            { 
                exists = TenantUtil.runAsTenant(new TenantRunAsWork<Boolean>()
                {
                    public Boolean doWork() throws Exception
                    {
                        return personService.personExists(user);
                    }
                }, domain);
            }
            else
            {
                exists = personService.personExists(user);
            }
            return exists;
        }
        
        public NodeRef getPerson(final String user)
        {
            NodeRef person = null;
            String domain = tenantService.getPrimaryDomain(user); // get primary tenant 
            if (domain != null) 
            { 
                person = TenantUtil.runAsTenant(new TenantRunAsWork<NodeRef>()
                {
                    public NodeRef doWork() throws Exception
                    {
                        return personService.getPerson(user);
                    }
                }, domain);
            }
            else
            {
                person = personService.getPerson(user);
            }
            return person;
        }
        
        public String getPersonEmail(final String user)
        {
            final NodeRef person = getPerson(user);
            String email = null;
            String domain = tenantService.getPrimaryDomain(user); // get primary tenant 
            if (domain != null) 
            { 
                email = TenantUtil.runAsTenant(new TenantRunAsWork<String>()
                {
                    public String doWork() throws Exception
                    {
                        return (String) nodeService.getProperty(person, ContentModel.PROP_EMAIL);
                    }
                }, domain);
            }
            else
            {
                email = (String) nodeService.getProperty(person, ContentModel.PROP_EMAIL);
            }
            return email;
        }
        
        /**
         * Gets the specified user's preferred locale, if available.
         * 
         * @param user the username of the user whose locale is sought.
         * @return the preferred locale for that user, if available, else <tt>null</tt>. The result would be <tt>null</tt>
         *         e.g. if the user does not exist in the system.
         */
        private Locale getLocaleForUser(final String user)
        {
            Locale locale = null;
            String localeString = null;
            
            // get primary tenant for the specified user.
            //
            // This can have one of (at least) 3 values currently:
            // 1. In single-tenant (community/enterprise) this will be the empty string.
            // 2. In the cloud, for a username such as this: joe.soap@acme.com:
            //    2A. If the acme.com tenant exists in the system, the primary domain is "acme.com"
            //    2B. Id the acme.xom tenant does not exist in the system, the primary domain is null.
            String domain = tenantService.getPrimaryDomain(user);
            
            if (domain != null) 
            { 
                // If the domain is not null, then the user exists in the system and we may get a preferred locale.
                localeString = TenantUtil.runAsSystemTenant(new TenantRunAsWork<String>()
                {
                    public String doWork() throws Exception
                    {
                        return (String) preferenceService.getPreference(user, "locale");
                    }
                }, domain);
            }
            else
            {
                // If the domain is null, then the beahviour here varies depending on whether it's a single tenant or multi-tenant cloud.
                if (personExists(user))
                {
                    localeString = (String) preferenceService.getPreference(user, "locale");
                }
                // else leave it as null - there's no tenant, no user for that username, so we can't get a preferred locale.
            }
            
            if (localeString != null)
            {
                locale = StringUtils.parseLocaleString(localeString);
            }
    
            return locale;
        }
        
        /**
         * Return true if address has valid format
         * @param address
         * @return
         */
        private boolean validateAddress(String address)
        {
            boolean result = false;
            
            // Validate the email, allowing for local email addresses
            EmailValidator emailValidator = EmailValidator.getInstance(true);
            if (!validateAddresses || emailValidator.isValid(address))
            {
                result = true;
            }
            else 
            {
                logger.error("Failed to send email to '" + address + "' as the address is incorrectly formatted" );
            }
          
            return result;
        }
    
       /**
        * @param ref    The node representing the current document ref (or null)
        * 
        * @return Model map for email templates
        */
       private Map<String, Object> createEmailTemplateModel(NodeRef ref, Map<String, Object> suppliedModel, NodeRef fromPerson)
       {
          Map<String, Object> model = new HashMap<String, Object>(8, 1.0f);
          
          if (fromPerson != null)
          {
              model.put("person", new TemplateNode(fromPerson, serviceRegistry, null));
          }      
          
          if (ref != null)
          {
              model.put("document", new TemplateNode(ref, serviceRegistry, null));
              NodeRef parent = serviceRegistry.getNodeService().getPrimaryParent(ref).getParentRef();
              model.put("space", new TemplateNode(parent, serviceRegistry, null));
          }
          
          // current date/time is useful to have and isn't supplied by FreeMarker by default
          model.put("date", new Date());
          
          // add custom method objects
          model.put("hasAspect", new HasAspectMethod());
          model.put("message", new I18NMessageMethod());
          model.put("dateCompare", new DateCompareMethod());
          
          // add URLs
          model.put("url", new URLHelper(sysAdminParams));
          model.put(TemplateService.KEY_SHARE_URL, UrlUtil.getShareUrl(this.serviceRegistry.getSysAdminParams()));
    
          if (imageResolver != null)
          {
              model.put(TemplateService.KEY_IMAGE_RESOLVER, imageResolver);
          }
    
          // if the caller specified a model, use it without overriding
          if(suppliedModel != null && suppliedModel.size() > 0)
          {
              for(String key : suppliedModel.keySet())
              {
                  if(model.containsKey(key))
                  {
                      if(logger.isDebugEnabled())
                      {
                          logger.debug("Not allowing overwriting of built in model parameter " + key);
                      }
                  }
                  else
                  {
                      model.put(key, suppliedModel.get(key));
                  }
              }
          }
          
          // all done
          return model;
       }
        
        /**
         * Add the parameter definitions
         */
        @Override
        protected void addParameterDefinitions(List<ParameterDefinition> paramList) 
        {
            paramList.add(new ParameterDefinitionImpl(PARAM_TO, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_TO)));
            paramList.add(new ParameterDefinitionImpl(PARAM_TO_MANY, DataTypeDefinition.ANY, false, getParamDisplayLabel(PARAM_TO_MANY), true));
            
            // NEW CHERRYSHOE add CC and BCC support
            paramList.add(new ParameterDefinitionImpl(PARAM_CC, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_CC)));
            paramList.add(new ParameterDefinitionImpl(PARAM_BCC, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_BCC)));
            paramList.add(new ParameterDefinitionImpl(PARAM_CC_MANY, DataTypeDefinition.ANY, false, getParamDisplayLabel(PARAM_CC_MANY), true));
            paramList.add(new ParameterDefinitionImpl(PARAM_BCC_MANY, DataTypeDefinition.ANY, false, getParamDisplayLabel(PARAM_BCC_MANY), true));
            
            paramList.add(new ParameterDefinitionImpl(PARAM_SUBJECT, DataTypeDefinition.TEXT, true, getParamDisplayLabel(PARAM_SUBJECT)));
            paramList.add(new ParameterDefinitionImpl(PARAM_TEXT, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_TEXT)));
            paramList.add(new ParameterDefinitionImpl(PARAM_FROM, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_FROM)));
            paramList.add(new ParameterDefinitionImpl(PARAM_TEMPLATE, DataTypeDefinition.NODE_REF, false, getParamDisplayLabel(PARAM_TEMPLATE), false, "ac-email-templates"));
            paramList.add(new ParameterDefinitionImpl(PARAM_TEMPLATE_MODEL, DataTypeDefinition.ANY, false, getParamDisplayLabel(PARAM_TEMPLATE_MODEL), true));
            paramList.add(new ParameterDefinitionImpl(PARAM_IGNORE_SEND_FAILURE, DataTypeDefinition.BOOLEAN, false, getParamDisplayLabel(PARAM_IGNORE_SEND_FAILURE)));
        }
    
        public void setTestMode(boolean testMode)
        {
            this.testMode = testMode;
        }
    
        public boolean isTestMode()
        {
            return testMode;
        }
    
        /**
         * Returns the most recent message that wasn't sent
         *  because TestMode had been enabled.
         */
        public MimeMessage retrieveLastTestMessage()
        {
            return lastTestMessage; 
        }
        
        /**
         * Used when test mode is enabled.
         * Clears the record of the last message that was sent. 
         */
        public void clearLastTestMessage()
        {
            lastTestMessage = null;
        }
    
        public void setFromEnabled(boolean fromEnabled)
        {
            this.fromEnabled = fromEnabled;
        }
    
        public boolean isFromEnabled()
        {
            return fromEnabled;
        }
    
        public static boolean isHTML(String value)
        {
            boolean result = false;
    
            // Note: only simplistic match here - expects <html tag at the start of the text
            String htmlPrefix = "<html";
            String trimmedText = value.trim();
            if (trimmedText.length() >= htmlPrefix.length() &&
                    trimmedText.substring(0, htmlPrefix.length()).equalsIgnoreCase(htmlPrefix))
            {
                result = true;
            }
    
            return result;
        }
    
        public static class URLHelper
        {
            private final SysAdminParams sysAdminParams;
            
            public URLHelper(SysAdminParams sysAdminParams)
            {
                this.sysAdminParams = sysAdminParams;
            }
            
            public String getContext()
            {
               return "/" + sysAdminParams.getAlfrescoContext();
            }
    
            public String getServerPath()
            {
                return sysAdminParams.getAlfrescoProtocol() + "://" + sysAdminParams.getAlfrescoHost() + ":"
                        + sysAdminParams.getAlfrescoPort();
            }
        }
        
        static AtomicInteger numberSuccessfulSends = new AtomicInteger(0);
        static AtomicInteger numberFailedSends = new AtomicInteger(0);
        protected void onSend()
        {
            numberSuccessfulSends.getAndIncrement();
        }
        
        protected void onFail()
        {
            numberFailedSends.getAndIncrement();
        }
        
        public int getNumberSuccessfulSends()
        {
            return numberSuccessfulSends.get();
        }
            
        public int getNumberFailedSends()
        {   
            return numberFailedSends.get();
        }
    }
    
    
  3. I also went ahead and created a custom version of MailActionExecuterMonitor, only changing the package to your new custom package (i.e. com.cherryshoe.action.executer).  I realize I probably didn't have to do this, since nothing was changed in the code besides the package.
    package com.cherryshoe.action.executer;
    
    import org.alfresco.error.AlfrescoRuntimeException;
    import org.springframework.extensions.surf.util.I18NUtil;
    
    public class MailActionExecuterMonitor
    {
        private MailActionExecuter mailActionExceuter;
        
        public String sendTestMessage()
        {
            try
            {
                mailActionExceuter.sendTestMessage();
                Object[] params = {mailActionExceuter.getTestMessageTo()};
                String message = I18NUtil.getMessage("email.outbound.test.send.success", params);
                return message;
            }
            catch
            (AlfrescoRuntimeException are)
            {
                return (are.getMessage());
            }
        }
        public int getNumberFailedSends()
        {
            return mailActionExceuter.getNumberFailedSends();
        }
        
        public int getNumberSuccessfulSends()
        {
            return mailActionExceuter.getNumberSuccessfulSends();
        }
        
        public void setMailActionExecuter(MailActionExecuter mailActionExceuter)
        {
            this.mailActionExceuter = mailActionExceuter;
        }
    }
    
    
    
  4. Overwrite alfresco's outboundSMTP-context.xml spring config file to point to your new custom MailActionExecuter and MailActionExecuterMonitor,  Just put this in the correct location in your AMP file and it will override the OOB location.  
    • Changes in the MailActionExecuterMonitor are indicated by comments that start with "NEW CHERRYSHOE" that tell you what was changed and why. TODO: I should do more research here as to see about not completely overwriting alfresco's version, and see if there was a way to override it.
    <?xml version='1.0' encoding='UTF-8'?>
    <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
    
    <!-- NEW CHERRYSHOE Override Alfresco OOB outboundSMTP-context.xml to add functionality
    for MailActionExecuter -->
    <beans>
       
       <!--                        -->
       <!-- MAIL SERVICE           -->
       <!--                        -->
        <import resource="classpath:alfresco/subsystems/email/OutboundSMTP/mail-template-services-context.xml"/>
       
        <bean id="mailService" class="org.alfresco.repo.mail.AlfrescoJavaMailSender">
          <property name="host">
             <value>${mail.host}</value>
          </property>
          <property name="port">
             <value>${mail.port}</value>
          </property>
            <property name="protocol">
             <value>${mail.protocol}</value>
          </property>
          <property name="username">
             <value>${mail.username}</value>
          </property>
          <property name="password">
             <value>${mail.password}</value>
          </property>
          <property name="defaultEncoding">
             <value>${mail.encoding}</value>
          </property>
            <property name="javaMailProperties">
            <props>
                    <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                    <prop key="mail.smtp.debug">${mail.smtp.debug}</prop>
                    <prop key="mail.smtp.timeout">${mail.smtp.timeout}</prop>
                    <prop key="mail.smtp.starttls.enable">${mail.smtp.starttls.enable}</prop>
                
                    <prop key="mail.smtps.auth">${mail.smtps.auth}</prop>
                    <prop key="mail.smtps.starttls.enable">${mail.smtps.starttls.enable}</prop>
            </props>
        </property>
            <property name="maxActive" value="${mail.transports.maxActive}"/>
            <property name="maxIdle" value="${mail.transports.maxIdle}"/>
            <property name="maxWait" value="${mail.tranports.maxWait}"/>
            <property name="minEvictableIdleTime" value="${mail.tranports.minEvictableIdleTime}"/>
            <property name="timeBetweenEvictionRuns" value="${mail.tranports.timeBetweenEvictionRuns}"/>
       </bean>
       
        <!-- NEW CHERRYSHOE Override the OOB MailActionExecuter with custom specific -->
        <bean id="mail" class="com.cherryshoe.action.executer.MailActionExecuter" parent="action-executer">
            <property name="queueName" value="outboundMail"/>
            <property name="mailService">
                <ref bean="mailService"></ref>
            </property>
            <property name="templateService">
                <ref bean="mailTemplateService"></ref>
            </property>
            <property name="personService">
                <ref bean="personService"></ref>
            </property>
            <property name="authenticationService">
                <ref bean="authenticationService"></ref>
            </property>
            <property name="nodeService">
                <ref bean="nodeService"></ref>
            </property>
            <property name="tenantService">
                <ref bean="tenantService"></ref>
            </property>
            <property name="authorityService">
                <ref bean="authorityService"></ref>
            </property>
            <property name="preferenceService">
                <ref bean="preferenceService"></ref>
            </property>
            <property name="serviceRegistry">
                <ref bean="ServiceRegistry"></ref>
            </property>
            <property name="imageResolver" ref="defaultImageResolver" />
            <property name="headerEncoding">
                <value>${mail.header}</value>
            </property>
            <property name="validateAddresses">
                <value>${mail.validate.addresses}</value>
                    </property>
                    <property name="fromAddress">
                            <value>${mail.from.default}</value>
                    </property>
                    <property name="fromEnabled">
             <value>${mail.from.enabled}</value>
            </property>
            <property name="sysAdminParams">
                <ref bean="sysAdminParams"></ref>
            </property>
            <property name="sendTestMessage">
                <value>${mail.testmessage.send}</value>
            </property>
            <property name="testMessageTo">
                <value>${mail.testmessage.to}</value>
            </property>
            <property name="testMessageSubject">
                <value>${mail.testmessage.subject}</value>
            </property>
            <property name="testMessageText">
                 <value>${mail.testmessage.text}</value>
            </property>
    
            <!-- For dev/test only - emails sent to test email, irrespective of intended recipient -->
            <property name="testModeRecipient" value="${dev.email.recipient.address}"/>
    
            <!-- For dev/test only - if true, emails are not actually sent -->
            <property name="testMode" value="${dev.email.not.sent}"/>
              
            <!-- NEW CHERRYSHOE I can't remember why I added this, must be in the class that MailActionExecuter is
            inherited from ActionExecuterAbstractBase -->
            <property name="publicAction">
                <value>false</value>
            </property>
        </bean>
         
        <!--  Put analytics actions on a dedicated asynchronous queue to ensure that they will not block
              any other ongoing asynchronous events in the system. -->
        <bean id="mailAsyncThreadPool" class="org.alfresco.util.ThreadPoolExecutorFactoryBean">
            <property name="poolName">
                <value>mailAsyncAction</value>
            </property>
            <property name="corePoolSize">
                <value>${mail.service.corePoolSize}</value>
            </property>
            <property name="maximumPoolSize">
                <value>${mail.service.maximumPoolSize}</value>
            </property>
        </bean>
        
        <bean id="mailAsynchronousActionExecutionQueue" class="org.alfresco.repo.action.AsynchronousActionExecutionQueueImpl" init-method="init">
          <property name="actionServiceImpl" ref="actionService"/>
          <property name="threadPoolExecutor">
             <ref bean="mailAsyncThreadPool"/>
          </property>
          <property name="transactionService">
             <ref bean="transactionService"/>
          </property>
          <property name="policyComponent">
             <ref bean="policyComponent"/>
          </property>
          <property name="id" value="outboundMail"/>
        </bean>
    
        <!-- NEW CHERRYSHOE Override the OOB MailActionExecuterMonitor with custom specific -->   
        <bean id="monitor" class="com.cherryshoe.action.executer.MailActionExecuterMonitor" >
            <property name="mailActionExecuter">
                <ref bean="mail"></ref>
            </property>
        </bean>
    
    </beans>

Wednesday, May 27, 2015

Case study of where certain JMX Mbean values are saved in Alfresco

Let's do a case study of two property value that can be overridden via alfreco-global.properties; synchronization.syncOnStartup and index.subsystem.name.  I was  particularly interested in these two properties because alfresco was not honoring the values I put in the alfresco-global.properties file.  That normally points to one of two things:  that another property file is overriding the value or that the properties are being overridden via JMX MBeans.  NOTE: I did this case study on Alfresco 4.1.1.27.

Using JConsole and navigating to the MBeans tab, I verified the location that alfresco documentation had pointed me to.  Note: Yes, I agree it's confusing that property values do not always match up with their MBean names.



  • synchronization.syncOnStartup:  Alfresco > Configuration > Search > manager > attributes > sourceBeanName













  • index.subsystem.name: Alfresco > Configuration  Synchronization > default > attributes > synchronization.syncOnStartup














  • The values were indeed different than what my alfresco-global.properties files were configured to.  To fix that, navigate to "Operations", you'll notice start|stop|revert operations available.  To have alfresco honor the values in your properties file, you will need to "revert" the value.  You can hit "revert" once and see the value that is replaced in the MBean.  You may need to hit "revert" numerous times, as as many times as the property may have been "set" previously.

    The Alfresco JMX MBean values are stored in the Alfresco database.  There are three tables that these values are stored in:

    • alf_prop_string_value: table where the MBean property is saved
    • alf_prop_root and alf_prop_unique_ctx: this will tell alfresco to honor the property file value or not
    Viewing all data in the alf_prop_string_value table, you can see the two MBean properties that we set.  You can tell that the sourceBeanName MBean got set twice (row 9 is the MBean, row 10 is first time it was set, row 11 is second time it was set), and that the synchronization.syncOnStartup got set once (row 13 is the MBean, row 14 is when it was set).














    This query locates the two MBean values that I know I manipulated, and shows the latest value it was set to:

    select r.id, sk.string_value, sv.string_value 
    from alf_prop_root r, alf_prop_link l, alf_prop_value k, alf_prop_value v, alf_prop_string_value sk, alf_prop_string_value sv 
    where 
    r.id = l.root_prop_id 
    and l.key_prop_id=k.id 
    and l.value_prop_id=v.id 
    and k.long_value=sk.id 
    and v.long_value=sv.id;


























    Several additional interesting things to note when I spoke with Alfresco on this topic:
    • It's not certain that all JMX MBeans work the same way, or are stored in the same tables.  
    • There's no easy way to tell if there are any JMX overridden values in the system.  You can use the queries above to see if you find the MBean that you interested in and see if it is stored in the same fashion.  
    • In a clustered environment, Alfresco MBean values are applied to all nodes in the cluster.  In other words, you cannot use an MBean to configured node 1 and node 2 differently.
    • The log4j MBeans are not stored in the database.  Any changes to log4j MBeans only persist until the alfresco server is restarted.  In other words, the log4j or custom-log4j properties will be honored as soon as alfresco is restarted.

    Sunday, March 29, 2015

    Alfresco - accessing a specific node's audit trail

    Alfresco auditing functionality is not enabled out-of-the-box, you'll have to enable it first.  There's also several auditing OOB webscripts available, including this one, where you get audit info of the last X audits in the system.

    PROBLEM:
    There is no alfresco oob webscript that grabs the audit trail by documentId/nodeReference. So we will roll our own modelling it off of how the Alfresco Explorer "Preview in Template" show_audit.ftl backend code works. 

    HIGH-LEVEL SOLUTION:
    The show_audit.ftl is eventually associated to TemplateNode.java, which eventually calls AuditServiceImpl.auditQuery().... to get the audits for that node. The TemplateNode.getAuditTrail() is first driven off of the PATH of the node in question (retrieved via the nodeService.getPath function). This retrieves the path up to the filename, which we can now access the document by node reference.  

    Now that we figured this out, we essentially pull everything out of the AuditServiceImpl.auditQuery() class method.  We'll copy this code into a new java-backed webscript, passing in the nodeReference as a webscript parameter, and you're off and running!!!


    DETAILED SOLUTION:

    We'll call the custom java-backed webscript RetrieveAuditTrailForNodeWebscript.java.

    package com.cherryshoe.webscript;
    
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.alfresco.error.AlfrescoRuntimeException;
    import org.alfresco.model.ContentModel;
    import org.alfresco.repo.model.Repository;
    import org.alfresco.repo.security.authentication.AuthenticationUtil;
    import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
    import org.alfresco.service.cmr.audit.AuditQueryParameters;
    import org.alfresco.service.cmr.audit.AuditService;
    import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
    import org.alfresco.service.cmr.repository.NodeRef;
    import org.alfresco.service.cmr.repository.NodeService;
    import org.alfresco.service.namespace.NamespaceService;
    import org.alfresco.service.namespace.QName;
    import org.alfresco.util.ISO9075;
    import org.apache.commons.lang.StringUtils;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.extensions.webscripts.Cache;
    import org.springframework.extensions.webscripts.DeclarativeWebScript;
    import org.springframework.extensions.webscripts.Status;
    import org.springframework.extensions.webscripts.WebScriptRequest;
    
    /*
     * This webscript retrieves audit trail info that is modeled off of the explorer show_audit.ftl backend code (TemplateNode.getAuditTrail)
     */
    public class RetrieveAuditTrailForNodeWebscript extends DeclarativeWebScript {
        private static Log logger = LogFactory.getLog(RetrieveAuditTrailService.class);
    
        private NodeService nodeService;
        private NamespaceService namespaceService;
        private AuditService auditService;
        private Repository repository;
        
        @Override
        protected Map<String, Object> executeImpl(WebScriptRequest req,
                Status status, Cache cache) {
            Map<String, Object> model = new HashMap<String, Object>();
            
            try {    
                // check if all required properties filled out
                String nodeRefString = req.getParameter("nodeRef");
                if(StringUtils.isBlank(nodeRefString)) throw new Exception("property 'nodeRef' missing when processing request");
    
                // strip off the :/ so workspace://SpacesStore/9d1095ca-214a-4f48-a162-b1452c566556 looks like workspace/SpacesStore/9d1095ca-214a-4f48-a162-b1452c566556
                nodeRefString = nodeRefString.replace(":/", "");
                NodeRef nodeRef = repository.findNodeRef("node", nodeRefString.split("/"));
                List<TemplateAuditInfo> auditTrailList = retrieveAuditTrail(nodeRef);
        
                // convert list to how we want it displayed
                String applicationName = "";
                List<Map<String, Object>> forDisplayList = new ArrayList<Map<String, Object>>(auditTrailList.size());
                for (TemplateAuditInfo tai: auditTrailList) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(tai.toString());
                    }
                    Map<String, Object> mapToAdd = new HashMap<String, Object>();
                    mapToAdd.put("id", tai.getId());
                    mapToAdd.put("userName", tai.getUserIdentifier());
                    mapToAdd.put("applicationMethod", tai.getAuditMethod());
                    mapToAdd.put("date", tai.getDate());
                    forDisplayList.add(mapToAdd);
                    if (StringUtils.isBlank(applicationName)) applicationName = tai.getAuditApplication();
                }
                
                Map<QName, Serializable> props = nodeService.getProperties(nodeRef);
                String fileName = (String)props.get(ContentModel.PROP_NAME);
                                    
                model.put("data", forDisplayList);
                model.put("applicationName", applicationName);
                model.put("nodeRef", nodeRef.getId());
                model.put("fileName", fileName);
                model.put("count", auditTrailList.size());
                model.put("returnStatus", Boolean.TRUE);
                model.put("statusMessage", "Successfully retrieved audit trail for nodeRef[" + nodeRef.getId() + "]");
                
            } catch (Exception e) {
                logger.warn(e.getMessage());
                model.put("returnStatus", Boolean.FALSE);
                model.put("statusMessage", e.getMessage());
            }
            return model;
        }
    
        protected List<TemplateAuditInfo> retrieveAuditTrail(NodeRef nodeRef) {
            final List<TemplateAuditInfo> result = new ArrayList<TemplateAuditInfo>();
            
            // create the callback for auditQuery method
            final AuditQueryCallback callback = new AuditQueryCallback()
            {
                public boolean valuesRequired()
                {
                    return true;
                }
    
                public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
                {
                    throw new AlfrescoRuntimeException("Failed to retrieve audit data.", error);
                }
    
                public boolean handleAuditEntry(Long entryId, String applicationName, String user, long time,
                        Map<String, Serializable> values)
                {
                    TemplateAuditInfo auditInfo = new TemplateAuditInfo(entryId, applicationName, user, time, values);
                    result.add(auditInfo);
                    return true;
                }
            };
    
            // resolve the path of the node 
            final String nodePath = ISO9075.decode(nodeService.getPath(nodeRef).toPrefixString(namespaceService)); 
    
            // run as admin user to allow everyone to see audit information
            // (new 3.4 API doesn't allow this by default)
            AuthenticationUtil.runAs(new RunAsWork<Object>()
            {
                public Object doWork() throws Exception
                {
                    String applicationName = "alfresco-access";
                    AuditQueryParameters pathParams = new AuditQueryParameters();
                    pathParams.setApplicationName(applicationName);
                    pathParams.addSearchKey("/alfresco-access/transaction/path", nodePath);
                    auditService.auditQuery(callback, pathParams, -1);
                    
                    AuditQueryParameters copyFromPathParams = new AuditQueryParameters();
                    copyFromPathParams.setApplicationName(applicationName);
                    copyFromPathParams.addSearchKey("/alfresco-access/transaction/copy/from/path", nodePath);
                    auditService.auditQuery(callback, copyFromPathParams, -1);
                    
                    AuditQueryParameters moveFromPathParams = new AuditQueryParameters();
                    moveFromPathParams.setApplicationName(applicationName);
                    moveFromPathParams.addSearchKey("/alfresco-access/transaction/move/from/path", nodePath);
                    auditService.auditQuery(callback, moveFromPathParams, -1);
                    return null;
                }
            }, AuthenticationUtil.getAdminUserName());
            
            // sort audit entries by time of generation
            Collections.sort(result, new Comparator<TemplateAuditInfo>()
            {
                public int compare(TemplateAuditInfo o1, TemplateAuditInfo o2)
                {
                    return o1.getDate().compareTo(o2.getDate());
                }
            });
            return result;
        }
        
        public class TemplateAuditInfo
        {
            private Long id;
            private String applicationName;
            private String userName;
            private long time;
            private Map<String, Serializable> values;
    
            public TemplateAuditInfo(Long id, String applicationName, String userName, long time, Map<String, Serializable> values)
            {
                this.id = id;
                this.applicationName = applicationName;
                this.userName = userName;
                this.time = time;
                this.values = values;
            }
            
            public Long getId() {
                return id;
            }
            
            public void setId(Long id) {
                this.id = id;
            }
            
            public String getAuditApplication()
            {
                return this.applicationName;
            }
    
            public String getUserIdentifier()
            {
                return this.userName;
            }
    
            public Date getDate()
            {
                return new Date(time);
            }
    
            public String getAuditMethod()
            {
                return this.values.get("/alfresco-access/transaction/action").toString();
            }
            
            public Map<String, Serializable> getValues()
            {
                return this.values;
            }
    
            @Override
            public String toString() {
                return "TemplateAuditInfo [id=" + id + ", applicationName="
                        + applicationName + ", userName=" + userName + ", time="
                        + time + ", values=" + values + "]";
            }      
            
        }
    
        public void setNodeService(NodeService nodeService) {
            this.nodeService = nodeService;
        }
    
        public void setNamespaceService(NamespaceService namespaceService) {
            this.namespaceService = namespaceService;
        }
    
        public void setAuditService(AuditService auditService) {
            this.auditService = auditService;
        }
    
        public void setRepository(Repository repository) {
            this.repository = repository;
        }
    
        
    }