org.alfresco.repo.security.person.PersonServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.security.person.PersonServiceImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.security.person;

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.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.events.types.Event;
import org.alfresco.events.types.UserManagementEvent;
import org.alfresco.model.ContentModel;
import org.alfresco.query.CannedQueryFactory;
import org.alfresco.query.CannedQueryResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.action.executer.MailActionExecuter;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.TransactionalCache;
import org.alfresco.repo.domain.permissions.AclDAO;
import org.alfresco.repo.events.EventPreparator;
import org.alfresco.repo.events.EventPublisher;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.search.SearcherException;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.PermissionServiceSPI;
import org.alfresco.repo.tenant.TenantDomainMismatchException;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.invitation.InvitationException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.InvalidStoreRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.TemplateService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.LimitBy;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.NoSuchPersonException;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.ModelUtil;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PersonServiceImpl extends TransactionListenerAdapter implements PersonService,
        NodeServicePolicies.BeforeCreateNodePolicy, NodeServicePolicies.OnCreateNodePolicy,
        NodeServicePolicies.BeforeDeleteNodePolicy, NodeServicePolicies.OnUpdatePropertiesPolicy

{
    private static Log logger = LogFactory.getLog(PersonServiceImpl.class);

    private static final String CANNED_QUERY_PEOPLE_LIST = "getPeopleCannedQueryFactory";

    private static final String DELETE = "DELETE";
    private static final String SPLIT = "SPLIT";
    private static final String LEAVE = "LEAVE";
    public static final String SYSTEM_FOLDER_SHORT_QNAME = "sys:system";
    public static final String PEOPLE_FOLDER_SHORT_QNAME = "sys:people";

    private static final String KEY_POST_TXN_DUPLICATES = "PersonServiceImpl.KEY_POST_TXN_DUPLICATES";
    public static final String KEY_ALLOW_UID_UPDATE = "PersonServiceImpl.KEY_ALLOW_UID_UPDATE";
    private static final String KEY_USERS_CREATED = "PersonServiceImpl.KEY_USERS_CREATED";

    private StoreRef storeRef;
    private TransactionService transactionService;
    private NodeService nodeService;
    private TenantService tenantService;
    private SearchService searchService;
    private AuthorityService authorityService;
    private MutableAuthenticationService authenticationService;
    private DictionaryService dictionaryService;
    private PermissionServiceSPI permissionServiceSPI;
    private NamespacePrefixResolver namespacePrefixResolver;
    private HomeFolderManager homeFolderManager;
    private PolicyComponent policyComponent;
    private AclDAO aclDao;
    private PermissionsManager permissionsManager;
    private RepoAdminService repoAdminService;
    private ServiceRegistry serviceRegistry;

    private boolean createMissingPeople;
    private static Set<QName> mutableProperties;
    private String defaultHomeFolderProvider;
    private boolean processDuplicates = true;
    private String duplicateMode = LEAVE;
    private boolean lastIsBest = true;
    private boolean includeAutoCreated = false;
    private EventPublisher eventPublisher;

    private NamedObjectRegistry<CannedQueryFactory<NodeRef>> cannedQueryRegistry;

    /** a transactionally-safe cache to be injected */
    private SimpleCache<String, Set<NodeRef>> personCache;

    // note: cache is tenant-aware (if using EhCacheAdapter shared cache)
    private SimpleCache<String, Object> singletonCache; // eg. for peopleContainerNodeRef
    private final String KEY_PEOPLECONTAINER_NODEREF = "key.peoplecontainer.noderef";

    private UserNameMatcher userNameMatcher;

    private JavaBehaviour beforeCreateNodeValidationBehaviour;
    private JavaBehaviour beforeDeleteNodeValidationBehaviour;

    private boolean homeFolderCreationEager;

    private boolean homeFolderCreationDisabled = false; // if true then home folders creation is disabled (ie. home folders are not created - neither eagerly nor lazily)

    static {
        Set<QName> props = new HashSet<QName>();
        props.add(ContentModel.PROP_HOMEFOLDER);
        props.add(ContentModel.PROP_FIRSTNAME);
        // Middle Name
        props.add(ContentModel.PROP_LASTNAME);
        props.add(ContentModel.PROP_EMAIL);
        props.add(ContentModel.PROP_ORGID);
        mutableProperties = Collections.unmodifiableSet(props);
    }

    @Override
    public boolean equals(Object obj) {
        return this == obj;
    }

    @Override
    public int hashCode() {
        return 1;
    }

    /**
     * Spring bean init method
     */
    public void init() {
        PropertyCheck.mandatory(this, "storeUrl", storeRef);
        PropertyCheck.mandatory(this, "transactionService", transactionService);
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "permissionServiceSPI", permissionServiceSPI);
        PropertyCheck.mandatory(this, "authorityService", authorityService);
        PropertyCheck.mandatory(this, "authenticationService", authenticationService);
        PropertyCheck.mandatory(this, "namespacePrefixResolver", namespacePrefixResolver);
        PropertyCheck.mandatory(this, "policyComponent", policyComponent);
        PropertyCheck.mandatory(this, "personCache", personCache);
        PropertyCheck.mandatory(this, "aclDao", aclDao);
        PropertyCheck.mandatory(this, "homeFolderManager", homeFolderManager);
        PropertyCheck.mandatory(this, "repoAdminService", repoAdminService);

        beforeCreateNodeValidationBehaviour = new JavaBehaviour(this, "beforeCreateNodeValidation");
        this.policyComponent.bindClassBehaviour(BeforeCreateNodePolicy.QNAME, ContentModel.TYPE_PERSON,
                beforeCreateNodeValidationBehaviour);

        beforeDeleteNodeValidationBehaviour = new JavaBehaviour(this, "beforeDeleteNodeValidation");
        this.policyComponent.bindClassBehaviour(BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_PERSON,
                beforeDeleteNodeValidationBehaviour);

        this.policyComponent.bindClassBehaviour(OnCreateNodePolicy.QNAME, ContentModel.TYPE_PERSON,
                new JavaBehaviour(this, "onCreateNode"));

        this.policyComponent.bindClassBehaviour(BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_PERSON,
                new JavaBehaviour(this, "beforeDeleteNode"));

        this.policyComponent.bindClassBehaviour(OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_PERSON,
                new JavaBehaviour(this, "onUpdateProperties"));

        this.policyComponent.bindClassBehaviour(OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_USER,
                new JavaBehaviour(this, "onUpdatePropertiesUser"));
    }

    /**
     * {@inheritDoc}
     */
    public void setCreateMissingPeople(boolean createMissingPeople) {
        this.createMissingPeople = createMissingPeople;
    }

    public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) {
        this.namespacePrefixResolver = namespacePrefixResolver;
    }

    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }

    public void setAuthenticationService(MutableAuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    public void setPermissionServiceSPI(PermissionServiceSPI permissionServiceSPI) {
        this.permissionServiceSPI = permissionServiceSPI;
    }

    public void setTransactionService(TransactionService transactionService) {
        this.transactionService = transactionService;
    }

    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }

    public void setSingletonCache(SimpleCache<String, Object> singletonCache) {
        this.singletonCache = singletonCache;
    }

    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public void setRepoAdminService(RepoAdminService repoAdminService) {
        this.repoAdminService = repoAdminService;
    }

    public void setPolicyComponent(PolicyComponent policyComponent) {
        this.policyComponent = policyComponent;
    }

    public void setStoreUrl(String storeUrl) {
        this.storeRef = new StoreRef(storeUrl);
    }

    public void setUserNameMatcher(UserNameMatcher userNameMatcher) {
        this.userNameMatcher = userNameMatcher;
    }

    void setDefaultHomeFolderProvider(String defaultHomeFolderProvider) {
        this.defaultHomeFolderProvider = defaultHomeFolderProvider;
    }

    public void setDuplicateMode(String duplicateMode) {
        this.duplicateMode = duplicateMode;
    }

    public void setIncludeAutoCreated(boolean includeAutoCreated) {
        this.includeAutoCreated = includeAutoCreated;
    }

    public void setLastIsBest(boolean lastIsBest) {
        this.lastIsBest = lastIsBest;
    }

    public void setProcessDuplicates(boolean processDuplicates) {
        this.processDuplicates = processDuplicates;
    }

    public void setHomeFolderManager(HomeFolderManager homeFolderManager) {
        this.homeFolderManager = homeFolderManager;
    }

    /**
     * Indicates if home folders should be created when the person
     * is created or delayed until first accessed.
     */
    public void setHomeFolderCreationEager(boolean homeFolderCreationEager) {
        this.homeFolderCreationEager = homeFolderCreationEager;
    }

    /**
     * Indicates if home folder creation should be disabled.
     */
    public void setHomeFolderCreationDisabled(boolean homeFolderCreationDisabled) {
        this.homeFolderCreationDisabled = homeFolderCreationDisabled;
    }

    public void setAclDAO(AclDAO aclDao) {
        this.aclDao = aclDao;
    }

    public void setPermissionsManager(PermissionsManager permissionsManager) {
        this.permissionsManager = permissionsManager;
    }

    /**
     * Set the registry of {@link CannedQueryFactory canned queries}
     */
    public void setCannedQueryRegistry(NamedObjectRegistry<CannedQueryFactory<NodeRef>> cannedQueryRegistry) {
        this.cannedQueryRegistry = cannedQueryRegistry;
    }

    /**
     * Set the username to person cache.
     */
    public void setPersonCache(SimpleCache<String, Set<NodeRef>> personCache) {
        this.personCache = personCache;
    }

    /**
     * Avoid injection issues: Look it up from the Service Registry as required
     */
    private FileFolderService getFileFolderService() {
        return serviceRegistry.getFileFolderService();
    }

    /**
     * Avoid injection issues: Look it up from the Service Registry as required
     */
    private NamespaceService getNamespaceService() {
        return serviceRegistry.getNamespaceService();
    }

    /**
     * Avoid injection issues: Look it up from the Service Registry as required
     */
    private ActionService getActionService() {
        return serviceRegistry.getActionService();
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef getPerson(String userName) {
        return getPerson(userName, true);
    }

    /**
     * {@inheritDoc}
     */
    public PersonInfo getPerson(NodeRef personRef) throws NoSuchPersonException {
        Map<QName, Serializable> props = null;
        try {
            props = nodeService.getProperties(personRef);
        } catch (InvalidNodeRefException inre) {
            throw new NoSuchPersonException(personRef.toString());
        }

        String username = (String) props.get(ContentModel.PROP_USERNAME);
        if (username == null) {
            throw new NoSuchPersonException(personRef.toString());
        }

        return new PersonInfo(personRef, username, (String) props.get(ContentModel.PROP_FIRSTNAME),
                (String) props.get(ContentModel.PROP_LASTNAME));
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef getPersonOrNull(String userName) {
        return getPersonImpl(userName, false, false);
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef getPerson(final String userName, final boolean autoCreateHomeFolderAndMissingPersonIfAllowed) {
        return getPersonImpl(userName, autoCreateHomeFolderAndMissingPersonIfAllowed, true);
    }

    private NodeRef getPersonImpl(final String userName,
            final boolean autoCreateHomeFolderAndMissingPersonIfAllowed, final boolean exceptionOrNull) {
        if (userName == null || userName.length() == 0) {
            return null;
        }
        final NodeRef personNode = getPersonOrNullImpl(userName);
        if (personNode == null) {
            TxnReadState txnReadState = AlfrescoTransactionSupport.getTransactionReadState();
            if (autoCreateHomeFolderAndMissingPersonIfAllowed && createMissingPeople()
                    && txnReadState == TxnReadState.TXN_READ_WRITE) {
                // We create missing people AND are in a read-write txn
                return createMissingPerson(userName, true);
            } else {
                if (exceptionOrNull) {
                    throw new NoSuchPersonException(userName);
                }
            }
        } else if (autoCreateHomeFolderAndMissingPersonIfAllowed) {
            makeHomeFolderIfRequired(personNode);
        }
        return personNode;
    }

    /**
     * {@inheritDoc}
     */
    public boolean personExists(String caseSensitiveUserName) {
        NodeRef person = getPersonOrNullImpl(caseSensitiveUserName);
        if (person != null) {
            // re: THOR-293
            return permissionServiceSPI.hasPermission(person, PermissionService.READ) == AccessStatus.ALLOWED;
        }
        return false;
    }

    private NodeRef getPersonOrNullImpl(String searchUserName) {
        Set<NodeRef> allRefs = getFromCache(searchUserName);
        boolean addToCache = false;
        if (allRefs == null) {
            NodeRef peopleContainer = null;
            try {
                peopleContainer = getPeopleContainer();
            } catch (InvalidStoreRefException isre) {
                return null;
            }

            List<ChildAssociationRef> childRefs = nodeService.getChildAssocs(peopleContainer,
                    ContentModel.ASSOC_CHILDREN, getChildNameLower(searchUserName), false);
            allRefs = new LinkedHashSet<NodeRef>(childRefs.size() * 2);

            for (ChildAssociationRef childRef : childRefs) {
                NodeRef nodeRef = childRef.getChildRef();
                allRefs.add(nodeRef);
            }
            addToCache = true;
        }

        List<NodeRef> refs = new ArrayList<NodeRef>(allRefs.size());
        Set<NodeRef> nodesToRemoveFromCache = new HashSet<NodeRef>();
        for (NodeRef nodeRef : allRefs) {
            if (nodeService.exists(nodeRef)) {
                Serializable value = nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME);
                String realUserName = DefaultTypeConverter.INSTANCE.convert(String.class, value);
                if (userNameMatcher.matches(searchUserName, realUserName)) {
                    refs.add(nodeRef);
                }
            } else {
                nodesToRemoveFromCache.add(nodeRef);
            }
        }

        if (!nodesToRemoveFromCache.isEmpty()) {
            allRefs.removeAll(nodesToRemoveFromCache);
        }

        NodeRef returnRef = null;
        if (refs.size() > 1) {
            returnRef = handleDuplicates(refs, searchUserName);
        } else if (refs.size() == 1) {
            returnRef = refs.get(0);

            if (addToCache) {
                // Don't bother caching unless we get a result that doesn't need duplicate processing
                putToCache(searchUserName, allRefs, false);
            }
        }
        return returnRef;
    }

    private NodeRef handleDuplicates(List<NodeRef> refs, String searchUserName) {
        if (processDuplicates) {
            NodeRef best = findBest(refs);
            HashSet<NodeRef> toHandle = new HashSet<NodeRef>();
            toHandle.addAll(refs);
            toHandle.remove(best);
            addDuplicateNodeRefsToHandle(toHandle);
            return best;
        } else {
            String userNameSensitivity = " (user name is case-"
                    + (userNameMatcher.getUserNamesAreCaseSensitive() ? "sensitive" : "insensitive") + ")";
            String domainNameSensitivity = "";
            if (!userNameMatcher.getDomainSeparator().equals("")) {
                domainNameSensitivity = " (domain name is case-"
                        + (userNameMatcher.getDomainNamesAreCaseSensitive() ? "sensitive" : "insensitive") + ")";
            }

            throw new AlfrescoRuntimeException(
                    "Found more than one user for " + searchUserName + userNameSensitivity + domainNameSensitivity);
        }
    }

    /**
     * Get the txn-bound usernames that need cleaning up
     */
    private Set<NodeRef> getPostTxnDuplicates() {
        @SuppressWarnings("unchecked")
        Set<NodeRef> postTxnDuplicates = (Set<NodeRef>) AlfrescoTransactionSupport
                .getResource(KEY_POST_TXN_DUPLICATES);
        if (postTxnDuplicates == null) {
            postTxnDuplicates = new HashSet<NodeRef>();
            AlfrescoTransactionSupport.bindResource(KEY_POST_TXN_DUPLICATES, postTxnDuplicates);
        }
        return postTxnDuplicates;
    }

    /**
     * Flag a username for cleanup after the transaction.
     */
    private void addDuplicateNodeRefsToHandle(Set<NodeRef> refs) {
        // Firstly, bind this service to the transaction
        AlfrescoTransactionSupport.bindListener(this);
        // Now get the post txn duplicate list
        Set<NodeRef> postTxnDuplicates = getPostTxnDuplicates();
        postTxnDuplicates.addAll(refs);
    }

    /**
     * Process clean up any duplicates that were flagged during the transaction.
     */
    @Override
    public void afterCommit() {
        // Get the duplicates in a form that can be read by the transaction work anonymous instance
        final Set<NodeRef> postTxnDuplicates = getPostTxnDuplicates();
        if (postTxnDuplicates.size() == 0) {
            // Nothing to do
            return;
        }

        RetryingTransactionCallback<Object> processDuplicateWork = new RetryingTransactionCallback<Object>() {
            public Object execute() throws Throwable {
                if (duplicateMode.equalsIgnoreCase(SPLIT)) {
                    logger.info("Splitting " + postTxnDuplicates.size() + " duplicate person objects.");
                    // Allow UIDs to be updated in this transaction
                    AlfrescoTransactionSupport.bindResource(KEY_ALLOW_UID_UPDATE, Boolean.TRUE);
                    split(postTxnDuplicates);
                    logger.info("Split " + postTxnDuplicates.size() + " duplicate person objects.");
                } else if (duplicateMode.equalsIgnoreCase(DELETE)) {
                    delete(postTxnDuplicates);
                    logger.info("Deleted duplicate person objects");
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Duplicate person objects exist");
                    }
                }

                // Done
                return null;
            }
        };
        transactionService.getRetryingTransactionHelper().doInTransaction(processDuplicateWork, false, true);
    }

    private void delete(Set<NodeRef> toDelete) {
        for (NodeRef nodeRef : toDelete) {
            deletePerson(nodeRef);
        }
    }

    private void split(Set<NodeRef> toSplit) {
        for (NodeRef nodeRef : toSplit) {
            String userName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME);
            String newUserName = userName + GUID.generate();
            nodeService.setProperty(nodeRef, ContentModel.PROP_USERNAME, userName + GUID.generate());
            logger.info("   New person object: " + newUserName);
        }
    }

    private NodeRef findBest(List<NodeRef> refs) {
        // Given that we might not have audit attributes, use the assumption that the node ID increases to sort the
        // nodes
        if (lastIsBest) {
            Collections.sort(refs, new NodeIdComparator(nodeService, false));
        } else {
            Collections.sort(refs, new NodeIdComparator(nodeService, true));
        }

        NodeRef fallBack = null;

        for (NodeRef nodeRef : refs) {
            if (fallBack == null) {
                fallBack = nodeRef;
            }

            if (includeAutoCreated || !wasAutoCreated(nodeRef)) {
                return nodeRef;
            }
        }

        return fallBack;
    }

    private boolean wasAutoCreated(NodeRef nodeRef) {
        String userName = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME));

        String testString = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_FIRSTNAME));
        if ((testString == null) || !testString.equals(userName)) {
            return false;
        }

        testString = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_LASTNAME));
        if ((testString == null) || !testString.equals("")) {
            return false;
        }

        testString = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_EMAIL));
        if ((testString == null) || !testString.equals("")) {
            return false;
        }

        testString = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_ORGID));
        if ((testString == null) || !testString.equals("")) {
            return false;
        }

        testString = DefaultTypeConverter.INSTANCE.convert(String.class,
                nodeService.getProperty(nodeRef, ContentModel.PROP_HOME_FOLDER_PROVIDER));
        if ((testString == null) || !testString.equals(defaultHomeFolderProvider)) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean createMissingPeople() {
        return createMissingPeople;
    }

    /**
     * {@inheritDoc}
     */
    public Set<QName> getMutableProperties() {
        return mutableProperties;
    }

    /**
     * {@inheritDoc}
     */
    public void setPersonProperties(String userName, Map<QName, Serializable> properties) {
        setPersonProperties(userName, properties, true);
    }

    /**
     * {@inheritDoc}
     */
    public void setPersonProperties(String userName, Map<QName, Serializable> properties,
            boolean autoCreateHomeFolder) {
        NodeRef personNode = getPersonOrNullImpl(userName);
        if (personNode == null) {
            if (createMissingPeople()) {
                personNode = createMissingPerson(userName, autoCreateHomeFolder);
            } else {
                throw new PersonException("No person found for user name " + userName);
            }
        } else {
            // Must create the home folder first as a property holds its location.
            if (autoCreateHomeFolder) {
                makeHomeFolderIfRequired(personNode);
            }
            String realUserName = DefaultTypeConverter.INSTANCE.convert(String.class,
                    nodeService.getProperty(personNode, ContentModel.PROP_USERNAME));
            String suggestedUserName;

            // LDAP sync: allow change of case if we have case insensitive user names and the same name in a different case
            if (getUserNamesAreCaseSensitive()
                    || (suggestedUserName = (String) properties.get(ContentModel.PROP_USERNAME)) == null
                    || !suggestedUserName.equalsIgnoreCase(realUserName)) {
                properties.put(ContentModel.PROP_USERNAME, realUserName);
            }
        }

        checkIfPersonShouldBeDisabledAndSetAspect(personNode, properties);

        Map<QName, Serializable> update = nodeService.getProperties(personNode);
        update.putAll(properties);

        nodeService.setProperties(personNode, update);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isMutable() {
        return true;
    }

    private NodeRef createMissingPerson(String userName, boolean autoCreateHomeFolder) {
        HashMap<QName, Serializable> properties = getDefaultProperties(userName);
        NodeRef person = createPerson(properties);

        // The home folder will ONLY exist after the the person is created if
        // homeFolderCreationEager == true
        if (autoCreateHomeFolder && homeFolderCreationEager == false) {
            makeHomeFolderIfRequired(person);
        }

        return person;
    }

    private void makeHomeFolderIfRequired(NodeRef person) {
        if ((person != null) && (homeFolderCreationDisabled == false)) {
            NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class,
                    nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER));
            if (homeFolder == null) {
                final ChildAssociationRef ref = nodeService.getPrimaryParent(person);
                RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
                txnHelper.setForceWritable(true);
                boolean requiresNew = false;
                if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE) {
                    // We can be in a read-only transaction, so force a new transaction
                    // Note that the transaction will *always* be in read-only mode if the server read-only veto is there 
                    requiresNew = true;
                }
                txnHelper.doInTransaction(new RetryingTransactionCallback<Object>() {
                    public Object execute() throws Throwable {
                        makeHomeFolderAsSystem(ref);
                        return null;
                    }
                }, false, requiresNew);
            }
        }
    }

    private void makeHomeFolderAsSystem(final ChildAssociationRef childAssocRef) {
        AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Object>() {
            @Override
            public Object doWork() throws Exception {
                homeFolderManager.makeHomeFolder(childAssocRef);
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    private HashMap<QName, Serializable> getDefaultProperties(String userName) {
        HashMap<QName, Serializable> properties = new HashMap<QName, Serializable>();
        properties.put(ContentModel.PROP_USERNAME, userName);
        properties.put(ContentModel.PROP_FIRSTNAME, tenantService.getBaseNameUser(userName));
        properties.put(ContentModel.PROP_LASTNAME, "");
        properties.put(ContentModel.PROP_EMAIL, "");
        properties.put(ContentModel.PROP_ORGID, "");
        properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, defaultHomeFolderProvider);

        properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
        properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota

        return properties;
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef createPerson(Map<QName, Serializable> properties) {
        return createPerson(properties, authorityService.getDefaultZones());
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef createPerson(Map<QName, Serializable> properties, Set<String> zones) {
        ParameterCheck.mandatory("properties", properties);
        String userName = DefaultTypeConverter.INSTANCE.convert(String.class,
                properties.get(ContentModel.PROP_USERNAME));
        if (userName == null) {
            throw new IllegalArgumentException("No username specified when creating the person.");
        }

        AuthorityType authorityType = AuthorityType.getAuthorityType(userName);
        if (authorityType != AuthorityType.USER) {
            throw new AlfrescoRuntimeException("Attempt to create person for an authority which is not a user");
        }

        tenantService.checkDomainUser(userName);

        if (personExists(userName)) {
            throw new AlfrescoRuntimeException("Person '" + userName + "' already exists.");
        }

        properties.put(ContentModel.PROP_USERNAME, userName);
        properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);

        NodeRef personRef = null;
        try {
            beforeCreateNodeValidationBehaviour.disable();

            personRef = nodeService
                    .createNode(getPeopleContainer(), ContentModel.ASSOC_CHILDREN, getChildNameLower(userName), // Lowercase:
                            ContentModel.TYPE_PERSON, properties)
                    .getChildRef();
        } finally {
            beforeCreateNodeValidationBehaviour.enable();
        }

        checkIfPersonShouldBeDisabledAndSetAspect(personRef, properties);

        if (zones != null) {
            for (String zone : zones) {
                // Add the person to an authentication zone (corresponding to an external user registry)
                // Let's preserve case on this child association
                nodeService.addChild(authorityService.getOrCreateZone(zone), personRef, ContentModel.ASSOC_IN_ZONE,
                        QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, userName,
                                namespacePrefixResolver));
            }
        }

        removeFromCache(userName, false);

        publishEvent("user.create", this.nodeService.getProperties(personRef));

        return personRef;
    }

    private void checkIfPersonShouldBeDisabledAndSetAspect(NodeRef person, Map<QName, Serializable> properties) {
        if (properties.get(ContentModel.PROP_ENABLED) != null) {
            boolean isEnabled = Boolean.parseBoolean(properties.get(ContentModel.PROP_ENABLED).toString());

            if (isEnabled) {
                if (nodeService.hasAspect(person, ContentModel.ASPECT_PERSON_DISABLED)) {
                    nodeService.removeAspect(person, ContentModel.ASPECT_PERSON_DISABLED);
                }
            } else {
                nodeService.addAspect(person, ContentModel.ASPECT_PERSON_DISABLED, null);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void notifyPerson(final String userName, final String password) {
        // Get the details of our user, or fail trying
        NodeRef noderef = getPerson(userName, false);
        Map<QName, Serializable> userProps = nodeService.getProperties(noderef);

        // Do they have an email set? We can't email them if not...
        String email = null;
        if (userProps.containsKey(ContentModel.PROP_EMAIL)) {
            email = (String) userProps.get(ContentModel.PROP_EMAIL);
        }

        if (email == null || email.length() == 0) {
            if (logger.isInfoEnabled()) {
                logger.info("Not sending new user notification to " + userName + " as no email address found");
            }

            return;
        }

        // We need a freemarker model, so turn the QNames into
        //  something a bit more freemarker friendly
        Map<String, Serializable> model = buildEmailTemplateModel(userProps);
        model.put("password", password); // Not stored on the person

        // Set the details of the person sending the email into the model
        NodeRef creatorNR = getPerson(AuthenticationUtil.getFullyAuthenticatedUser());
        Map<QName, Serializable> creatorProps = nodeService.getProperties(creatorNR);
        Map<String, Serializable> creator = buildEmailTemplateModel(creatorProps);
        model.put("creator", (Serializable) creator);

        // Set share information into the model
        String productName = ModelUtil.getProductName(repoAdminService);
        model.put(TemplateService.KEY_PRODUCT_NAME, productName);

        // Set the details for the action
        Map<String, Serializable> actionParams = new HashMap<String, Serializable>();
        actionParams.put(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) model);
        actionParams.put(MailActionExecuter.PARAM_TO, email);
        actionParams.put(MailActionExecuter.PARAM_FROM, creatorProps.get(ContentModel.PROP_EMAIL));
        actionParams.put(MailActionExecuter.PARAM_SUBJECT, "invitation.notification.person.email.subject");
        actionParams.put(MailActionExecuter.PARAM_SUBJECT_PARAMS, new Object[] { productName });

        // Pick the appropriate localised template
        actionParams.put(MailActionExecuter.PARAM_TEMPLATE, getNotifyEmailTemplateNodeRef());

        // Ask for the email to be sent asynchronously
        Action mailAction = getActionService().createAction(MailActionExecuter.NAME, actionParams);
        getActionService().executeAction(mailAction, noderef, false, true);
    }

    /**
     * Finds the email template and then attempts to find a localized version
     */
    private NodeRef getNotifyEmailTemplateNodeRef() {
        // Find the new user email template
        String xpath = "app:company_home/app:dictionary/app:email_templates/cm:invite/cm:new-user-email.html.ftl";
        try {
            NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
            List<NodeRef> nodeRefs = searchService.selectNodes(rootNodeRef, xpath, null, getNamespaceService(),
                    false);
            if (nodeRefs.size() > 1) {
                logger.error("Found too many email templates using: " + xpath);
                nodeRefs = Collections.singletonList(nodeRefs.get(0));
            } else if (nodeRefs.size() == 0) {
                throw new InvitationException("Cannot find the email template using " + xpath);
            }
            // Now localise this
            NodeRef base = nodeRefs.get(0);
            NodeRef local = getFileFolderService().getLocalizedSibling(base);
            return local;
        } catch (SearcherException e) {
            throw new InvitationException("Cannot find the email template!", e);
        }
    }

    private Map<String, Serializable> buildEmailTemplateModel(Map<QName, Serializable> props) {
        Map<String, Serializable> model = new HashMap<String, Serializable>((int) (props.size() * 1.5));
        for (QName qname : props.keySet()) {
            model.put(qname.getLocalName(), props.get(qname));
            model.put(qname.getLocalName().toLowerCase(), props.get(qname));
        }
        return model;
    }

    /**
     * {@inheritDoc}
     */
    public NodeRef getPeopleContainer() {
        NodeRef peopleNodeRef = (NodeRef) singletonCache.get(KEY_PEOPLECONTAINER_NODEREF);
        if (peopleNodeRef == null) {
            NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
            List<ChildAssociationRef> children = nodeService.getChildAssocs(rootNodeRef,
                    RegexQNamePattern.MATCH_ALL,
                    QName.createQName(SYSTEM_FOLDER_SHORT_QNAME, namespacePrefixResolver), false);

            if (children.size() == 0) {
                throw new AlfrescoRuntimeException(
                        "Required people system path not found: " + SYSTEM_FOLDER_SHORT_QNAME);
            }

            NodeRef systemNodeRef = children.get(0).getChildRef();

            children = nodeService.getChildAssocs(systemNodeRef, RegexQNamePattern.MATCH_ALL,
                    QName.createQName(PEOPLE_FOLDER_SHORT_QNAME, namespacePrefixResolver), false);

            if (children.size() == 0) {
                throw new AlfrescoRuntimeException(
                        "Required people system path not found: " + PEOPLE_FOLDER_SHORT_QNAME);
            }

            peopleNodeRef = children.get(0).getChildRef();
            singletonCache.put(KEY_PEOPLECONTAINER_NODEREF, peopleNodeRef);
        }
        return peopleNodeRef;
    }

    /**
     * {@inheritDoc}
     */
    public void deletePerson(String userName) {
        // Normalize the username to avoid case sensitivity issues
        userName = getUserIdentifier(userName);
        if (userName == null) {
            return;
        }

        NodeRef personRef = getPersonOrNullImpl(userName);

        deletePersonAndAuthenticationImpl(userName, personRef);
    }

    /**
     * {@inheritDoc}
     */
    public void deletePerson(NodeRef personRef) {
        QName typeQName = nodeService.getType(personRef);
        if (typeQName.equals(ContentModel.TYPE_PERSON)) {
            String userName = (String) this.nodeService.getProperty(personRef, ContentModel.PROP_USERNAME);
            deletePersonAndAuthenticationImpl(userName, personRef);
        } else {
            throw new AlfrescoRuntimeException("deletePerson: invalid type of node " + personRef + " (actual="
                    + typeQName + ", expected=" + ContentModel.TYPE_PERSON + ")");
        }
    }

    /**
     * {@inheritDoc} 
     */
    public void deletePerson(NodeRef personRef, boolean deleteAuthentication) {
        QName typeQName = nodeService.getType(personRef);
        if (typeQName.equals(ContentModel.TYPE_PERSON)) {
            if (deleteAuthentication) {
                String userName = (String) this.nodeService.getProperty(personRef, ContentModel.PROP_USERNAME);
                deletePersonAndAuthenticationImpl(userName, personRef);
            } else {
                deletePersonImpl(personRef);
            }
        } else {
            throw new AlfrescoRuntimeException("deletePerson: invalid type of node " + personRef + " (actual="
                    + typeQName + ", expected=" + ContentModel.TYPE_PERSON + ")");
        }
    }

    private void deletePersonAndAuthenticationImpl(String userName, NodeRef personRef) {
        if (userName != null) {
            // Remove internally-stored password information, if any
            try {
                authenticationService.deleteAuthentication(userName);
            } catch (AuthenticationException e) {
                // Ignore this - externally authenticated user
            }

            // Invalidate all that user's tickets
            try {
                authenticationService.invalidateUserSession(userName);
            } catch (AuthenticationException e) {
                // Ignore this
            }

            // remove any user permissions
            permissionServiceSPI.deletePermissions(userName);
        }

        deletePersonImpl(personRef);
    }

    private void deletePersonImpl(NodeRef personRef) {
        // delete the person
        if (personRef != null) {
            try {
                beforeDeleteNodeValidationBehaviour.disable();

                nodeService.deleteNode(personRef);
            } finally {
                beforeDeleteNodeValidationBehaviour.enable();
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @deprecated see getPeople
     */
    public Set<NodeRef> getAllPeople() {
        List<PersonInfo> personInfos = getPeople(null, null, null, new PagingRequest(Integer.MAX_VALUE, null))
                .getPage();
        Set<NodeRef> refs = new HashSet<NodeRef>(personInfos.size());
        for (PersonInfo personInfo : personInfos) {
            refs.add(personInfo.getNodeRef());
        }
        return refs;
    }

    /**
     * {@inheritDoc}
     */
    public PagingResults<PersonInfo> getPeople(String pattern, List<QName> filterStringProps,
            List<Pair<QName, Boolean>> sortProps, PagingRequest pagingRequest) {
        return getPeople(pattern, filterStringProps, null, null, true, sortProps, pagingRequest);
    }

    /**
     * {@inheritDoc}
     */
    public PagingResults<PersonInfo> getPeople(String pattern, List<QName> filterStringProps,
            Set<QName> inclusiveAspects, Set<QName> exclusiveAspects, boolean includeAdministraotrs,
            List<Pair<QName, Boolean>> sortProps, PagingRequest pagingRequest) {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);

        CannedQueryResults<NodeRef> cqResults = null;

        NodeRef contextNodeRef = getPeopleContainer();

        // get canned query
        GetPeopleCannedQueryFactory getPeopleCannedQueryFactory = (GetPeopleCannedQueryFactory) cannedQueryRegistry
                .getNamedObject(CANNED_QUERY_PEOPLE_LIST);

        GetPeopleCannedQuery cq = (GetPeopleCannedQuery) getPeopleCannedQueryFactory.getCannedQuery(contextNodeRef,
                pattern, filterStringProps, inclusiveAspects, exclusiveAspects, includeAdministraotrs, sortProps,
                pagingRequest);

        // execute canned query
        cqResults = cq.execute();

        final CannedQueryResults<NodeRef> results = cqResults;

        boolean nonFinalHasMoreItems = results.hasMoreItems();
        List<NodeRef> nodeRefs;
        if (results.getPageCount() > 0) {
            nodeRefs = results.getPages().get(0);
            if (nodeRefs.size() > pagingRequest.getMaxItems()) {
                // eg. since hasMoreItems added one (for a pre-paged result)
                nodeRefs = nodeRefs.subList(0, pagingRequest.getMaxItems());
                nonFinalHasMoreItems = true;
            }
        } else {
            nodeRefs = Collections.emptyList();
        }

        final boolean hasMoreItems = nonFinalHasMoreItems;

        // set total count
        final Pair<Integer, Integer> totalCount;
        if (pagingRequest.getRequestTotalCountMax() > 0) {
            totalCount = results.getTotalResultCount();
        } else {
            totalCount = null;
        }

        if (start != null) {
            int cnt = nodeRefs.size();
            int skipCount = pagingRequest.getSkipCount();
            int maxItems = pagingRequest.getMaxItems();
            int pageNum = (skipCount / maxItems) + 1;

            if (logger.isDebugEnabled()) {
                logger.debug("getPeople: " + cnt + " items in " + (System.currentTimeMillis() - start) + " msecs "
                        + "[pageNum=" + pageNum + ",skip=" + skipCount + ",max=" + maxItems + ",hasMorePages="
                        + hasMoreItems + ",totalCount=" + totalCount + ",pattern=" + pattern + ",filterStringProps="
                        + filterStringProps + ",sortProps=" + sortProps + "]");
            }
        }

        final List<PersonInfo> personInfos = new ArrayList<PersonInfo>(nodeRefs.size());
        for (NodeRef nodeRef : nodeRefs) {
            if (nodeService.exists(nodeRef)) {
                Map<QName, Serializable> props = nodeService.getProperties(nodeRef);
                personInfos.add(new PersonInfo(nodeRef, (String) props.get(ContentModel.PROP_USERNAME),
                        (String) props.get(ContentModel.PROP_FIRSTNAME),
                        (String) props.get(ContentModel.PROP_LASTNAME)));
            }
        }

        return new PagingResults<PersonInfo>() {
            @Override
            public String getQueryExecutionId() {
                return results.getQueryExecutionId();
            }

            @Override
            public List<PersonInfo> getPage() {
                return personInfos;
            }

            @Override
            public boolean hasMoreItems() {
                return hasMoreItems;
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                return totalCount;
            }
        };
    }

    /**
     * {@inheritDoc}
     * 
     * @deprecated see getPeople(String pattern, List<QName> filterProps, List<Pair<QName, Boolean>> sortProps, PagingRequest pagingRequest)
     */
    public PagingResults<PersonInfo> getPeople(List<Pair<QName, String>> stringPropFilters,
            boolean filterIgnoreCase, List<Pair<QName, Boolean>> sortProps, PagingRequest pagingRequest) {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        if (stringPropFilters == null) {
            return getPeople(null, null, sortProps, pagingRequest);
        }

        String firstName = "";
        String lastName = "";
        String userName = "";
        for (Pair<QName, String> item : stringPropFilters) {
            if (ContentModel.PROP_FIRSTNAME.equals(item.getFirst())) {
                firstName = item.getSecond().trim();
            }
            if (ContentModel.PROP_LASTNAME.equals(item.getFirst())) {
                lastName = item.getSecond().trim();
            }
            if (ContentModel.PROP_USERNAME.equals(item.getFirst())) {
                userName = item.getSecond().trim();
            }
        }
        String searchStr = "";
        boolean useCQ = false;
        if (userName.length() == 0) {
            if (firstName.equalsIgnoreCase(lastName)) {
                searchStr = firstName;
                useCQ = true;
            } else {
                searchStr = firstName + " " + lastName;
            }
        } else {
            searchStr = userName;
            useCQ = searchStr.split(" ").length == 1;
        }

        PagingResults<PersonInfo> result = null;
        if (useCQ) {
            List<QName> filterProps = new ArrayList<QName>(3);
            filterProps.add(ContentModel.PROP_FIRSTNAME);
            filterProps.add(ContentModel.PROP_LASTNAME);
            filterProps.add(ContentModel.PROP_USERNAME);
            sortProps = sortProps == null ? new ArrayList<Pair<QName, Boolean>>(1)
                    : new ArrayList<Pair<QName, Boolean>>(sortProps);
            sortProps.add(new Pair<QName, Boolean>(ContentModel.PROP_USERNAME, true));
            result = getPeople(searchStr, filterProps, sortProps, pagingRequest);

            // Fall back to FTS if no results. For case:  First Name: Gerard, Last Name: Perez Winkler
            if (result.getPage().size() == 0) {
                result = null;
            }
        }

        if (result == null) {
            result = getPeopleFts(searchStr, pagingRequest);
        }

        return result;
    }

    /**
     * Get paged list of people optionally filtered and/or sorted using FTS
     * 
     * @param pattern - String to search
     * @param pagingRequest PagingRequest
     * @return PagingResults<PersonInfo>
     */
    private PagingResults<PersonInfo> getPeopleFts(String pattern, PagingRequest pagingRequest) {
        Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);
        List<NodeRef> people = null;
        try {
            people = getPeopleFtsList(pattern, pagingRequest);
        } catch (Throwable e1) {
            // search is failed
        }

        List<NodeRef> nodeRefs;
        boolean nonFinalHasMoreItems = false;
        if (people != null && people.size() > 0) {
            nodeRefs = people;
            if (nodeRefs.size() > pagingRequest.getMaxItems()) {
                // eg. since hasMoreItems added one (for a pre-paged result)
                nodeRefs = nodeRefs.subList(0, pagingRequest.getMaxItems());
                nonFinalHasMoreItems = true;
            }
        } else {
            nodeRefs = Collections.emptyList();
        }
        if (people == null || people.size() == 0) {
            nodeRefs = Collections.emptyList();
        }

        final boolean hasMoreItems = nonFinalHasMoreItems;

        // set total count
        final Pair<Integer, Integer> totalCount;
        if (pagingRequest.getRequestTotalCountMax() > 0) {
            int size = people != null ? people.size() : 0;
            totalCount = new Pair<Integer, Integer>(size, size);
        } else {
            totalCount = null;
        }

        if (start != null) {
            int cnt = nodeRefs.size();
            int skipCount = pagingRequest.getSkipCount();
            int maxItems = pagingRequest.getMaxItems();
            int pageNum = (skipCount / maxItems) + 1;

            if (logger.isDebugEnabled()) {
                logger.debug("getPeople: " + cnt + " items in " + (System.currentTimeMillis() - start) + " msecs "
                        + "[pageNum=" + pageNum + ",skip=" + skipCount + ",max=" + maxItems + ",hasMorePages="
                        + hasMoreItems + ",totalCount=" + totalCount + ",pattern=" + pattern + "]");
            }
        }

        final List<PersonInfo> personInfos = new ArrayList<PersonInfo>(nodeRefs.size());
        for (NodeRef nodeRef : nodeRefs) {
            try {
                Map<QName, Serializable> props = nodeService.getProperties(nodeRef);
                personInfos.add(new PersonInfo(nodeRef, (String) props.get(ContentModel.PROP_USERNAME),
                        (String) props.get(ContentModel.PROP_FIRSTNAME),
                        (String) props.get(ContentModel.PROP_LASTNAME)));
            } catch (InvalidNodeRefException e) {
                logger.warn("Stale search result", e);
            }
        }

        return new PagingResults<PersonInfo>() {
            @Override
            public String getQueryExecutionId() {
                // it's FTS search
                return null;
            }

            @Override
            public List<PersonInfo> getPage() {
                return personInfos;
            }

            @Override
            public boolean hasMoreItems() {
                return hasMoreItems;
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                return totalCount;
            }
        };
    }

    private List<NodeRef> getPeopleFtsList(String pattern, PagingRequest pagingRequest) throws Throwable {
        // Think this code is based on org.alfresco.repo.jscript.People.getPeopleImplSearch(String, StringTokenizer, int, int)
        List<NodeRef> people = null;

        SearchParameters params = new SearchParameters();
        params.addQueryTemplate("_PERSON", "|%firstName OR |%lastName OR |%userName");
        params.setDefaultFieldName("_PERSON");

        StringBuilder query = new StringBuilder(256);

        query.append("TYPE:\"").append(ContentModel.TYPE_PERSON).append("\" AND (");

        StringTokenizer t = new StringTokenizer(pattern, " ");

        if (t.countTokens() == 1) {
            // fts-alfresco property search i.e. location:"maidenhead"
            query.append('"').append(pattern).append("*\"");
        } else {
            // multiple terms supplied - look for first and second name etc.
            // assume first term is first name, any more are second i.e.
            // "Fraun van de Wiels"
            // also allow fts-alfresco property search to reduce results
            params.setDefaultOperator(SearchParameters.Operator.AND);
            StringBuilder multiPartNames = new StringBuilder(pattern.length());
            int numOfTokens = t.countTokens();
            int counter = 1;
            String term = null;
            // MNT-8539, in order to support firstname and lastname search
            while (t.hasMoreTokens()) {
                term = t.nextToken();
                // ALF-11311, in order to support multi-part
                // firstNames/lastNames, we need to use the whole tokenized term for both
                // firstName and lastName
                if (term.endsWith("*")) {
                    term = term.substring(0, term.lastIndexOf("*"));
                }
                multiPartNames.append("\"");
                multiPartNames.append(term);
                multiPartNames.append("*\"");
                if (numOfTokens > counter) {
                    multiPartNames.append(' ');
                }
                counter++;
            }
            // ALF-11311, in order to support multi-part firstNames/lastNames,
            // we need to use the whole tokenized term for both firstName and lastName.
            // e.g. "john junior lewis martinez", where "john junior" is the first
            // name and "lewis martinez" is the last name.
            if (multiPartNames.length() > 0) {
                query.append("firstName:");
                query.append(multiPartNames);
                query.append(" OR lastName:");
                query.append(multiPartNames);
            }
        }
        query.append(")");

        // define the search parameters
        params.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO);
        params.addStore(this.storeRef);
        params.setQuery(query.toString());
        if (pagingRequest.getMaxItems() > 0) {
            params.setLimitBy(LimitBy.FINAL_SIZE);
            params.setLimit(pagingRequest.getMaxItems());
        }

        ResultSet results = null;
        try {
            results = searchService.query(params);
            people = results.getNodeRefs();
        } catch (Throwable err) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to execute people search: " + query.toString(), err);
            }

            throw err;
        } finally {
            if (results != null) {
                results.close();
            }
        }

        return people;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<NodeRef> getPeopleFilteredByProperty(QName propertyKey, Serializable propertyValue, int count) {
        if (count > 1000) {
            throw new IllegalArgumentException(
                    "Only 1000 results are allowed but got a request for " + count + ". Use getPeople.");
        }

        // check that given property key is defined for content model type 'cm:person'
        // and throw exception if it isn't
        if (this.dictionaryService.getProperty(ContentModel.TYPE_PERSON, propertyKey) == null) {
            throw new AlfrescoRuntimeException(
                    "Property '" + propertyKey + "' is not defined " + "for content model type cm:person");
        }
        if (!propertyKey.equals(ContentModel.PROP_FIRSTNAME) && !propertyKey.equals(ContentModel.PROP_LASTNAME)
                && !propertyKey.equals(ContentModel.PROP_USERNAME)) {
            logger.warn("PersonService.getPeopleFilteredByProperty() is being called to find people by "
                    + propertyKey
                    + ". Only PROP_FIRSTNAME, PROP_LASTNAME, PROP_USERNAME are now used in the search, so fewer nodes may "
                    + "be returned than expected of there are more than " + count + " users in total.");
        }

        List<Pair<QName, String>> filterProps = new ArrayList<Pair<QName, String>>(1);
        filterProps.add(new Pair<QName, String>(propertyKey, (String) propertyValue));

        PagingRequest pagingRequest = new PagingRequest(count, null);
        List<PersonInfo> personInfos = getPeople(filterProps, true, null, pagingRequest).getPage();

        Set<NodeRef> refs = new HashSet<NodeRef>(personInfos.size());
        for (PersonInfo personInfo : personInfos) {
            NodeRef nodeRef = personInfo.getNodeRef();
            String value = (String) this.nodeService.getProperty(nodeRef, propertyKey);
            if (EqualsHelper.nullSafeEquals(value, propertyValue)) {
                refs.add(nodeRef);
            }
        }

        return refs;
    }

    // Policies

    /**
     * {@inheritDoc}
     */
    public void onCreateNode(ChildAssociationRef childAssocRef) {
        NodeRef personRef = childAssocRef.getChildRef();

        String userName = (String) this.nodeService.getProperty(personRef, ContentModel.PROP_USERNAME);

        if (getPeopleContainer().equals(childAssocRef.getParentRef())) {
            // The value is stale.  However, we have already made the data change and
            // therefore do not need to lock the removal from further changes.
            removeFromCache(userName, false);
        }

        permissionsManager.setPermissions(personRef, userName, userName);

        // Make sure there is an authority entry - with a DB constraint for uniqueness
        // aclDao.createAuthority(username);

        if ((homeFolderCreationEager) && (homeFolderCreationDisabled == false)) {
            makeHomeFolderAsSystem(childAssocRef);
        }

    }

    private QName getChildNameLower(String userName) {
        return QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, userName.toLowerCase(),
                namespacePrefixResolver);
    }

    public void beforeCreateNode(NodeRef parentRef, QName assocTypeQName, QName assocQName, QName nodeTypeQName) {
        // NOOP
    }

    public void beforeCreateNodeValidation(NodeRef parentRef, QName assocTypeQName, QName assocQName,
            QName nodeTypeQName) {
        if (getPeopleContainer().equals(parentRef)) {
            throw new AlfrescoRuntimeException("beforeCreateNode: use PersonService to create person");
        } else {
            logger.info("Person node is not being created under the people container (actual=" + parentRef
                    + ", expected=" + getPeopleContainer() + ")");
        }
    }

    public void beforeDeleteNode(NodeRef nodeRef) {
        String userName = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME);
        if (this.authorityService.isGuestAuthority(userName) && !this.tenantService.isTenantUser(userName)) {
            throw new AlfrescoRuntimeException("The " + userName + " user cannot be deleted.");
        }

        NodeRef parentRef = null;
        ChildAssociationRef parentAssocRef = nodeService.getPrimaryParent(nodeRef);
        if (parentAssocRef != null) {
            parentRef = parentAssocRef.getParentRef();
            if (getPeopleContainer().equals(parentRef)) {
                // Remove the cache entry.
                // Note that the associated node has not been deleted and is therefore still
                // visible to any other code that attempts to see it.  We therefore need to
                // prevent the value from being added back before the node is actually
                // deleted.
                removeFromCache(userName, true);
            }
        }
    }

    public void beforeDeleteNodeValidation(NodeRef nodeRef) {
        NodeRef parentRef = null;
        ChildAssociationRef parentAssocRef = nodeService.getPrimaryParent(nodeRef);
        if (parentAssocRef != null) {
            parentRef = parentAssocRef.getParentRef();
        }

        if (getPeopleContainer().equals(parentRef)) {
            throw new AlfrescoRuntimeException("beforeDeleteNode: use PersonService to delete person");
        } else {
            logger.info("Person node that is being deleted is not under the parent people container (actual="
                    + parentRef + ", expected=" + getPeopleContainer() + ")");
        }
    }

    private Set<NodeRef> getFromCache(String userName) {
        return this.personCache.get(userName.toLowerCase());
    }

    /**
     * Put a value into the {@link #setPersonCache(SimpleCache) personCache}, optionally
     * locking the value against any changes.
     */
    private void putToCache(String userName, Set<NodeRef> refs, boolean lock) {
        String key = userName.toLowerCase();
        this.personCache.put(key, refs);
        if (lock && personCache instanceof TransactionalCache) {
            TransactionalCache<String, Set<NodeRef>> personCacheTxn = (TransactionalCache<String, Set<NodeRef>>) personCache;
            personCacheTxn.lockValue(key);
        }
    }

    /**
     * Remove a value from the {@link #setPersonCache(SimpleCache) personCache}, optionally
     * locking the value against any changes.
     */
    private void removeFromCache(String userName, boolean lock) {
        String key = userName.toLowerCase();
        personCache.remove(key);
        if (lock && personCache instanceof TransactionalCache) {
            TransactionalCache<String, Set<NodeRef>> personCacheTxn = (TransactionalCache<String, Set<NodeRef>>) personCache;
            personCacheTxn.lockValue(key);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getUserIdentifier(String caseSensitiveUserName) {
        NodeRef nodeRef = getPersonOrNullImpl(caseSensitiveUserName);
        if ((nodeRef != null) && nodeService.exists(nodeRef)) {
            String realUserName = DefaultTypeConverter.INSTANCE.convert(String.class,
                    nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME));
            return realUserName;
        }
        return null;
    }

    public static class NodeIdComparator implements Comparator<NodeRef> {
        private NodeService nodeService;

        boolean ascending;

        NodeIdComparator(NodeService nodeService, boolean ascending) {
            this.nodeService = nodeService;
            this.ascending = ascending;
        }

        public int compare(NodeRef first, NodeRef second) {
            Long firstId = DefaultTypeConverter.INSTANCE.convert(Long.class,
                    nodeService.getProperty(first, ContentModel.PROP_NODE_DBID));
            Long secondId = DefaultTypeConverter.INSTANCE.convert(Long.class,
                    nodeService.getProperty(second, ContentModel.PROP_NODE_DBID));

            if (firstId != null) {
                if (secondId != null) {
                    return firstId.compareTo(secondId) * (ascending ? 1 : -1);
                } else {
                    return ascending ? -1 : 1;
                }
            } else {
                if (secondId != null) {
                    return ascending ? 1 : -1;
                } else {
                    return 0;
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean getUserNamesAreCaseSensitive() {
        return userNameMatcher.getUserNamesAreCaseSensitive();
    }

    /**
     * When a uid is changed we need to create an alias for the old uid so permissions are not broken. This can happen
     * when an already existing user is updated via LDAP e.g. migration to LDAP, or when a user is auto created and then
     * updated by LDAP This is probably less likely after 3.2 and sync on missing person See
     * https://issues.alfresco.com/jira/browse/ETWOTWO-389 (non-Javadoc)
     */
    public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before,
            Map<QName, Serializable> after) {
        String uidBefore = DefaultTypeConverter.INSTANCE.convert(String.class,
                before.get(ContentModel.PROP_USERNAME));
        if (uidBefore == null) {
            // Node has just been created; nothing to do
            return;
        }
        String uidAfter = DefaultTypeConverter.INSTANCE.convert(String.class,
                after.get(ContentModel.PROP_USERNAME));
        if (!EqualsHelper.nullSafeEquals(uidBefore, uidAfter)) {
            // Only allow UID update if we are in the special split processing txn or we are just changing case
            if (AlfrescoTransactionSupport.getResource(KEY_ALLOW_UID_UPDATE) != null
                    || uidBefore.equalsIgnoreCase(uidAfter)) {
                if (uidBefore != null) {
                    // Fix any ACLs
                    aclDao.renameAuthority(uidBefore, uidAfter);
                }

                // Fix primary association local name
                QName newAssocQName = getChildNameLower(uidAfter);
                ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef);
                nodeService.moveNode(nodeRef, assoc.getParentRef(), assoc.getTypeQName(), newAssocQName);

                // Fix other non-case sensitive parent associations
                QName oldAssocQName = QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, uidBefore,
                        namespacePrefixResolver);
                newAssocQName = QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, uidAfter,
                        namespacePrefixResolver);
                for (ChildAssociationRef parent : nodeService.getParentAssocs(nodeRef)) {
                    if (!parent.isPrimary() && parent.getQName().equals(oldAssocQName)) {
                        nodeService.removeChildAssociation(parent);
                        nodeService.addChild(parent.getParentRef(), parent.getChildRef(), parent.getTypeQName(),
                                newAssocQName);
                    }
                }

                // Fix cache
                // We are going to be pessimistic here.  Even though the properties have changed and
                // should always be seen correctly by other policy listeners, we are not entirely sure
                // that there won't be some sort of corruption i.e. the behaviour was pessimistic before
                // this change so I'm leaving it that way.
                removeFromCache(uidBefore, true);
            } else {
                throw new UnsupportedOperationException("The user name on a person can not be changed");
            }
        }

        if (validUserUpdateEvent(before, after)) {
            publishEvent("user.update", after);
        }

    }

    /**
     * Determine if the updated properties constitute a valid user update.
     * Currently we only check for updates to the user firstname, lastname
     * 
     * @param before Map<QName, Serializable>
     * @param after Map<QName, Serializable>
     * @return boolean
     */
    private boolean validUserUpdateEvent(Map<QName, Serializable> before, Map<QName, Serializable> after) {
        final String firstnameBefore = (String) before.get(ContentModel.PROP_FIRSTNAME);
        final String lastnameBefore = (String) before.get(ContentModel.PROP_LASTNAME);
        final String firstnameAfter = (String) after.get(ContentModel.PROP_FIRSTNAME);
        final String lastnameAfter = (String) after.get(ContentModel.PROP_LASTNAME);

        boolean updatedFirstName = !EqualsHelper.nullSafeEquals(firstnameBefore, firstnameAfter);
        boolean updatedLastName = !EqualsHelper.nullSafeEquals(lastnameBefore, lastnameAfter);

        return updatedFirstName || updatedLastName;
    }

    /**
     * Publish new user event
     * 
     * @param eventType String
     * @param properties Map<QName, Serializable>
     */
    private void publishEvent(String eventType, Map<QName, Serializable> properties) {
        if (properties == null)
            return;

        final String managedUsername = (String) properties.get(ContentModel.PROP_USERNAME);
        final String managedFirstname = (String) properties.get(ContentModel.PROP_FIRSTNAME);
        final String managedLastname = (String) properties.get(ContentModel.PROP_LASTNAME);
        final String eventTType = eventType;

        eventPublisher.publishEvent(new EventPreparator() {
            @Override
            public Event prepareEvent(String user, String networkId, String transactionId) {
                return new UserManagementEvent(eventTType, transactionId, networkId, new Date().getTime(), user,
                        managedUsername, managedFirstname, managedLastname);
            }
        });
    }

    /**
     * Track the {@link ContentModel#PROP_ENABLED enabled/disabled} flag on {@link ContentModel#TYPE_USER <b>cm:user</b>}.
     */
    public void onUpdatePropertiesUser(NodeRef nodeRef, Map<QName, Serializable> before,
            Map<QName, Serializable> after) {
        String userName = (String) after.get(ContentModel.PROP_USER_USERNAME);
        if (userName == null) {
            // Won't find user
            return;
        }
        // Get the person
        NodeRef personNodeRef = getPersonOrNullImpl(userName);
        if (personNodeRef == null) {
            // Don't attempt to maintain enabled/disabled flag
            return;
        }

        // Check the enabled/disabled flag
        Boolean enabled = (Boolean) after.get(ContentModel.PROP_ENABLED);
        if (enabled == null || enabled.booleanValue()) {
            nodeService.removeAspect(personNodeRef, ContentModel.ASPECT_PERSON_DISABLED);
        } else {
            nodeService.addAspect(personNodeRef, ContentModel.ASPECT_PERSON_DISABLED, null);
        }

        // Do post-commit user counting, if required
        Set<String> usersCreated = TransactionalResourceHelper.getSet(KEY_USERS_CREATED);
        usersCreated.add(userName);
        AlfrescoTransactionSupport.bindListener(this);
    }

    public int countPeople() {
        NodeRef peopleContainer = getPeopleContainer();
        return nodeService.countChildAssocs(peopleContainer, true);
    }

    /**
     * Helper for when creating new users and people:
     * Updates the supplied username with any required tenant
     *  details, and ensures that the tenant domains match.
     * If Multi-Tenant is disabled, returns the same username.
     */
    public static String updateUsernameForTenancy(String username, TenantService tenantService)
            throws TenantDomainMismatchException {
        if (!tenantService.isEnabled()) {
            // Nothing to do if not using multi tenant
            return username;
        }

        String currentDomain = tenantService.getCurrentUserDomain();
        if (!currentDomain.equals(TenantService.DEFAULT_DOMAIN)) {
            if (!tenantService.isTenantUser(username)) {
                // force domain onto the end of the username
                username = tenantService.getDomainUser(username, currentDomain);
                logger.warn("Added domain to username: " + username);
            } else {
                // Check the user's domain matches the current domain
                // Throws a TenantDomainMismatchException if they don't match
                tenantService.checkDomainUser(username);
            }
        }
        return username;
    }

    @Override
    public boolean isEnabled(String userName) {
        NodeRef noderef = getPerson(userName, false);

        for (QName aspectName : nodeService.getAspects(noderef)) {
            if (ContentModel.ASPECT_PERSON_DISABLED.isMatch(aspectName)) {
                return false;
            }
        }

        return true;
    }

    public void setEventPublisher(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

}