org.alfresco.repo.security.authority.AuthorityDAOImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.security.authority.AuthorityDAOImpl.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.authority;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.ibatis.IdsEntity;
import org.alfresco.model.ContentModel;
import org.alfresco.query.CannedQuery;
import org.alfresco.query.CannedQueryFactory;
import org.alfresco.query.CannedQueryResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.cache.AsynchronouslyRefreshedCache;
import org.alfresco.util.cache.RefreshableCacheEvent;
import org.alfresco.util.cache.RefreshableCacheListener;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.TransactionalCache;
import org.alfresco.repo.domain.permissions.AclDAO;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.domain.query.CannedQueryDAO;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.search.impl.lucene.AbstractLuceneQueryParser;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.person.PersonServiceImpl;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.ResultSetRow;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService.AuthorityFilter;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.NoSuchPersonException;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.security.PersonService.PersonInfo;
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.util.BridgeTable;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.ISO9075;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.SearchLanguageConversion;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.BeforeDeleteNodePolicy,
        NodeServicePolicies.OnUpdatePropertiesPolicy, RefreshableCacheListener, InitializingBean {
    private static Log logger = LogFactory.getLog(AuthorityDAOImpl.class);

    private static String PARENTS_OF_DELETING_CHILDREN_SET_RESOURCE = "ParentsOfDeletingChildrenSetResource";

    private static final NodeRef NULL_NODEREF = new NodeRef("null", "null", "null");
    private static final String CANNED_QUERY_AUTHS_LIST = "authsGetAuthoritiesCannedQueryFactory"; // see authority-services-context.xml
    private static final Collection<AuthorityType> SEARCHABLE_AUTHORITY_TYPES = new LinkedList<AuthorityType>();
    static {
        SEARCHABLE_AUTHORITY_TYPES.add(AuthorityType.ROLE);
        SEARCHABLE_AUTHORITY_TYPES.add(AuthorityType.GROUP);
    }

    private StoreRef storeRef;
    private NodeService nodeService;
    private NamespacePrefixResolver namespacePrefixResolver;
    private QName qnameAssocSystem;
    private QName qnameAssocAuthorities;
    private QName qnameAssocZones;
    private SearchService searchService;
    private DictionaryService dictionaryService;
    private PersonService personService;
    private TenantService tenantService;

    private SimpleCache<Pair<String, String>, NodeRef> authorityLookupCache;
    private SimpleCache<String, Set<String>> userAuthorityCache;
    private SimpleCache<Pair<String, String>, List<ChildAssociationRef>> zoneAuthorityCache;
    private SimpleCache<NodeRef, Pair<Map<NodeRef, String>, List<NodeRef>>> childAuthorityCache;
    private AuthorityBridgeTableAsynchronouslyRefreshedCache authorityBridgeTableCache;
    private SimpleCache<String, Object> singletonCache; // eg. for system container nodeRefs (authorityContainer and zoneContainer)
    private final String KEY_SYSTEMCONTAINER_NODEREF = "key.systemcontainer.noderef";
    /** Limit the number of copies of authority names floating about by keeping them in a pool **/
    private ConcurrentMap<String, String> authorityNamePool = new ConcurrentHashMap<String, String>();

    /** The number of authorities in a zone to pre-cache, allowing quick generation of 'first n' results. */
    private int zoneAuthoritySampleSize = 10000;

    private boolean useBridgeTable = true;

    private QNameDAO qnameDAO;
    private CannedQueryDAO cannedQueryDAO;
    private AclDAO aclDao;
    private PolicyComponent policyComponent;
    private NamedObjectRegistry<CannedQueryFactory<?>> cannedQueryRegistry;
    private AuthorityBridgeDAO authorityBridgeDAO;

    public AuthorityDAOImpl() {
        super();
    }

    /**
     * Sets number of authorities in a zone to pre-cache, allowing quick generation of 'first n' results and adaption of
     * search technique based on hit rate.
     * 
     * @param zoneAuthoritySampleSize
     *            the zoneAuthoritySampleSize to set
     */
    public void setZoneAuthoritySampleSize(int zoneAuthoritySampleSize) {
        this.zoneAuthoritySampleSize = zoneAuthoritySampleSize;
    }

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

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

    public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) {
        this.namespacePrefixResolver = namespacePrefixResolver;
        qnameAssocSystem = QName.createQName("sys", "system", namespacePrefixResolver);
        qnameAssocAuthorities = QName.createQName("sys", "authorities", namespacePrefixResolver);
        qnameAssocZones = QName.createQName("sys", "zones", namespacePrefixResolver);
    }

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

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

    public void setAuthorityLookupCache(SimpleCache<Pair<String, String>, NodeRef> authorityLookupCache) {
        this.authorityLookupCache = authorityLookupCache;
    }

    public void setUserAuthorityCache(SimpleCache<String, Set<String>> userAuthorityCache) {
        this.userAuthorityCache = userAuthorityCache;
    }

    public void setZoneAuthorityCache(
            SimpleCache<Pair<String, String>, List<ChildAssociationRef>> zoneAuthorityCache) {
        this.zoneAuthorityCache = zoneAuthorityCache;
    }

    public void setChildAuthorityCache(
            SimpleCache<NodeRef, Pair<Map<NodeRef, String>, List<NodeRef>>> childAuthorityCache) {
        this.childAuthorityCache = childAuthorityCache;
    }

    public void setAuthorityBridgeTableCache(
            AuthorityBridgeTableAsynchronouslyRefreshedCache authorityBridgeTableCache) {
        this.authorityBridgeTableCache = authorityBridgeTableCache;
    }

    /**
     * @param useBridgeTable the useBridgeTable to set
     */
    public void setUseBridgeTable(boolean useBridgeTable) {
        this.useBridgeTable = useBridgeTable;
    }

    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }

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

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

    public void setQnameDAO(QNameDAO qnameDAO) {
        this.qnameDAO = qnameDAO;
    }

    public void setCannedQueryDAO(CannedQueryDAO cannedQueryDAO) {
        this.cannedQueryDAO = cannedQueryDAO;
    }

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

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

    public void setCannedQueryRegistry(NamedObjectRegistry<CannedQueryFactory<?>> cannedQueryRegistry) {
        this.cannedQueryRegistry = cannedQueryRegistry;
    }

    /**
     * @param authorityBridgeDAO the authorityBridgeDAO to set
     */
    public void setAuthorityBridgeDAO(AuthorityBridgeDAO authorityBridgeDAO) {
        this.authorityBridgeDAO = authorityBridgeDAO;
    }

    @Override
    public long getPersonCount() {
        /* Unboxing accepted.  See CannedQueryDAO javadoc and implementation. */
        Pair<Long, QName> qnamePair = qnameDAO.getQName(ContentModel.TYPE_PERSON);
        if (qnamePair == null) {
            // No results
            return 0L;
        }

        IdsEntity ids = new IdsEntity();
        ids.setIdOne(qnamePair.getFirst());
        Long personCount = cannedQueryDAO.executeCountQuery("alfresco.query.authorities",
                "select_AuthorityCount_People", ids);
        if (logger.isDebugEnabled()) {
            logger.debug("Counted authorities (people): " + personCount);
        }
        return personCount;
    }

    @Override
    public long getGroupCount() {
        /* Unboxing accepted.  See CannedQueryDAO javadoc and implementation. */
        Pair<Long, QName> qnamePair = qnameDAO.getQName(ContentModel.TYPE_AUTHORITY_CONTAINER);
        if (qnamePair == null) {
            // No results
            return 0L;
        }

        IdsEntity ids = new IdsEntity();
        ids.setIdOne(qnamePair.getFirst());
        Long groupCount = cannedQueryDAO.executeCountQuery("alfresco.query.authorities",
                "select_AuthorityCount_Groups", ids);
        if (logger.isDebugEnabled()) {
            logger.debug("Counted authorities (groups):" + groupCount);
        }
        return groupCount;
    }

    public boolean authorityExists(String name) {
        NodeRef ref = getAuthorityOrNull(name);
        return ref != null;
    }

    public void addAuthority(Collection<String> parentNames, String childName) {
        Set<NodeRef> parentRefs = new HashSet<NodeRef>(parentNames.size() * 2);
        AuthorityType authorityType = AuthorityType.getAuthorityType(childName);
        boolean isUser = authorityType.equals(AuthorityType.USER);
        boolean notUserOrGroup = !isUser && !authorityType.equals(AuthorityType.GROUP);
        for (String parentName : parentNames) {
            NodeRef parentRef = getAuthorityOrNull(parentName);
            if (parentRef == null) {
                throw new UnknownAuthorityException("An authority was not found for " + parentName);
            }
            if (notUserOrGroup && !(authorityType.equals(AuthorityType.ROLE)
                    && AuthorityType.getAuthorityType(parentName).equals(AuthorityType.ROLE))) {
                throw new AlfrescoRuntimeException(
                        "Authorities of the type " + authorityType + " may not be added to other authorities");
            }
            childAuthorityCache.remove(parentRef);
            parentRefs.add(parentRef);
        }
        NodeRef childRef = getAuthorityOrNull(childName);

        if (childRef == null) {
            throw new UnknownAuthorityException("An authority was not found for " + childName);
        }

        // Normalize the user name if necessary
        if (isUser) {
            childName = (String) nodeService.getProperty(childRef, ContentModel.PROP_USERNAME);
        }

        nodeService.addChild(parentRefs, childRef, ContentModel.ASSOC_MEMBER,
                QName.createQName("cm", childName, namespacePrefixResolver));
        if (isUser) {
            userAuthorityCache.remove(childName);
        } else {
            userAuthorityCache.clear();
            authorityBridgeTableCache.refresh();
        }
    }

    public void createAuthority(String name, String authorityDisplayName, Set<String> authorityZones) {
        HashMap<QName, Serializable> props = new HashMap<QName, Serializable>();
        /* MNT-11749 : Alfresco allows to create authorities with different char cases, but disallow duplicates */
        props.put(ContentModel.PROP_NAME, DigestUtils.md5Hex(name));
        props.put(ContentModel.PROP_AUTHORITY_NAME, name);
        props.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, authorityDisplayName);
        NodeRef childRef;
        NodeRef authorityContainerRef = getAuthorityContainer();
        childRef = nodeService.createNode(authorityContainerRef, ContentModel.ASSOC_CHILDREN,
                QName.createQName("cm", name, namespacePrefixResolver), ContentModel.TYPE_AUTHORITY_CONTAINER,
                props).getChildRef();
        if (authorityZones != null) {
            Set<NodeRef> zoneRefs = new HashSet<NodeRef>(authorityZones.size() * 2);
            String currentUserDomain = tenantService.getCurrentUserDomain();
            for (String authorityZone : authorityZones) {
                zoneRefs.add(getOrCreateZone(authorityZone));
                zoneAuthorityCache.remove(new Pair<String, String>(currentUserDomain, authorityZone));
            }
            zoneAuthorityCache.remove(new Pair<String, String>(currentUserDomain, null));
            nodeService.addChild(zoneRefs, childRef, ContentModel.ASSOC_IN_ZONE,
                    QName.createQName("cm", name, namespacePrefixResolver));
        }
        authorityLookupCache.put(cacheKey(name), childRef);
    }

    private Pair<String, String> cacheKey(String authorityName) {
        String tenantDomain = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER
                ? tenantService.getDomain(authorityName)
                : tenantService.getCurrentUserDomain();
        return new Pair<String, String>(tenantDomain, getPooledName(authorityName));
    }

    private String getPooledName(String authorityName) {
        String pooledName = authorityNamePool.putIfAbsent(authorityName, authorityName);
        return pooledName == null ? authorityName : pooledName;
    }

    public void deleteAuthority(String name) {
        NodeRef nodeRef = getAuthorityOrNull(name);
        if (nodeRef == null) {
            throw new UnknownAuthorityException("An authority was not found for " + name);
        }
        String currentUserDomain = tenantService.getCurrentUserDomain();
        for (String authorityZone : getAuthorityZones(name)) {
            zoneAuthorityCache.remove(new Pair<String, String>(currentUserDomain, authorityZone));
        }
        zoneAuthorityCache.remove(new Pair<String, String>(currentUserDomain, null));
        removeParentsFromChildAuthorityCache(nodeRef, false);

        nodeService.deleteNode(nodeRef);

        authorityLookupCache.remove(cacheKey(name));
        userAuthorityCache.clear();
        authorityBridgeTableCache.refresh();
    }

    public PagingResults<AuthorityInfo> getAuthoritiesInfo(AuthorityType type, String zoneName,
            String displayNameFilter, String sortBy, boolean sortAscending, PagingRequest pagingRequest) {
        checkGetAuthorityParams(type, zoneName, pagingRequest);

        return getAuthoritiesImpl(type, getContainerRef(zoneName), displayNameFilter, sortBy, sortAscending,
                pagingRequest, new PagingResultsAuthorityInfo());
    }

    public PagingResults<String> getAuthorities(AuthorityType type, String zoneName, String displayNameFilter,
            boolean sortByDisplayName, boolean sortAscending, PagingRequest pagingRequest) {
        checkGetAuthorityParams(type, zoneName, pagingRequest);

        if ((zoneName == null) && (type.equals(AuthorityType.USER))) {
            return getUserAuthoritiesImpl(displayNameFilter, sortByDisplayName, sortAscending, pagingRequest);
        }

        return getAuthoritiesImpl(type, getContainerRef(zoneName), displayNameFilter,
                (sortByDisplayName ? GetAuthoritiesCannedQuery.DISPLAY_NAME : null), sortAscending, pagingRequest,
                new PagingResultsString());
    }

    private void checkGetAuthorityParams(AuthorityType type, String zoneName, PagingRequest pagingRequest) {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        if ((type == null) && (zoneName == null)) {
            throw new IllegalArgumentException("Type and/or zoneName required - both cannot be null");
        }
    }

    private NodeRef getContainerRef(String zoneName) {
        NodeRef containerRef = null;
        if (zoneName != null) {
            containerRef = getZone(zoneName);
            if (containerRef == null) {
                throw new UnknownAuthorityException("A zone was not found for " + zoneName);
            }
        } else {
            containerRef = getAuthorityContainer();
        }
        return containerRef;
    }

    private <T> PagingResults<T> getAuthoritiesImpl(AuthorityType type, NodeRef containerRef,
            String displayNameFilter, String sortBy, boolean sortAscending, PagingRequest pagingRequest,
            AbstractPagingResults<T> finalResults) {
        Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);

        if (type != null) {
            switch (type) {
            case GROUP:
            case ROLE:
            case USER:
                // drop through
                break;
            default:
                throw new UnsupportedOperationException("Unexpected authority type: " + type);
            }
        }

        // get canned query
        GetAuthoritiesCannedQueryFactory getAuthoritiesCannedQueryFactory = (GetAuthoritiesCannedQueryFactory) cannedQueryRegistry
                .getNamedObject(CANNED_QUERY_AUTHS_LIST);
        CannedQuery<AuthorityInfo> cq = getAuthoritiesCannedQueryFactory.getCannedQuery(type, containerRef,
                displayNameFilter, sortBy, sortAscending, pagingRequest);

        // execute canned query
        final CannedQueryResults<AuthorityInfo> results = cq.execute();

        finalResults.setResults(results);

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

            logger.debug("getAuthoritiesByType: " + cnt + " items in " + (System.currentTimeMillis() - start)
                    + " msecs [type=" + type + ",pageNum=" + pageNum + ",skip=" + skipCount + ",max=" + maxItems
                    + ",hasMorePages=" + hasMoreItems + ",filter=" + displayNameFilter + "]");
        }

        return finalResults;
    }

    // delegate to PersonService.getPeople
    private PagingResults<String> getUserAuthoritiesImpl(String displayNameFilter, boolean sortByDisplayName,
            boolean sortAscending, PagingRequest pagingRequest) {
        List<Pair<QName, String>> filter = null;
        if (displayNameFilter != null) {
            filter = new ArrayList<Pair<QName, String>>();
            filter.add(new Pair<QName, String>(ContentModel.PROP_USERNAME, displayNameFilter));
        }

        List<Pair<QName, Boolean>> sort = null;
        if (sortByDisplayName) {
            sort = new ArrayList<Pair<QName, Boolean>>();
            sort.add(new Pair<QName, Boolean>(ContentModel.PROP_USERNAME, sortAscending));
        }

        final PagingResults<PersonInfo> ppr = personService.getPeople(filter, true, sort, pagingRequest);

        List<PersonInfo> result = ppr.getPage();
        final List<String> auths = new ArrayList<String>(result.size());

        for (PersonInfo person : result) {
            auths.add(getPooledName(person.getUserName()));
        }

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

            @Override
            public List<String> getPage() {
                return auths;
            }

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

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

    public Set<String> getRootAuthorities(AuthorityType type, String zoneName) {
        NodeRef container = (zoneName == null ? getAuthorityContainer() : getZone(zoneName));
        if (container == null) {
            // The zone doesn't even exist so there are no root authorities
            return Collections.emptySet();
        }

        return getRootAuthoritiesUnderContainer(container, type);
    }

    public Set<String> findAuthorities(AuthorityType type, String parentAuthority, boolean immediate,
            String displayNamePattern, String zoneName) {
        Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);

        Pattern pattern = displayNamePattern == null ? null
                : Pattern.compile(SearchLanguageConversion.convert(SearchLanguageConversion.DEF_LUCENE,
                        SearchLanguageConversion.DEF_REGEX, displayNamePattern), Pattern.CASE_INSENSITIVE);

        // Use SQL to determine root authorities
        Set<String> rootAuthorities = null;
        if (parentAuthority == null && immediate) {
            rootAuthorities = getRootAuthorities(type, zoneName);
            if (pattern == null) {
                if (start != null) {
                    logger.debug("findAuthorities (rootAuthories): " + rootAuthorities.size() + " items in "
                            + (System.currentTimeMillis() - start) + " msecs [type=" + type + ",zone=" + zoneName
                            + "]");
                }

                return rootAuthorities;
            }
        }

        // Use a Lucene search for other criteria
        Set<String> authorities = new TreeSet<String>();
        SearchParameters sp = new SearchParameters();
        sp.addStore(this.storeRef);
        sp.setLanguage("lucene");
        StringBuilder query = new StringBuilder(500);
        if (type == null || type == AuthorityType.USER) {
            if (type == null) {
                query.append("((");
            }
            query.append("TYPE:\"").append(ContentModel.TYPE_PERSON).append("\"");
            if (displayNamePattern != null) {
                query.append(" AND @")
                        .append(SearchLanguageConversion
                                .escapeLuceneQuery("{" + ContentModel.PROP_USERNAME.getNamespaceURI() + "}"
                                        + ISO9075.encode(ContentModel.PROP_USERNAME.getLocalName())))
                        .append(":\"").append(SearchLanguageConversion.escapeLuceneQuery(displayNamePattern))
                        .append("\"");

            }
            if (type == null) {
                query.append(") OR (");
            }
        }
        if (type != AuthorityType.USER) {
            query.append("TYPE:\"").append(ContentModel.TYPE_AUTHORITY_CONTAINER).append("\"");
            if (displayNamePattern != null) {
                query.append(" AND (");
                if (!displayNamePattern.startsWith("*")) {
                    // Allow for the appropriate type prefix in the authority name
                    Collection<AuthorityType> authorityTypes = type == null ? SEARCHABLE_AUTHORITY_TYPES
                            : Collections.singleton(type);
                    boolean first = true;
                    for (AuthorityType subType : authorityTypes) {
                        if (first) {
                            first = false;
                        } else {
                            query.append(" OR ");
                        }
                        query.append("@")
                                .append(SearchLanguageConversion.escapeLuceneQuery(
                                        "{" + ContentModel.PROP_AUTHORITY_NAME.getNamespaceURI() + "}"
                                                + ISO9075.encode(ContentModel.PROP_AUTHORITY_NAME.getLocalName())))
                                .append(":\"");
                        query.append(
                                getName(subType, SearchLanguageConversion.escapeLuceneQuery(displayNamePattern)))
                                .append("\"");

                    }
                } else {
                    query.append("@")
                            .append(SearchLanguageConversion.escapeLuceneQuery(
                                    "{" + ContentModel.PROP_AUTHORITY_NAME.getNamespaceURI() + "}"
                                            + ISO9075.encode(ContentModel.PROP_AUTHORITY_NAME.getLocalName())))
                            .append(":\"");
                    query.append(getName(type, SearchLanguageConversion.escapeLuceneQuery(displayNamePattern)))
                            .append("\"");
                }
                query.append(" OR @")
                        .append(SearchLanguageConversion.escapeLuceneQuery(
                                "{" + ContentModel.PROP_AUTHORITY_DISPLAY_NAME.getNamespaceURI() + "}"
                                        + ISO9075.encode(ContentModel.PROP_AUTHORITY_DISPLAY_NAME.getLocalName())))
                        .append(":\"").append(SearchLanguageConversion.escapeLuceneQuery(displayNamePattern))
                        .append("\")");
            }
            if (type == null) {
                query.append("))");
            }
        }
        if (parentAuthority != null) {
            if (immediate) {
                // use PARENT
                NodeRef parentAuthorityNodeRef = getAuthorityNodeRefOrNull(parentAuthority);
                if (parentAuthorityNodeRef != null) {
                    query.append(" AND PARENT:\"")
                            .append(SearchLanguageConversion.escapeLuceneQuery(parentAuthorityNodeRef.toString()))
                            .append("\"");
                } else {
                    throw new UnknownAuthorityException("An authority was not found for " + parentAuthority);
                }
            } else {
                // use PATH
                query.append(" AND PATH:\"/sys:system/sys:authorities/cm:").append(ISO9075.encode(parentAuthority));
                query.append("//*\"");
            }
        }
        if (zoneName != null) {
            // Zones are all direct links to those within so it is safe to use PARENT to look them up
            NodeRef zoneNodeRef = getZone(zoneName);
            if (zoneNodeRef != null) {
                query.append(" AND PARENT:\"")
                        .append(SearchLanguageConversion.escapeLuceneQuery(zoneNodeRef.toString())).append("\"");
            } else {
                throw new UnknownAuthorityException("A zone was not found for " + zoneName);
            }
        }
        sp.setQuery(query.toString());
        sp.setMaxItems(100);
        ResultSet rs = null;
        try {
            rs = searchService.query(sp);

            for (ResultSetRow row : rs) {
                NodeRef nodeRef = row.getNodeRef();
                QName idProp = dictionaryService.isSubClass(nodeService.getType(nodeRef),
                        ContentModel.TYPE_AUTHORITY_CONTAINER) ? ContentModel.PROP_AUTHORITY_NAME
                                : ContentModel.PROP_USERNAME;
                addAuthorityNameIfMatches(authorities, DefaultTypeConverter.INSTANCE.convert(String.class,
                        nodeService.getProperty(nodeRef, idProp)), type, pattern);
            }

            // If we asked for root authorities, we must do an intersection with the set of root authorities
            if (rootAuthorities != null) {
                authorities.retainAll(rootAuthorities);
            }

            if (start != null) {
                logger.debug("findAuthorities: " + authorities.size() + " items in "
                        + (System.currentTimeMillis() - start) + " msecs [type=" + type + ",zone=" + zoneName
                        + ",parent=" + parentAuthority + ",immediate=" + immediate + ",filter=" + displayNamePattern
                        + "]");
            }

            return authorities;
        } finally {
            if (rs != null) {
                rs.close();
            }
        }
    }

    public Set<String> getContainedAuthorities(AuthorityType type, String parentName, boolean immediate) {
        AuthorityType parentAuthorityType = AuthorityType.getAuthorityType(parentName);
        if (parentAuthorityType == AuthorityType.USER) {
            // Users never contain other authorities
            return Collections.<String>emptySet();
        } else {
            NodeRef nodeRef = getAuthorityOrNull(parentName);
            if (nodeRef == null) {
                throw new UnknownAuthorityException("An authority was not found for " + parentName);
            }

            Set<String> authorities = new TreeSet<String>();
            listAuthorities(type, nodeRef, authorities, false, !immediate, false);
            return authorities;
        }
    }

    public void removeAuthority(String parentName, String childName) {
        removeAuthority(parentName, childName, true);
    }

    public void removeAuthority(String parentName, String childName, boolean cacheRefresh) {
        NodeRef parentRef = getAuthorityOrNull(parentName);
        if (parentRef == null) {
            throw new UnknownAuthorityException("An authority was not found for " + parentName);
        }
        NodeRef childRef = getAuthorityOrNull(childName);
        if (childRef == null) {
            throw new UnknownAuthorityException("An authority was not found for " + childName);
        }
        nodeService.removeChild(parentRef, childRef);
        childAuthorityCache.remove(parentRef);
        if (AuthorityType.getAuthorityType(childName) == AuthorityType.USER) {
            userAuthorityCache.remove(childName);
        } else {
            userAuthorityCache.clear();
            if (cacheRefresh) {
                authorityBridgeTableCache.refresh();
            }
        }
    }

    /**
     * Explicitly use the bridge table to list authorities.
     */
    private void listAuthoritiesByBridgeTable(Set<String> authorities, String name) {
        BridgeTable<String> bridgeTable = authorityBridgeTableCache.get();

        AuthorityType type = AuthorityType.getAuthorityType(name);
        switch (type) {
        case WILDCARD:
            // Dual use of the enum means that this value should not be received
            logger.warn("Found an authority with type '" + AuthorityType.WILDCARD + "': " + name);
        case ADMIN:
        case GUEST:
        case USER:
        case EVERYONE:
            NodeRef authRef = getAuthorityOrNull(name);
            List<AuthorityBridgeLink> parents = authorityBridgeDAO.getDirectAuthoritiesForUser(authRef);
            for (AuthorityBridgeLink parent : parents) {
                authorities.add(getPooledName(parent.getParentName()));
                for (String ancestor : bridgeTable.getAncestors(parent.getParentName())) {
                    authorities.add(getPooledName(ancestor));
                }
            }
            break;
        case GROUP:
        case OWNER:
        case ROLE:
            for (String ancestor : bridgeTable.getAncestors(name)) {
                authorities.add(getPooledName(ancestor));
            }
            break;
        }
    }

    @Override
    public Set<String> getContainingAuthorities(AuthorityType type, String name, boolean immediate) {
        // Optimize for the case where we want all the authorities that a user belongs to
        if (!immediate && AuthorityType.getAuthorityType(name) == AuthorityType.USER) {
            // Get the unfiltered set of authorities from the cache or generate it
            Set<String> authorities = userAuthorityCache.get(name);
            if (authorities == null) {
                authorities = new TreeSet<String>();
                if (useBridgeTable) {
                    listAuthoritiesByBridgeTable(authorities, name);
                } else {
                    listAuthorities(null, name, authorities, true, true);
                }
                // Add the set back to the cache.  If the value is locked then nothing will happen.
                userAuthorityCache.put(name, Collections.unmodifiableSet(authorities));
            }
            // If we wanted the unfiltered set we are done
            if (type == null) {
                return authorities;
            }
            // Apply the filtering by type
            Set<String> filteredAuthorities = new TreeSet<String>();
            for (String authority : authorities) {
                addAuthorityNameIfMatches(filteredAuthorities, authority, type);
            }
            return filteredAuthorities;
        }
        // Otherwise, crawl the DB for the answer
        else {
            Set<String> authorities = new TreeSet<String>();
            listAuthorities(type, name, authorities, true, !immediate);
            return authorities;
        }
    }

    public Set<String> getContainingAuthoritiesInZone(AuthorityType type, String authority, final String zoneName,
            AuthorityFilter filter, int size) {
        // Retrieved the cached 'sample' of authorities in the zone
        String currentUserDomain = tenantService.getCurrentUserDomain();
        Pair<String, String> cacheKey = new Pair<String, String>(currentUserDomain, zoneName);
        List<ChildAssociationRef> zoneAuthorities = zoneAuthorityCache.get(cacheKey);
        final int maxToProcess = Math.max(size, zoneAuthoritySampleSize);
        if (zoneAuthorities == null) {
            zoneAuthorities = AuthenticationUtil.runAs(new RunAsWork<List<ChildAssociationRef>>() {
                @Override
                public List<ChildAssociationRef> doWork() throws Exception {
                    NodeRef root = zoneName == null ? getAuthorityContainer() : getZone(zoneName);
                    if (root == null) {
                        return Collections.emptyList();
                    }
                    return nodeService.getChildAssocs(root, null, null, maxToProcess, false);
                }
            }, tenantService.getDomainUser(AuthenticationUtil.getSystemUserName(), currentUserDomain));
            zoneAuthorityCache.put(cacheKey, zoneAuthorities);
        }

        // Now search each for the required authority. If the number of results is greater than or close to the size
        // limit, then this will be the most efficient route
        Set<String> result = new TreeSet<String>();
        Set<String> positiveHits = new TreeSet<String>();
        Set<String> negativeHits = new TreeSet<String>();
        final int maxResults = size > 0 ? size : Integer.MAX_VALUE;
        int hits = 0, processed = 0;
        for (ChildAssociationRef groupAssoc : zoneAuthorities) {
            String containing = groupAssoc.getQName().getLocalName();
            AuthorityType containingType = AuthorityType.getAuthorityType(containing);
            processed++;
            // Cache the authority by key, if appropriate
            switch (containingType) {
            case USER:
            case ADMIN:
            case GUEST:
                break;
            default:
                Pair<String, String> containingKey = cacheKey(containing);
                if (!authorityLookupCache.contains(containingKey)) {
                    authorityLookupCache.put(containingKey, groupAssoc.getChildRef());
                }
            }
            if ((type == null || containingType == type)
                    && (authority == null || isAuthorityContained(groupAssoc.getChildRef(), containing, authority,
                            positiveHits, negativeHits))
                    && (filter == null || filter.includeAuthority(containing))) {
                result.add(getPooledName(containing));
                if (++hits == maxResults) {
                    break;
                }
            }

            // If this top down search is not providing an adequate hit count then resort to a naiive unlimited search
            if (processed >= maxToProcess) {
                Set<String> unfilteredResult;
                boolean filterZone;
                if (authority == null) {
                    unfilteredResult = new HashSet<String>(getAuthorities(type, zoneName, null, false, true,
                            new PagingRequest(0, filter == null ? maxResults : Integer.MAX_VALUE, null)).getPage());
                    if (filter == null) {
                        return unfilteredResult;
                    }
                    filterZone = false;
                } else {
                    unfilteredResult = getContainingAuthorities(type, authority, false);
                    filterZone = zoneName != null;
                }
                Set<String> newResult = new TreeSet<String>(result);
                int i = newResult.size();
                for (String container : unfilteredResult) {
                    // Do not call the filter multiple times on the same result in case it is 'stateful'
                    if (!result.contains(container) && (filter == null || filter.includeAuthority(container))
                            && (!filterZone || getAuthorityZones(container).contains(zoneName))) {
                        newResult.add(container);
                        if (++i >= maxResults) {
                            break;
                        }
                    }
                }
                result = newResult;
                break;
            }
        }
        return result;
    }

    public String getShortName(String name) {
        AuthorityType type = AuthorityType.getAuthorityType(name);
        if (type.isFixedString()) {
            return "";
        } else if (type.isPrefixed()) {
            return name.substring(type.getPrefixString().length());
        } else {
            return name;
        }
    }

    public String getName(AuthorityType type, String shortName) {
        String authorityName = shortName;
        if (type.isFixedString()) {
            authorityName = type.getFixedString();
        } else if (type.isPrefixed()) {
            String prefix = type.getPrefixString();
            if (shortName.startsWith(prefix)) {
                String doublePrefixed = prefix + prefix;
                if (shortName.startsWith(doublePrefixed)) {
                    throw new AuthorityException("The authority name is double-prefixed");
                }
            } else {
                authorityName = prefix + shortName;
            }
        }
        return authorityName;

    }

    protected void addAuthorityNameIfMatches(Set<String> authorities, String authorityName, AuthorityType type) {
        if (type == null || AuthorityType.getAuthorityType(authorityName).equals(type)) {
            authorities.add(getPooledName(authorityName));
        }
    }

    protected void addAuthorityNameIfMatches(Set<String> authorities, String authorityName, AuthorityType type,
            Pattern pattern) {
        if (type == null || AuthorityType.getAuthorityType(authorityName).equals(type)) {
            if (pattern == null) {
                authorities.add(getPooledName(authorityName));
            } else {
                if (pattern.matcher(getShortName(authorityName)).matches()) {
                    authorities.add(getPooledName(authorityName));
                } else {
                    String displayName = getAuthorityDisplayName(authorityName);
                    if (displayName != null && pattern.matcher(displayName).matches()) {
                        authorities.add(getPooledName(authorityName));
                    }
                }
            }
        }
    }

    private void listAuthorities(AuthorityType type, String name, Set<String> authorities, boolean parents,
            boolean recursive) {
        AuthorityType localType = AuthorityType.getAuthorityType(name);
        if (localType.equals(AuthorityType.GUEST)) {
            // Nothing to do
        } else {
            NodeRef ref = getAuthorityOrNull(name);

            if (ref != null) {
                listAuthorities(type, ref, authorities, parents, recursive, false);
            } else if (!localType.equals(AuthorityType.USER)) {
                // Don't worry about missing person objects. It might be the system user or a user yet to be
                // auto-created
                throw new UnknownAuthorityException("An authority was not found for " + name);
            }
        }
    }

    private void listAuthorities(AuthorityType type, NodeRef nodeRef, Set<String> authorities, boolean parents,
            boolean recursive, boolean includeNode) {
        Set<String> unfilteredAuthorities = new TreeSet<String>();
        listAuthoritiesUnfiltered(nodeRef, unfilteredAuthorities, parents, recursive, includeNode);
        for (String authorityName : unfilteredAuthorities) {
            addAuthorityNameIfMatches(authorities, authorityName, type);
        }
    }

    private void listAuthoritiesUnfiltered(NodeRef nodeRef, Set<String> authorities, boolean parents,
            boolean recursive, boolean includeNode) {
        if (includeNode) {
            String authorityName = DefaultTypeConverter.INSTANCE.convert(String.class,
                    nodeService.getProperty(nodeRef,
                            dictionaryService.isSubClass(nodeService.getType(nodeRef),
                                    ContentModel.TYPE_AUTHORITY_CONTAINER) ? ContentModel.PROP_AUTHORITY_NAME
                                            : ContentModel.PROP_USERNAME));
            if (!authorities.add(authorityName)) {
                // Stop recursing if we've already been here
                return;
            }
        }

        // Loop over children if we want immediate children or are in recursive mode
        if (!includeNode || recursive) {
            if (parents) {
                List<ChildAssociationRef> cars = nodeService.getParentAssocs(nodeRef, ContentModel.ASSOC_MEMBER,
                        RegexQNamePattern.MATCH_ALL);

                for (ChildAssociationRef car : cars) {
                    listAuthoritiesUnfiltered(car.getParentRef(), authorities, true, recursive, true);
                }
            } else {
                Pair<Map<NodeRef, String>, List<NodeRef>> childAuthorities = getChildAuthorities(nodeRef);
                Map<NodeRef, String> childAuthorityMap = childAuthorities.getFirst();
                // Recurse on non-leaves
                if (recursive) {
                    for (NodeRef childRef : childAuthorities.getSecond()) {
                        String childAuthorityName = childAuthorityMap.get(childRef);
                        if (authorities.add(childAuthorityName)) {
                            listAuthoritiesUnfiltered(childRef, authorities, false, true, false);
                        }
                    }
                }
                // Add leaves
                authorities.addAll(childAuthorityMap.values());
            }
        }
    }

    public boolean isAuthorityContained(String authority, String authorityToFind, Set<String> positiveHits,
            Set<String> negativeHits) {
        if (positiveHits.contains(authority)) {
            return true;
        }
        if (negativeHits.contains(authority)) {
            return false;
        }

        NodeRef authorityNodeRef = getAuthorityNodeRefOrNull(authority);
        if (authorityNodeRef == null) {
            negativeHits.add(getPooledName(authority));
            return false;
        }
        if (authorityBridgeTableCache.isUpToDate()) {
            return getContainingAuthorities(null, authorityToFind, false).contains(authority);
        } else {
            return isAuthorityContained(authorityNodeRef, getPooledName(authority), authorityToFind, positiveHits,
                    negativeHits);
        }
    }

    private boolean isAuthorityContained(NodeRef authorityNodeRef, String authority, String authorityToFind,
            Set<String> positiveHits, Set<String> negativeHits) {
        // Look up the desired authority using case sensitivity rules appropriate for the authority type
        NodeRef authorityToFindRef = getAuthorityOrNull(authorityToFind);
        if (authorityToFindRef == null) {
            // No such authority so it won't be contained anywhere
            negativeHits.add(authority);
            return false;
        }
        // Now we can just search for the NodeRef
        return isAuthorityContainedImpl(authorityNodeRef, authority, authorityToFindRef, positiveHits,
                negativeHits);
    }

    /**
     * @param authorityNodeRef              a containing authority
     * @param authorityToFindRef            an authority to find in the hierarchy
     * @return                              Returns <tt>true</tt> if the authority to find occurs
     *                                      in the hierarchy and is reachable via the {@link ContentModel#ASSOC_MEMBER}
     *                                      association.
     */
    private boolean isAuthorityContainedImpl(NodeRef authorityNodeRef, String authority, NodeRef authorityToFindRef,
            Set<String> positiveHits, Set<String> negativeHits) {
        if (positiveHits.contains(authority)) {
            return true;
        }
        if (negativeHits.contains(authority)) {
            return false;
        }
        Pair<Map<NodeRef, String>, List<NodeRef>> childAuthorities = getChildAuthorities(authorityNodeRef);
        Map<NodeRef, String> childAuthorityMap = childAuthorities.getFirst();

        // Is the authority we are looking for in the set provided (NodeRef is lookup)
        if (childAuthorityMap.containsKey(authorityToFindRef)) {
            positiveHits.add(authority);
            return true;
        }

        // Recurse on non-user authorities
        for (NodeRef nodeRef : childAuthorities.getSecond()) {
            if (isAuthorityContainedImpl(nodeRef, childAuthorityMap.get(nodeRef), authorityToFindRef, positiveHits,
                    negativeHits)) {
                positiveHits.add(authority);
                return true;
            }
        }
        negativeHits.add(authority);
        return false;
    }

    /**
     * Remove entries for the parents of the given node.
     * 
     * @param lock          <tt>true</tt> if the cache modifications need to be locked
     *                      i.e. if the caller is handling a <b>beforeXYZ</b> callback.
     */
    private void removeParentsFromChildAuthorityCache(NodeRef nodeRef, boolean lock) {
        // Get the transactional version of the cache if we need locking
        TransactionalCache<NodeRef, Pair<Map<NodeRef, String>, List<NodeRef>>> childAuthorityCacheTxn = null;
        if (lock && childAuthorityCache instanceof TransactionalCache) {
            childAuthorityCacheTxn = (TransactionalCache<NodeRef, Pair<Map<NodeRef, String>, List<NodeRef>>>) childAuthorityCache;
        }

        // Iterate over all relevant parents of the given node
        for (ChildAssociationRef car : nodeService.getParentAssocs(nodeRef)) {
            NodeRef parentRef = car.getParentRef();
            if (dictionaryService.isSubClass(nodeService.getType(parentRef),
                    ContentModel.TYPE_AUTHORITY_CONTAINER)) {
                TransactionalResourceHelper.getSet(PARENTS_OF_DELETING_CHILDREN_SET_RESOURCE).add(parentRef);
                childAuthorityCache.remove(parentRef);
                if (childAuthorityCacheTxn != null) {
                    childAuthorityCacheTxn.lockValue(parentRef);
                }
            }
        }
    }

    private NodeRef getAuthorityOrNull(final String name) {
        try {
            final AuthorityType authType = AuthorityType.getAuthorityType(name);
            switch (authType) {
            case USER:
                return personService.getPerson(name, false);
            case GUEST:
            case ADMIN:
            case EVERYONE:
            case OWNER:
                return null;
            default: {
                Pair<String, String> cacheKey = cacheKey(name);
                NodeRef result = authorityLookupCache.get(cacheKey);
                if (result == null) {
                    List<ChildAssociationRef> results = nodeService.getChildAssocs(getAuthorityContainer(),
                            ContentModel.ASSOC_CHILDREN, QName.createQName("cm", name, namespacePrefixResolver),
                            false);
                    result = results.isEmpty() ? NULL_NODEREF : results.get(0).getChildRef();
                    authorityLookupCache.put(cacheKey, result);
                }
                return result.equals(NULL_NODEREF) ? null : result;
            }
            }
        } catch (NoSuchPersonException e) {
            return null;
        }
    }

    /**
     * @return Returns the authority container, <b>which must exist</b>
     */
    private NodeRef getAuthorityContainer() {
        return getSystemContainer(qnameAssocAuthorities);
    }

    /**
     * @return Returns the zone container, <b>which must exist</b>
     */
    private NodeRef getZoneContainer() {
        return getSystemContainer(qnameAssocZones);
    }

    /**
     * Return the system container for the specified assoc name.
     * The containers are cached in a thread safe Tenant aware cache.
     *
     * @return System container, <b>which must exist</b>
     */
    private NodeRef getSystemContainer(QName assocQName) {
        final String cacheKey = KEY_SYSTEMCONTAINER_NODEREF + "." + assocQName.toString();
        NodeRef systemContainerRef = (NodeRef) singletonCache.get(cacheKey);
        if (systemContainerRef == null) {
            NodeRef rootNodeRef = nodeService.getRootNode(this.storeRef);
            List<ChildAssociationRef> results = nodeService.getChildAssocs(rootNodeRef, RegexQNamePattern.MATCH_ALL,
                    qnameAssocSystem, false);
            if (results.size() == 0) {
                throw new AlfrescoRuntimeException("Required system path not found: " + qnameAssocSystem);
            }
            NodeRef sysNodeRef = results.get(0).getChildRef();
            results = nodeService.getChildAssocs(sysNodeRef, RegexQNamePattern.MATCH_ALL, assocQName, false);
            if (results.size() == 0) {
                throw new AlfrescoRuntimeException("Required path not found: " + assocQName);
            }
            systemContainerRef = results.get(0).getChildRef();
            singletonCache.put(cacheKey, systemContainerRef);
        }
        return systemContainerRef;
    }

    public NodeRef getAuthorityNodeRefOrNull(String name) {
        return getAuthorityOrNull(name);
    }

    public String getAuthorityName(NodeRef authorityRef) {
        String name = null;
        if (nodeService.exists(authorityRef)) {
            QName type = nodeService.getType(authorityRef);
            if (dictionaryService.isSubClass(type, ContentModel.TYPE_AUTHORITY_CONTAINER)) {
                name = (String) nodeService.getProperty(authorityRef, ContentModel.PROP_AUTHORITY_NAME);
            } else if (dictionaryService.isSubClass(type, ContentModel.TYPE_PERSON)) {
                name = (String) nodeService.getProperty(authorityRef, ContentModel.PROP_USERNAME);
            }
        }
        return getPooledName(name);
    }

    public String getAuthorityDisplayName(String authorityName) {
        NodeRef ref = getAuthorityOrNull(authorityName);
        if (ref == null) {
            return null;
        }
        Serializable value = nodeService.getProperty(ref, ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
        if (value == null) {
            return null;
        }
        return DefaultTypeConverter.INSTANCE.convert(String.class, value);
    }

    public void setAuthorityDisplayName(String authorityName, String authorityDisplayName) {
        NodeRef ref = getAuthorityOrNull(authorityName);
        if (ref == null) {
            return;
        }
        nodeService.setProperty(ref, ContentModel.PROP_AUTHORITY_DISPLAY_NAME, authorityDisplayName);

    }

    public NodeRef getOrCreateZone(String zoneName) {
        return getOrCreateZone(zoneName, true);
    }

    private NodeRef getOrCreateZone(String zoneName, boolean create) {
        NodeRef zoneContainerRef = getZoneContainer();
        QName zoneQName = QName.createQName("cm", zoneName, namespacePrefixResolver);
        List<ChildAssociationRef> results = nodeService.getChildAssocs(zoneContainerRef,
                ContentModel.ASSOC_CHILDREN, zoneQName, false);
        if (results.isEmpty()) {
            if (create) {
                HashMap<QName, Serializable> props = new HashMap<QName, Serializable>();
                props.put(ContentModel.PROP_NAME, zoneName);
                return nodeService.createNode(zoneContainerRef, ContentModel.ASSOC_CHILDREN, zoneQName,
                        ContentModel.TYPE_ZONE, props).getChildRef();
            } else {
                return null;
            }
        } else {
            return results.get(0).getChildRef();
        }
    }

    public NodeRef getZone(String zoneName) {
        return getOrCreateZone(zoneName, false);
    }

    public Set<String> getAuthorityZones(String name) {
        Set<String> zones = new TreeSet<String>();
        NodeRef childRef = getAuthorityOrNull(name);
        if (childRef == null) {
            return null;
        }
        List<ChildAssociationRef> results = nodeService.getParentAssocs(childRef, ContentModel.ASSOC_IN_ZONE,
                RegexQNamePattern.MATCH_ALL);
        if (results.isEmpty()) {
            return zones;
        }

        for (ChildAssociationRef current : results) {
            NodeRef zoneRef = current.getParentRef();
            Serializable value = nodeService.getProperty(zoneRef, ContentModel.PROP_NAME);
            if (value == null) {
                continue;
            } else {
                String zone = DefaultTypeConverter.INSTANCE.convert(String.class, value);
                zones.add(zone);
            }
        }
        return zones;
    }

    public Set<String> getAllAuthoritiesInZone(String zoneName, AuthorityType type) {
        NodeRef zoneRef = getZone(zoneName);
        if (zoneRef == null) {
            return Collections.emptySet();
        }
        return new HashSet<String>(getAuthoritiesImpl(type, zoneRef, null, null, false,
                new PagingRequest(0, Integer.MAX_VALUE, null), new PagingResultsString()).getPage());
    }

    public void addAuthorityToZones(String authorityName, Set<String> zones) {
        if ((zones != null) && (zones.size() > 0)) {
            Set<NodeRef> zoneRefs = new HashSet<NodeRef>(zones.size() * 2);
            for (String authorityZone : zones) {
                zoneRefs.add(getOrCreateZone(authorityZone));
            }
            NodeRef authRef = getAuthorityOrNull(authorityName);
            if (authRef != null) {
                // Normalize the user name if necessary
                if (AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER) {
                    authorityName = (String) nodeService.getProperty(authRef, ContentModel.PROP_USERNAME);
                }

                nodeService.addChild(zoneRefs, authRef, ContentModel.ASSOC_IN_ZONE,
                        QName.createQName("cm", authorityName, namespacePrefixResolver));
            }
        }
    }

    public void removeAuthorityFromZones(String authorityName, Set<String> zones) {
        if ((zones != null) && (zones.size() > 0)) {
            NodeRef authRef = getAuthorityOrNull(authorityName);
            List<ChildAssociationRef> results = nodeService.getParentAssocs(authRef, ContentModel.ASSOC_IN_ZONE,
                    RegexQNamePattern.MATCH_ALL);
            for (ChildAssociationRef current : results) {
                NodeRef zoneRef = current.getParentRef();
                Serializable value = nodeService.getProperty(zoneRef, ContentModel.PROP_NAME);
                if (value == null) {
                    continue;
                } else {
                    String testZone = DefaultTypeConverter.INSTANCE.convert(String.class, value);
                    if (zones.contains(testZone)) {
                        nodeService.removeChildAssociation(current);
                    }
                }
            }
        }
    }

    private Set<String> getRootAuthoritiesUnderContainer(NodeRef container, AuthorityType type) {
        if (type != null && type.equals(AuthorityType.USER)) {
            return Collections.<String>emptySet();
        }
        Collection<ChildAssociationRef> childRefs = nodeService.getChildAssocsWithoutParentAssocsOfType(container,
                ContentModel.ASSOC_MEMBER);
        Set<String> authorities = new TreeSet<String>();
        for (ChildAssociationRef childRef : childRefs) {
            addAuthorityNameIfMatches(authorities, childRef.getQName().getLocalName(), type);
        }
        return authorities;
    }

    /**
     * Listen out for person removals so that we can clear cached authorities.
     */
    public void beforeDeleteNode(NodeRef nodeRef) {
        String authorityName = getAuthorityName(nodeRef);
        userAuthorityCache.remove(authorityName);
        if (userAuthorityCache instanceof TransactionalCache) {
            /*
             * We lock the removal for the duration of the transaction as the node has not
             * yet been deleted, leaving scope for some other code to come along and add the
             * value back before the deletion can actually take place.
             */
            TransactionalCache<String, Set<String>> userAuthorityCacheTxn = (TransactionalCache<String, Set<String>>) userAuthorityCache;
            userAuthorityCacheTxn.lockValue(authorityName);
        }
        // Remove cache elements for the parents, ensuring that we lock because the data still exists
        removeParentsFromChildAuthorityCache(nodeRef, true);
    }

    public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before,
            Map<QName, Serializable> after) {
        boolean isAuthority = dictionaryService.isSubClass(nodeService.getType(nodeRef),
                ContentModel.TYPE_AUTHORITY_CONTAINER);
        QName idProp = isAuthority ? ContentModel.PROP_AUTHORITY_NAME : ContentModel.PROP_USERNAME;
        String authBefore = DefaultTypeConverter.INSTANCE.convert(String.class, before.get(idProp));
        if (authBefore == null) {
            // Node has just been created; nothing to do
            return;
        }
        String authAfter = DefaultTypeConverter.INSTANCE.convert(String.class, after.get(idProp));
        if (!EqualsHelper.nullSafeEquals(authBefore, authAfter)) {
            if (AlfrescoTransactionSupport.getResource(PersonServiceImpl.KEY_ALLOW_UID_UPDATE) != null
                    || authBefore.equalsIgnoreCase(authAfter)) {
                if (isAuthority) {
                    if (authBefore != null) {
                        // Fix any ACLs
                        aclDao.renameAuthority(authBefore, authAfter);
                    }

                    // Fix primary association local name
                    QName newAssocQName = QName.createQName("cm", authAfter, namespacePrefixResolver);
                    ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef);
                    nodeService.moveNode(nodeRef, assoc.getParentRef(), assoc.getTypeQName(), newAssocQName);

                    // Fix other non-case sensitive parent associations
                    QName oldAssocQName = QName.createQName("cm", authBefore, namespacePrefixResolver);
                    newAssocQName = QName.createQName("cm", authAfter, 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);
                        }
                    }
                    authorityLookupCache.clear();
                    authorityBridgeTableCache.refresh();

                    // Cache is out of date
                    userAuthorityCache.clear();
                } else {
                    userAuthorityCache.remove(authBefore);
                }
                // Remove cache entires for the parents.  No need to lock because the data has already been updated.
                removeParentsFromChildAuthorityCache(nodeRef, false);
            } else {
                throw new UnsupportedOperationException("The name of an authority can not be changed");
            }
        }
    }

    public void init() {
        // Listen out for person removals so that we can clear cached authorities
        this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), ContentModel.TYPE_PERSON,
                new JavaBehaviour(this, "beforeDeleteNode"));
        // Listen out for updates to persons and authority containers to handle renames
        this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), ContentModel.TYPE_AUTHORITY,
                new JavaBehaviour(this, "onUpdateProperties"));
    }

    /**
     * @param parentNodeRef         the parent authority
     * @return                      Returns authorities reachable by the {@link ContentModel#ASSOC_MEMBER} association
     */
    private Pair<Map<NodeRef, String>, List<NodeRef>> getChildAuthorities(NodeRef parentNodeRef) {
        Pair<Map<NodeRef, String>, List<NodeRef>> result = childAuthorityCache.get(parentNodeRef);
        if (result == null) {
            List<ChildAssociationRef> cars = nodeService.getChildAssocs(parentNodeRef, ContentModel.ASSOC_MEMBER,
                    RegexQNamePattern.MATCH_ALL, false);
            if (cars.isEmpty()) {
                // ALF-17702: BM-0013: Soak: Run 02: getCachedChildAuthorities is not caching results
                //            Don't return here.  We need to cache the miss.
                result = new Pair<Map<NodeRef, String>, List<NodeRef>>(Collections.<NodeRef, String>emptyMap(),
                        Collections.<NodeRef>emptyList());
            } else {
                Map<NodeRef, String> lookup = new HashMap<NodeRef, String>(cars.size() * 2);
                List<NodeRef> parents = new LinkedList<NodeRef>();
                for (ChildAssociationRef car : cars) {
                    NodeRef memberNodeRef = car.getChildRef();
                    String memberName = getPooledName(car.getQName().getLocalName());
                    lookup.put(memberNodeRef, memberName);
                    AuthorityType authorityType = AuthorityType.getAuthorityType(memberName);
                    if (authorityType == AuthorityType.GROUP || authorityType == AuthorityType.ROLE) {
                        parents.add(memberNodeRef);
                    }
                }
                result = new Pair<Map<NodeRef, String>, List<NodeRef>>(lookup, parents);
            }
            // Cache whatever we have
            if (!TransactionalResourceHelper.getSet(PARENTS_OF_DELETING_CHILDREN_SET_RESOURCE)
                    .contains(parentNodeRef)) {
                childAuthorityCache.put(parentNodeRef, result);
            }
        }
        return result;
    }

    private abstract class AbstractPagingResults<R> implements PagingResults<R> {
        protected CannedQueryResults<AuthorityInfo> results;

        public void setResults(CannedQueryResults<AuthorityInfo> results) {
            this.results = results;
        }

        @Override
        public String getQueryExecutionId() {
            return results.getQueryExecutionId();
        }

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

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

    private class PagingResultsString extends AbstractPagingResults<String> {
        @Override
        public List<String> getPage() {
            List<String> auths = new ArrayList<String>(results.getPageCount());
            for (AuthorityInfo authInfo : results.getPage()) {
                auths.add((String) getPooledName(authInfo.getAuthorityName()));
            }
            return auths;
        }
    };

    private class PagingResultsAuthorityInfo extends AbstractPagingResults<AuthorityInfo> {
        @Override
        public List<AuthorityInfo> getPage() {
            List<AuthorityInfo> auths = new ArrayList<AuthorityInfo>(results.getPageCount());
            for (AuthorityInfo authInfo : results.getPage()) {
                auths.add((AuthorityInfo) authInfo);
            }
            return auths;
        }
    }

    @Override
    public void onRefreshableCacheEvent(RefreshableCacheEvent refreshableCacheEvent) {
        if (logger.isDebugEnabled()) {
            logger.debug("Bridge Table cache triggering userAuthorityCache.clear()");
        }
        userAuthorityCache.clear();
    }

    @Override
    public String getCacheId() {
        return AuthorityDAOImpl.class.getName();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        PropertyCheck.mandatory(this, "aclDao", aclDao);
        PropertyCheck.mandatory(this, "authorityBridgeDAO", authorityBridgeDAO);
        PropertyCheck.mandatory(this, "authorityBridgeTableCache", authorityBridgeTableCache);
        PropertyCheck.mandatory(this, "authorityLookupCache", authorityLookupCache);
        PropertyCheck.mandatory(this, "cannedQueryRegistry", cannedQueryRegistry);
        PropertyCheck.mandatory(this, "childAuthorityCache", childAuthorityCache);
        PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
        PropertyCheck.mandatory(this, "namespacePrefixResolver", namespacePrefixResolver);
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "personService", personService);
        PropertyCheck.mandatory(this, "policyComponent", policyComponent);
        PropertyCheck.mandatory(this, "searchService", searchService);
        PropertyCheck.mandatory(this, "storeRef", storeRef);
        PropertyCheck.mandatory(this, "tenantService", tenantService);
        PropertyCheck.mandatory(this, "userAuthorityCache", userAuthorityCache);
        PropertyCheck.mandatory(this, "zoneAuthorityCache", zoneAuthorityCache);
        PropertyCheck.mandatory(this, "storeRef", storeRef);
        PropertyCheck.mandatory(this, "storeRef", storeRef);
        authorityBridgeTableCache.register(this);

    };
}