/*
 * #%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.domain.permissions;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.domain.node.NodeDAO;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.ACEType;
import org.alfresco.repo.security.permissions.ACLCopyMode;
import org.alfresco.repo.security.permissions.ACLType;
import org.alfresco.repo.security.permissions.AccessControlEntry;
import org.alfresco.repo.security.permissions.AccessControlList;
import org.alfresco.repo.security.permissions.AccessControlListProperties;
import org.alfresco.repo.security.permissions.SimpleAccessControlEntry;
import org.alfresco.repo.security.permissions.SimpleAccessControlList;
import org.alfresco.repo.security.permissions.SimpleAccessControlListProperties;
import org.alfresco.repo.security.permissions.impl.AclChange;
import org.alfresco.repo.security.permissions.impl.SimplePermissionReference;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * DAO to manage ACL persistence
 * 
 * Note: based on earlier AclDaoComponentImpl
 * 
 * @author Andy Hind, janv
 * @since 3.4
 */
public class AclDAOImpl implements AclDAO
{
    private static Log logger = LogFactory.getLog(AclDAOImpl.class);

    private QNameDAO qnameDAO;
    private AclCrudDAO aclCrudDAO;
    private NodeDAO nodeDAO;
    private TenantService tenantService;
    private SimpleCache<Serializable, AccessControlList> aclCache;
    
    private enum WriteMode
    {
        /**
         * Remove inherited ACEs after that set
         */
        TRUNCATE_INHERITED,
        /**
         * Add inherited ACEs
         */
        ADD_INHERITED,
        /**
         * The source of inherited ACEs is changing
         */
        CHANGE_INHERITED,
        /**
         * Remove all inherited ACEs
         */
        REMOVE_INHERITED,
        /**
         * Insert inherited ACEs
         */
        INSERT_INHERITED,
        /**
         * Copy ACLs and update ACEs and inheritance
         */
        COPY_UPDATE_AND_INHERIT,
        /**
         * Simple copy
         */
        COPY_ONLY, CREATE_AND_INHERIT;
    }

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

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

    public void setAclCrudDAO(AclCrudDAO aclCrudDAO)
    {
        this.aclCrudDAO = aclCrudDAO;
    }

    public void setNodeDAO(NodeDAO nodeDAO)
    {
        this.nodeDAO = nodeDAO;
    }

    /**
     * Set the ACL cache
     * 
     */
    public void setAclCache(SimpleCache<Serializable, AccessControlList> aclCache)
    {
        this.aclCache = aclCache;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long createAccessControlList()
    {
        return createAccessControlList(getDefaultProperties()).getId();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AccessControlListProperties getDefaultProperties()
    {
        SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
        properties.setAclType(ACLType.DEFINING);
        properties.setInherits(true);
        properties.setVersioned(false);
        return properties;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Acl createAccessControlList(AccessControlListProperties properties)
    {
        if (properties == null)
        {
            throw new IllegalArgumentException("Properties cannot be null");
        }

        if (properties.getAclType() == null)
        {
            throw new IllegalArgumentException("ACL Type must be defined");
        }
        switch (properties.getAclType())
        {
        case OLD:
            if (properties.isVersioned() == Boolean.TRUE)
            {
                throw new IllegalArgumentException("Old acls can not be versioned");
            }
            break;
        case SHARED:
            throw new IllegalArgumentException("Can not create shared acls direct - use get inherited");
        case DEFINING:
        case LAYERED:
            break;
        case FIXED:
            if (properties.getInherits() == Boolean.TRUE)
            {
                throw new IllegalArgumentException("Fixed ACLs can not inherit");
            }
        case GLOBAL:
            if (properties.getInherits() == Boolean.TRUE)
            {
                throw new IllegalArgumentException("Fixed ACLs can not inherit");
            }
        default:
            break;
        }
        return createAccessControlList(properties, null, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Acl createAccessControlList(AccessControlListProperties properties, List<AccessControlEntry> aces, Long inherited)
    {
        if (properties == null)
        {
            throw new IllegalArgumentException("Properties cannot be null");
        }

        AclEntity acl = new AclEntity();
        if (properties.getAclId() != null)
        {
            acl.setAclId(properties.getAclId());
        }
        else
        {
            acl.setAclId(GUID.generate());
        }
        acl.setAclType(properties.getAclType());
        acl.setAclVersion(Long.valueOf(1l));

        switch (properties.getAclType())
        {
        case FIXED:
        case GLOBAL:
            acl.setInherits(Boolean.FALSE);
        case OLD:
        case SHARED:
        case DEFINING:
        case LAYERED:
        default:
            if (properties.getInherits() != null)
            {
                acl.setInherits(properties.getInherits());
            }
            else
            {
                acl.setInherits(Boolean.TRUE);
            }
            break;
        }
        acl.setLatest(Boolean.TRUE);

        switch (properties.getAclType())
        {
        case OLD:
            acl.setVersioned(Boolean.FALSE);
            break;
        case LAYERED:
            if (properties.isVersioned() != null)
            {
                acl.setVersioned(properties.isVersioned());
            }
            else
            {
                acl.setVersioned(Boolean.TRUE);
            }
            break;
        case FIXED:
        case GLOBAL:
        case SHARED:
        case DEFINING:
        default:
            if (properties.isVersioned() != null)
            {
                acl.setVersioned(properties.isVersioned());
            }
            else
            {
                acl.setVersioned(Boolean.FALSE);
            }
            break;
        }

        acl.setAclChangeSetId(getCurrentChangeSetId());
        acl.setRequiresVersion(false);

        Acl createdAcl = (AclEntity)aclCrudDAO.createAcl(acl);
        long created = createdAcl.getId();

        List<Ace> toAdd = new ArrayList<Ace>();
        List<AccessControlEntry> excluded = new ArrayList<AccessControlEntry>();
        List<AclChange> changes = new ArrayList<AclChange>();
        if ((aces != null) && aces.size() > 0)
        {
            for (AccessControlEntry ace : aces)
            {
                if ((ace.getPosition() != null) && (ace.getPosition() != 0))
                {
                    throw new IllegalArgumentException("Invalid position");
                }

                // Find authority
                Authority authority = aclCrudDAO.getOrCreateAuthority(ace.getAuthority());
                Permission permission = aclCrudDAO.getOrCreatePermission(ace.getPermission());

                // Find context
                if (ace.getContext() != null)
                {
                    throw new UnsupportedOperationException();
                }

                // Find ACE
                Ace entry = aclCrudDAO.getOrCreateAce(permission, authority, ace.getAceType(), ace.getAccessStatus());

                // Wire up
                // COW and remove any existing matches

                SimpleAccessControlEntry exclude = new SimpleAccessControlEntry();
                // match any access status
                exclude.setAceType(ace.getAceType());
                exclude.setAuthority(ace.getAuthority());
                exclude.setPermission(ace.getPermission());
                exclude.setPosition(0);

                toAdd.add(entry);
                excluded.add(exclude);
                // Will remove from the cache
            }
        }
        Long toInherit = null;
        if (inherited != null)
        {
            toInherit = getInheritedAccessControlList(inherited);
        }
        getWritable(created, toInherit, excluded, toAdd, toInherit, false, changes, WriteMode.CREATE_AND_INHERIT);

        // Fetch an up-to-date version
        return getAcl(created);
    }

    private void getWritable(
            final Long id, final Long parent,
            List<? extends AccessControlEntry> exclude, List<Ace> toAdd,
            Long inheritsFrom, boolean cascade,
            List<AclChange> changes, WriteMode mode)
    {
        List<Ace> inherited = null;
        List<Integer> positions = null;

        if ((mode == WriteMode.ADD_INHERITED) || (mode == WriteMode.INSERT_INHERITED) || (mode == WriteMode.CHANGE_INHERITED) || (mode == WriteMode.CREATE_AND_INHERIT ))
        {
            inherited = new ArrayList<Ace>();
            positions = new ArrayList<Integer>();

            // get aces for acl (via acl member)
            List<AclMember> members;
            if(parent != null)
            {
                members = aclCrudDAO.getAclMembersByAcl(parent);
            }
            else
            {
                members = Collections.<AclMember>emptyList(); 
            }

            for (AclMember member : members)
            {
                Ace aceEntity = aclCrudDAO.getAce(member.getAceId());

                if ((mode == WriteMode.INSERT_INHERITED) && (member.getPos() == 0))
                {
                    inherited.add(aceEntity);
                    positions.add(member.getPos());
                }
                else
                {
                    inherited.add(aceEntity);
                    positions.add(member.getPos());
                }
            }
        }

        getWritable(id, parent, new HashSet<Long>(), exclude, toAdd, inheritsFrom, inherited, positions, cascade, 0, changes, mode, false);
    }

    /**
     * Make a whole tree of ACLs copy on write if required Includes adding and removing ACEs which can be optimised
     * slightly for copy on write (no need to add and then remove)
     */
    private void getWritable(
            final Long id, final Long parent, Set<Long> visitedAcls,
            List<? extends AccessControlEntry> exclude, List<Ace> toAdd, Long inheritsFrom,
            List<Ace> inherited, List<Integer> positions,
            boolean cascade, int depth, List<AclChange> changes, WriteMode mode, boolean requiresVersion)
    {

        AclChange current = getWritable(id, parent, exclude, toAdd, inheritsFrom, inherited, positions, depth, mode, requiresVersion);
        changes.add(current);

        boolean cascadeVersion = requiresVersion;
        if (!cascadeVersion)
        {
            cascadeVersion = !current.getBefore().equals(current.getAfter());
        }

        if (cascade)
        {
            List<Long> inheritors = aclCrudDAO.getAclsThatInheritFromAcl(id);
            for (Long nextId : inheritors)
            {
                if (visitedAcls.contains(nextId))
                {
                    if (logger.isWarnEnabled())
                    {
                        StringBuilder message = new StringBuilder("ACL cycle detected! Repeated ALC id = '").append(nextId).append("', inherited ACL id = '").append(id).append(
                                "', already visited ACLs: '").append(visitedAcls).append("'. Skipping processing of the ACL id...");
                        logger.warn(message.toString());
                    }
                }
                else
                {
                    // Check for those that inherit themselves to other nodes ...
                    getWritable(nextId, current.getAfter(), visitedAcls, exclude, toAdd, current.getAfter(), inherited, positions, cascade, depth + 1, changes, mode,
                            cascadeVersion);
                }
            }
        }
    }

    /**
     * COW for an individual ACL
     * @return - an AclChange
     */
    private AclChange getWritable(
            final Long id, final Long parent,
            List<? extends AccessControlEntry> exclude, List<Ace> acesToAdd, Long inheritsFrom,
            List<Ace> inherited, List<Integer> positions, int depth, WriteMode mode, boolean requiresVersion)
    {
        AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id);
        if (!acl.isLatest())
        {
            return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType());
        }

        List<Long> toAdd = new ArrayList<Long>(0);
        if (acesToAdd != null)
        {
            for (Ace ace : acesToAdd)
            {
                toAdd.add(ace.getId());
            }
        }

        if (!acl.isVersioned())
        {
            switch (mode)
            {
            case COPY_UPDATE_AND_INHERIT:
                removeAcesFromAcl(id, exclude, depth);
                aclCrudDAO.addAclMembersToAcl(acl.getId(), toAdd, depth);
                break;
            case CHANGE_INHERITED:
                replaceInherited(id, acl, inherited, positions, depth);
                break;
            case ADD_INHERITED:
                addInherited(acl, inherited, positions, depth);
                break;
            case TRUNCATE_INHERITED:
                truncateInherited(id, depth);
                break;
            case INSERT_INHERITED:
                insertInherited(id, acl, inherited, positions, depth);
                break;
            case REMOVE_INHERITED:
                removeInherited(id, depth);
                break;
            case CREATE_AND_INHERIT:
                aclCrudDAO.addAclMembersToAcl(acl.getId(), toAdd, depth);
                addInherited(acl, inherited, positions, depth);
            case COPY_ONLY:
            default:
                break;
            }
            if (inheritsFrom != null)
            {
                acl.setInheritsFrom(inheritsFrom);
            }
            acl.setAclChangeSetId(getCurrentChangeSetId());
            aclCrudDAO.updateAcl(acl);
            return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType());
        }
        else if ((acl.getAclChangeSetId() == getCurrentChangeSetId()) && (!requiresVersion) && (!acl.getRequiresVersion()))
        {
            switch (mode)
            {
            case COPY_UPDATE_AND_INHERIT:
                removeAcesFromAcl(id, exclude, depth);
                aclCrudDAO.addAclMembersToAcl(acl.getId(), toAdd, depth);
                break;
            case CHANGE_INHERITED:
                replaceInherited(id, acl, inherited, positions, depth);
                break;
            case ADD_INHERITED:
                addInherited(acl, inherited, positions, depth);
                break;
            case TRUNCATE_INHERITED:
                truncateInherited(id, depth);
                break;
            case INSERT_INHERITED:
                insertInherited(id, acl, inherited, positions, depth);
                break;
            case REMOVE_INHERITED:
                removeInherited(id, depth);
                break;
            case CREATE_AND_INHERIT:
                aclCrudDAO.addAclMembersToAcl(acl.getId(), toAdd, depth);
                addInherited(acl, inherited, positions, depth);
            case COPY_ONLY:
            default:
                break;
            }
            if (inheritsFrom != null)
            {
                acl.setInheritsFrom(inheritsFrom);
            }
            aclCrudDAO.updateAcl(acl);
            return new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType());
        }
        else
        {
            AclEntity newAcl = new AclEntity();
            newAcl.setAclChangeSetId(getCurrentChangeSetId());
            newAcl.setAclId(acl.getAclId());
            newAcl.setAclType(acl.getAclType());
            newAcl.setAclVersion(acl.getAclVersion() + 1);
            newAcl.setInheritedAcl(-1l);
            newAcl.setInherits(acl.getInherits());
            newAcl.setInheritsFrom((inheritsFrom != null) ? inheritsFrom : acl.getInheritsFrom());
            newAcl.setLatest(Boolean.TRUE);
            newAcl.setVersioned(Boolean.TRUE);
            newAcl.setRequiresVersion(Boolean.FALSE);

            AclEntity createdAcl = (AclEntity)aclCrudDAO.createAcl(newAcl);
            long created = createdAcl.getId();

            // Create new membership entries - excluding those in the given pattern

            // AcePatternMatcher excluder = new AcePatternMatcher(exclude);

            // get aces for acl (via acl member)
            List<AclMember> members = aclCrudDAO.getAclMembersByAcl(id);

            if (members.size() > 0)
            {
                List<Pair<Long,Integer>> aceIdsWithDepths = new ArrayList<Pair<Long,Integer>>(members.size());

                for (AclMember member : members)
                {
                    aceIdsWithDepths.add(new Pair<Long, Integer>(member.getAceId(), member.getPos()));
                }

                // copy acl members to new acl
                aclCrudDAO.addAclMembersToAcl(newAcl.getId(), aceIdsWithDepths);
            }

            // add new

            switch (mode)
            {
            case COPY_UPDATE_AND_INHERIT:
                // Done above
                removeAcesFromAcl(newAcl.getId(), exclude, depth);
                aclCrudDAO.addAclMembersToAcl(newAcl.getId(), toAdd, depth);
                break;
            case CHANGE_INHERITED:
                replaceInherited(newAcl.getId(), newAcl, inherited, positions, depth);
                break;
            case ADD_INHERITED:
                addInherited(newAcl, inherited, positions, depth);
                break;
            case TRUNCATE_INHERITED:
                truncateInherited(newAcl.getId(), depth);
                break;
            case INSERT_INHERITED:
                insertInherited(newAcl.getId(), newAcl, inherited, positions, depth);
                break;
            case REMOVE_INHERITED:
                removeInherited(newAcl.getId(), depth);
                break;
            case CREATE_AND_INHERIT:
                aclCrudDAO.addAclMembersToAcl(acl.getId(), toAdd, depth);
                addInherited(acl, inherited, positions, depth);
            case COPY_ONLY:
            default:
                break;
            }

            // Fix up inherited ACL if required
            if (newAcl.getAclType() == ACLType.SHARED)
            {
                if (parent != null)
                {
                    Long writableParentAcl = getWritable(parent, null, null, null, null, null, null, 0, WriteMode.COPY_ONLY, false).getAfter();
                    AclUpdateEntity parentAcl = aclCrudDAO.getAclForUpdate(writableParentAcl);
                    parentAcl.setInheritedAcl(created);
                    aclCrudDAO.updateAcl(parentAcl);
                }
            }

            // fix up old version
            acl.setLatest(Boolean.FALSE);
            acl.setRequiresVersion(Boolean.FALSE);
            aclCrudDAO.updateAcl(acl);
            return new AclChangeImpl(id, created, acl.getAclType(), newAcl.getAclType());
        }
    }

    /**
     * Helper to remove ACEs from an ACL
     */
    private void removeAcesFromAcl(final Long id, final List<? extends AccessControlEntry> exclude, final int depth)
    {
        if (exclude == null)
        {
            // cascade delete all acl members - no exclusion
            aclCrudDAO.deleteAclMembersByAcl(id);
        }
        else
        {
            AcePatternMatcher excluder = new AcePatternMatcher(exclude);

            List<Map<String, Object>> results = aclCrudDAO.getAcesAndAuthoritiesByAcl(id);
            List<Long> memberIds = new ArrayList<Long>(results.size());

            for (Map<String, Object> result : results)
            {
                Long result_aclmemId = (Long) result.get("aclmemId");

                if ((exclude != null) && excluder.matches(aclCrudDAO, result, depth))
                {
                    memberIds.add(result_aclmemId);
                }
            }

            // delete list of acl members
            aclCrudDAO.deleteAclMembers(memberIds);
        }
    }

    private void replaceInherited(Long id, Acl acl, List<Ace> inherited, List<Integer> positions, int depth)
    {
        truncateInherited(id, depth);
        addInherited(acl, inherited, positions, depth);
    }

    private void truncateInherited(final Long id, int depth)
    {
        List<AclMember> members = aclCrudDAO.getAclMembersByAcl(id);

        List<Long> membersToDelete = new ArrayList<Long>(members.size());
        for (AclMember member : members)
        {
            if (member.getPos() > depth)
            {
                membersToDelete.add(member.getId());
            }
        }

        if (membersToDelete.size() > 0)
        {
            // delete list of acl members
            aclCrudDAO.deleteAclMembers(membersToDelete);
        }
    }

    private void removeInherited(final Long id, int depth)
    {
        List<AclMemberEntity> members = aclCrudDAO.getAclMembersByAclForUpdate(id);

        List<Long> membersToDelete = new ArrayList<Long>(members.size());
        for (AclMemberEntity member : members)
        {
            if (member.getPos() == depth + 1)
            {
                membersToDelete.add(member.getId());
            }
            else if (member.getPos() > (depth + 1))
            {
                member.setPos(member.getPos() - 1);
                aclCrudDAO.updateAclMember(member);
            }
        }

        if (membersToDelete.size() > 0)
        {
            // delete list of acl members
            aclCrudDAO.deleteAclMembers(membersToDelete);
        }
    }

    private void addInherited(Acl acl, List<Ace> inherited, List<Integer> positions, int depth)
    {
        if ((inherited != null) && (inherited.size() > 0))
        {
            List<Pair<Long,Integer>> aceIdsWithDepths = new ArrayList<Pair<Long,Integer>>(inherited.size());
            for (int i = 0; i < inherited.size(); i++)
            {
                Ace add = inherited.get(i);
                Integer position = positions.get(i);
                aceIdsWithDepths.add(new Pair<Long, Integer>(add.getId(), position.intValue() + depth + 1));
            }
            aclCrudDAO.addAclMembersToAcl(acl.getId(), aceIdsWithDepths);
        }
    }

    private void insertInherited(final Long id, AclEntity acl, List<Ace> inherited, List<Integer> positions, int depth)
    {
        // get aces for acl (via acl member)
        List<AclMemberEntity> members = aclCrudDAO.getAclMembersByAclForUpdate(id);

        for (AclMemberEntity member : members)
        {
            if (member.getPos() > depth)
            {
                member.setPos(member.getPos() + 1);
                aclCrudDAO.updateAclMember(member);
            }
        }

        addInherited(acl, inherited, positions, depth);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> deleteAccessControlEntries(final String authority)
    {
        List<AclChange> aclChanges = new LinkedList<AclChange>();

        // get authority
        Authority authEntity = aclCrudDAO.getAuthority(authority);
        if (authEntity == null)
        {
            return aclChanges;
        }

        List<Long> aces = new ArrayList<Long>();

        List<AclMember> members = aclCrudDAO.getAclMembersByAuthority(authority);

        boolean leaveAuthority = false;
        if (members.size() > 0)
        {
            Set<AclUpdateEntity> acls = new HashSet<AclUpdateEntity>(members.size() * 2);
            List<Long> membersToDelete = new ArrayList<Long>(members.size());

            // fix up members and extract acls and aces
            for (AclMember member : members)
            {
                // Delete acl entry
                Long aclMemberId = member.getId();
                Long aclId = member.getAclId();
                Long aceId = member.getAceId();
                
                boolean hasAnotherTenantNodes = false;
                if (AuthenticationUtil.isMtEnabled())
                {
                    // ALF-3563
                    
                    // Retrieve dependent nodes
                    List<Long> nodeIds = aclCrudDAO.getADMNodesByAcl(aclId, -1);
                    
                    if (nodeIds.size() > 0)
                    {
                        for (Long nodeId : nodeIds)
                        {
                            Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(nodeId);
                            if (nodePair == null)
                            {
                                logger.warn("Node does not exist: " + nodeId);
                                continue;
                            }
                            else
                            {
                                NodeRef nodeRef = nodePair.getSecond();
                                try
                                {
                                    // Throws AlfrescoRuntimeException in case of domain mismatch
                                    tenantService.checkDomain(nodeRef.getStoreRef().getIdentifier());
                                }
                                catch (AlfrescoRuntimeException e)
                                {
                                    hasAnotherTenantNodes = true;
                                    leaveAuthority = true;
                                    break;
                                }
                            }
                        }
                    }
                }
                
                if (!hasAnotherTenantNodes)
                {
                    AclUpdateEntity list = aclCrudDAO.getAclForUpdate(aclId);
                    aclChanges.add(new AclChangeImpl(aclId, aclId, list.getAclType(), list.getAclType()));
                    acls.add(list);
                    membersToDelete.add(aclMemberId);
                    aces.add((Long)aceId);
                }
            }

            // delete list of acl members
            aclCrudDAO.deleteAclMembers(membersToDelete);

            // Remember to 'touch' all affected ACLs
            for (AclUpdateEntity acl : acls)
            {
                acl.setAclChangeSetId(getCurrentChangeSetId());
                aclCrudDAO.updateAcl(acl);
            }
        }

        if (!leaveAuthority)
        {
            // remove ACEs
            aclCrudDAO.deleteAces(aces);

            // Tidy up any unreferenced ACEs

            // get aces by authority
            List<Ace> unreferenced = aclCrudDAO.getAcesByAuthority(authEntity.getId());

            if (unreferenced.size() > 0)
            {
                List<Long> unrefencedAcesToDelete = new ArrayList<Long>(unreferenced.size());
                for (Ace ace : unreferenced)
                {
                    unrefencedAcesToDelete.add(ace.getId());
                }
                aclCrudDAO.deleteAces(unrefencedAcesToDelete);
            }

            // remove authority
            if (authEntity != null)
            {
                aclCrudDAO.deleteAuthority(authEntity.getId());
            }
        }

        return aclChanges;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteAclForNode(long aclId)
    {
        Acl dbAcl = getAcl(aclId);
        if (dbAcl.getAclType() == ACLType.DEFINING)
        {
            // delete acl members & acl
            aclCrudDAO.deleteAclMembersByAcl(aclId);
            aclCrudDAO.deleteAcl(aclId);
        }
        if (dbAcl.getAclType() == ACLType.SHARED)
        {
            // check unused
            Long defining = dbAcl.getInheritsFrom();
            if (aclCrudDAO.getAcl(defining) == null)
            {
                // ADM
                if (getADMNodesByAcl(aclId, 1).size() == 0)
                {
                    // delete acl members & acl
                    aclCrudDAO.deleteAclMembersByAcl(aclId);
                    aclCrudDAO.deleteAcl(aclId);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> deleteAccessControlList(final Long id)
    {
        if (logger.isDebugEnabled())
        {
            // debug only
            int maxForDebug = 11;
            List<Long> nodeIds = getADMNodesByAcl(id, maxForDebug);

            for (Long nodeId : nodeIds)
            {
                logger.debug("deleteAccessControlList: Found nodeId=" + nodeId + ", aclId=" + id);
            }
        }

        List<AclChange> acls = new ArrayList<AclChange>();

        final AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id);
        if (!acl.isLatest())
        {
            throw new UnsupportedOperationException("Old ACL versions can not be updated");
        }
        if (acl.getAclType() == ACLType.SHARED)
        {
            throw new UnsupportedOperationException("Delete is not supported for shared acls - they are deleted with the defining acl");
        }

        if ((acl.getAclType() == ACLType.DEFINING) || (acl.getAclType() == ACLType.LAYERED))
        {
            if ((acl.getInheritedAcl() != null) && (acl.getInheritedAcl() != -1))
            {
                final Acl inherited = aclCrudDAO.getAcl(acl.getInheritedAcl());

                // Will remove from the cache
                getWritable(inherited.getId(), acl.getInheritsFrom(), null, null, null, true, acls, WriteMode.REMOVE_INHERITED);
                Acl unusedInherited = null;
                for (AclChange change : acls)
                {
                    if (change.getBefore()!= null && change.getBefore().equals(inherited.getId()))
                    {
                        unusedInherited = aclCrudDAO.getAcl(change.getAfter());
                    }
                }

                final Long newId = unusedInherited.getId();
                List<Long> inheritors = aclCrudDAO.getAclsThatInheritFromAcl(newId);
                for (Long nextId : inheritors)
                {
                    // Will remove from the cache
                    getWritable(nextId, acl.getInheritsFrom(), null, null, acl.getInheritsFrom(), true, acls, WriteMode.REMOVE_INHERITED);
                }

                // delete acl members
                aclCrudDAO.deleteAclMembersByAcl(newId);

                // delete 'unusedInherited' acl
                aclCrudDAO.deleteAcl(unusedInherited.getId());

                if (inherited.isVersioned())
                {
                    AclUpdateEntity inheritedForUpdate = aclCrudDAO.getAclForUpdate(inherited.getId());
                    if (inheritedForUpdate != null)
                    {
                        inheritedForUpdate.setLatest(Boolean.FALSE);
                        aclCrudDAO.updateAcl(inheritedForUpdate);
                    }
                }
                else
                {
                    // delete 'inherited' acl 
                    aclCrudDAO.deleteAcl(inherited.getId());
                }
            }
        }
        else
        {
            List<Long> inheritors = aclCrudDAO.getAclsThatInheritFromAcl(id);
            for (Long nextId : inheritors)
            {
                // Will remove from the cache
                getWritable(nextId, acl.getInheritsFrom(), null, null, null, true, acls, WriteMode.REMOVE_INHERITED);
            }
        }

        // delete
        if (acl.isVersioned())
        {
            acl.setLatest(Boolean.FALSE);
            acl.setAclChangeSetId(getCurrentChangeSetId());
            aclCrudDAO.updateAcl(acl);
        }
        else
        {
            // delete acl members & acl
            aclCrudDAO.deleteAclMembersByAcl(id);
            aclCrudDAO.deleteAcl(acl.getId());
        }

        acls.add(new AclChangeImpl(id, null, acl.getAclType(), null));
        return acls;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> deleteLocalAccessControlEntries(Long id)
    {
        List<AclChange> changes = new ArrayList<AclChange>();
        SimpleAccessControlEntry pattern = new SimpleAccessControlEntry();
        pattern.setPosition(Integer.valueOf(0));
        // Will remove from the cache
        getWritable(id, null, Collections.singletonList(pattern), null, null, true, changes, WriteMode.COPY_UPDATE_AND_INHERIT);
        return changes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> deleteInheritedAccessControlEntries(Long id)
    {
        List<AclChange> changes = new ArrayList<AclChange>();
        SimpleAccessControlEntry pattern = new SimpleAccessControlEntry();
        pattern.setPosition(Integer.valueOf(-1));
        // Will remove from the cache
        getWritable(id, null, Collections.singletonList(pattern), null, null, true, changes, WriteMode.COPY_UPDATE_AND_INHERIT);
        return changes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> deleteAccessControlEntries(Long id, AccessControlEntry pattern)
    {
        List<AclChange> changes = new ArrayList<AclChange>();
        // Will remove from the cache
        getWritable(id, null, Collections.singletonList(pattern), null, null, true, changes, WriteMode.COPY_UPDATE_AND_INHERIT);
        return changes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Acl getAcl(Long id)
    {
        return aclCrudDAO.getAcl(id);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AccessControlListProperties getAccessControlListProperties(Long id)
    {
        ParameterCheck.mandatory("id", id);                 // Prevent unboxing failures
        return aclCrudDAO.getAcl(id);
    }

    @Override
    public void setCheckAclConsistency()
    {
        aclCrudDAO.setCheckAclConsistency();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AccessControlList getAccessControlList(Long id)
    {
        // Used the cached properties as our cache key
        AccessControlListProperties properties = getAccessControlListProperties(id);
        if (properties == null)
        {
            return null;
        }
        AccessControlList aclCached = aclCache.get((Serializable)properties);
        if (aclCached != null)
        {
            return aclCached;
        }

        SimpleAccessControlList acl = new SimpleAccessControlList();
        acl.setProperties(properties);

        List<Map<String, Object>> results = aclCrudDAO.getAcesAndAuthoritiesByAcl(id);

        List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>(results.size());
        for (Map<String, Object> result : results)
            // for (AclMemberEntity member : members)
        {
            Boolean aceIsAllowed = (Boolean) result.get("allowed");
            Integer aceType = (Integer) result.get("applies");
            String authority = (String) result.get("authority");
            Long permissionId = (Long) result.get("permissionId");
            Integer position = (Integer) result.get("pos");
            //Long result_aclmemId = (Long) result.get("aclmemId"); // not used here

            SimpleAccessControlEntry sacEntry = new SimpleAccessControlEntry();
            sacEntry.setAccessStatus(aceIsAllowed ? AccessStatus.ALLOWED : AccessStatus.DENIED);
            sacEntry.setAceType(ACEType.getACETypeFromId(aceType));
            sacEntry.setAuthority(authority);
            // if (entry.getContext() != null)
            // {
            // SimpleAccessControlEntryContext context = new SimpleAccessControlEntryContext();
            // context.setClassContext(entry.getContext().getClassContext());
            // context.setKVPContext(entry.getContext().getKvpContext());
            // context.setPropertyContext(entry.getContext().getPropertyContext());
            // sacEntry.setContext(context);
            // }
            Permission perm = aclCrudDAO.getPermission(permissionId);
            QName permTypeQName = qnameDAO.getQName(perm.getTypeQNameId()).getSecond(); // Has an ID so must exist
            SimplePermissionReference permissionRefernce = SimplePermissionReference.getPermissionReference(permTypeQName, perm.getName());
            sacEntry.setPermission(permissionRefernce);
            sacEntry.setPosition(position);
            entries.add(sacEntry);
        }

        Collections.sort(entries);
        acl.setEntries(entries);
        
        // Cache it for next time
        aclCache.put((Serializable)properties, acl);

        return acl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long getInheritedAccessControlList(Long id)
    {
        AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id);
        if (acl.getAclType() == ACLType.OLD)
        {
            return null;
        }
        if ((acl.getInheritedAcl() != null) && (acl.getInheritedAcl() != -1))
        {
            return acl.getInheritedAcl();
        }

        Long inheritedAclId = null;

        if ((acl.getAclType() == ACLType.DEFINING) || (acl.getAclType() == ACLType.LAYERED))
        {
            List<AclChange> changes = new ArrayList<AclChange>();
            // created shared acl
            SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
            properties.setAclType(ACLType.SHARED);
            properties.setInherits(Boolean.TRUE);
            properties.setVersioned(acl.isVersioned());
            Long sharedId = createAccessControlList(properties, null, null).getId();
            getWritable(sharedId, id, null, null, id, true, changes, WriteMode.ADD_INHERITED);
            acl.setInheritedAcl(sharedId);
            inheritedAclId = sharedId;
        }
        else
        {
            acl.setInheritedAcl(acl.getId());
            inheritedAclId = acl.getId();
        }

        acl.setAclChangeSetId(getCurrentChangeSetId());
        aclCrudDAO.updateAcl(acl);
        return inheritedAclId;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> mergeInheritedAccessControlList(Long inherited, Long target)
    {
        // TODO: For now we do a replace - we could do an insert if both inherit from the same acl

        List<AclChange> changes = new ArrayList<AclChange>();

        Acl targetAcl = aclCrudDAO.getAcl(target);

        Acl inheritedAcl = null;
        if (inherited != null)
        {
            inheritedAcl = aclCrudDAO.getAcl(inherited);
        }
        else
        {
            // Assume we are just resetting it to inherit as before
            if (targetAcl.getInheritsFrom() != null)
            {
                inheritedAcl = aclCrudDAO.getAcl(targetAcl.getInheritsFrom());
                if (inheritedAcl == null)
                {
                    // TODO: Try previous versions
                    throw new IllegalStateException("No old inheritance definition to use");
                }
                else
                {
                    // find the latest version of the acl
                    if (!inheritedAcl.isLatest())
                    {
                        final String searchAclId = inheritedAcl.getAclId();

                        Long actualInheritor = (Long)aclCrudDAO.getLatestAclByGuid(searchAclId);

                        inheritedAcl = aclCrudDAO.getAcl(actualInheritor);
                        if (inheritedAcl == null)
                        {
                            // TODO: Try previous versions
                            throw new IllegalStateException("No ACL found");
                        }
                    }
                }
            }
            else
            {
                // There is no inheritance to set
                return changes;
            }
        }

        // recursion test
        // if inherited already inherits from the target

        Acl test = inheritedAcl;
        Set<Long> visitedAcls = new HashSet<Long>();
        while (test != null)
        {
            Long testId = test.getId();
            if (testId != null && testId.equals(target))
            {
                throw new IllegalStateException("Cyclical ACL detected");
            }
            if (visitedAcls.contains(testId))
            {
                throw new IllegalStateException("Cyclical InheritsFrom detected. AclId: " + testId);
            }
            else
            {
                visitedAcls.add(testId);
            }
            Long parent = test.getInheritsFrom();
            if ((parent == null) || (parent == -1l))
            {
                test = null;
            }
            else
            {
                test = aclCrudDAO.getAcl(test.getInheritsFrom());
            }
        }

        if ((targetAcl.getAclType() != ACLType.DEFINING) && (targetAcl.getAclType() != ACLType.LAYERED))
        {
            throw new IllegalArgumentException("Only defining ACLs can have their inheritance set");
        }

        if (!targetAcl.getInherits())
        {
            return changes;
        }

        Long actualInheritedId = inheritedAcl.getId();

        if ((inheritedAcl.getAclType() == ACLType.DEFINING) || (inheritedAcl.getAclType() == ACLType.LAYERED))
        {
            actualInheritedId = getInheritedAccessControlList(actualInheritedId);
        }
        // Will remove from the cache
        getWritable(target, actualInheritedId, null, null, actualInheritedId, true, changes, WriteMode.CHANGE_INHERITED);

        return changes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> setAccessControlEntry(final Long id, final AccessControlEntry ace)
    {
        Acl target = aclCrudDAO.getAcl(id);
        if (target.getAclType() == ACLType.SHARED)
        {
            throw new IllegalArgumentException("Shared ACLs are immutable");
        }

        List<AclChange> changes = new ArrayList<AclChange>();

        if ((ace.getPosition() != null) && (ace.getPosition() != 0))
        {
            throw new IllegalArgumentException("Invalid position");
        }

        // Find authority
        Authority authority = aclCrudDAO.getOrCreateAuthority(ace.getAuthority());
        Permission permission = aclCrudDAO.getOrCreatePermission(ace.getPermission());

        // Find context
        if (ace.getContext() != null)
        {
            throw new UnsupportedOperationException();
        }

        // Find ACE
        Ace entry = aclCrudDAO.getOrCreateAce(permission, authority, ace.getAceType(), ace.getAccessStatus());

        // Wire up
        // COW and remove any existing matches

        SimpleAccessControlEntry exclude = new SimpleAccessControlEntry();
        // match any access status
        exclude.setAceType(ace.getAceType());
        exclude.setAuthority(ace.getAuthority());
        exclude.setPermission(ace.getPermission());
        exclude.setPosition(0);
        List<Ace> toAdd = new ArrayList<Ace>(1);
        toAdd.add(entry);
        // Will remove from the cache
        getWritable(id, null, Collections.singletonList(exclude), toAdd, null, true, changes, WriteMode.COPY_UPDATE_AND_INHERIT);

        return changes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> enableInheritance(Long id, Long parent)
    {
        List<AclChange> changes = new ArrayList<AclChange>();

        AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id);

        switch (acl.getAclType())
        {
        case FIXED:
        case GLOBAL:
            throw new IllegalArgumentException("Fixed and global permissions can not inherit");
        case OLD:
            acl.setInherits(Boolean.TRUE);
            acl.setAclChangeSetId(getCurrentChangeSetId());
            aclCrudDAO.updateAcl(acl);
            changes.add(new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()));
            return changes;
        case SHARED:
            // TODO support a list of children and casacade if given
            throw new IllegalArgumentException(
            "Shared acls should be replace by creating a definig ACL, wiring it up for inhertitance, and then applying inheritance to any children. It can not be done by magic ");
        case DEFINING:
        case LAYERED:
        default:
            if (!acl.getInherits())
            {
                // Will remove from the cache
                getWritable(id, null, null, null, null, false, changes, WriteMode.COPY_ONLY);
                acl = aclCrudDAO.getAclForUpdate(changes.get(0).getAfter());
                acl.setInherits(Boolean.TRUE);
                acl.setAclChangeSetId(getCurrentChangeSetId());
                aclCrudDAO.updateAcl(acl);
            }
            else
            {
                // Will remove from the cache
                getWritable(id, null, null, null, null, false, changes, WriteMode.COPY_ONLY);
            }

            List<AclChange> merged = mergeInheritedAccessControlList(parent, changes.get(0).getAfter());
            changes.addAll(merged);
            return changes;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AclChange> disableInheritance(Long id, boolean setInheritedOnAcl)
    {
        AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(id);
        List<AclChange> changes = new ArrayList<AclChange>(1);
        switch (acl.getAclType())
        {
        case FIXED:
        case GLOBAL:
            return Collections.<AclChange> singletonList(new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()));
        case OLD:
            acl.setInherits(Boolean.FALSE);
            acl.setAclChangeSetId(getCurrentChangeSetId());
            aclCrudDAO.updateAcl(acl);
            changes.add(new AclChangeImpl(id, id, acl.getAclType(), acl.getAclType()));
            return changes;
        case SHARED:
            // TODO support a list of children and casacade if given
            throw new IllegalArgumentException("Shared ACL must inherit");
        case DEFINING:
        case LAYERED:
        default:
            return disableInheritanceImpl(id, setInheritedOnAcl, acl);
        }
    }

    private Long getCopy(Long toCopy, Long toInheritFrom, ACLCopyMode mode)
    {
        AclUpdateEntity aclToCopy;
        Long inheritedId;
        Acl aclToInheritFrom;
        switch (mode)
        {
        case INHERIT:
            if (toCopy.equals(toInheritFrom))
            {
                return getInheritedAccessControlList(toCopy);
            }
            else
            {
                throw new UnsupportedOperationException();
            }
        case COW:
            aclToCopy = aclCrudDAO.getAclForUpdate(toCopy);
            aclToCopy.setRequiresVersion(true);
            aclToCopy.setAclChangeSetId(getCurrentChangeSetId());
            aclCrudDAO.updateAcl(aclToCopy);
            inheritedId = getInheritedAccessControlList(toCopy);
            if ((inheritedId != null) && (!inheritedId.equals(toCopy)))
            {
                AclUpdateEntity inheritedAcl = aclCrudDAO.getAclForUpdate(inheritedId);
                inheritedAcl.setRequiresVersion(true);
                inheritedAcl.setAclChangeSetId(getCurrentChangeSetId());
                aclCrudDAO.updateAcl(inheritedAcl);
            }
            return toCopy;
        case REDIRECT:
            if ((toInheritFrom != null) && (toInheritFrom.equals(toCopy)))
            {
                return getInheritedAccessControlList(toInheritFrom);
            }
            aclToCopy = aclCrudDAO.getAclForUpdate(toCopy);
            aclToInheritFrom = null;
            if (toInheritFrom != null)
            {
                aclToInheritFrom = aclCrudDAO.getAcl(toInheritFrom);
            }

            switch (aclToCopy.getAclType())
            {
            case DEFINING:
                // This is not called on the redirecting node as only LAYERED change permissions when redirected
                // So this needs to make a copy in the same way layered does
            case LAYERED:
                if (toInheritFrom == null)
                {
                    return toCopy;
                }
                // manages cache clearing beneath
                List<AclChange> changes = mergeInheritedAccessControlList(toInheritFrom, toCopy);
                for (AclChange change : changes)
                {
                    if (change.getBefore().equals(toCopy))
                    {
                        return change.getAfter();
                    }
                }
                throw new UnsupportedOperationException();
            case SHARED:
                if (aclToInheritFrom != null)
                {
                    return getInheritedAccessControlList(toInheritFrom);
                }
                else
                {
                    throw new UnsupportedOperationException();
                }
            case FIXED:
            case GLOBAL:
            case OLD:
                return toCopy;
            default:
                throw new UnsupportedOperationException();
            }
        case COPY:
            aclToCopy = aclCrudDAO.getAclForUpdate(toCopy);
            aclToInheritFrom = null;
            if (toInheritFrom != null)
            {
                aclToInheritFrom = aclCrudDAO.getAcl(toInheritFrom);
            }

            switch (aclToCopy.getAclType())
            {
            case DEFINING:
                SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
                properties.setAclType(ACLType.DEFINING);
                properties.setInherits(aclToCopy.getInherits());
                properties.setVersioned(true);

                Long id = createAccessControlList(properties).getId();

                AccessControlList indirectAcl = getAccessControlList(toCopy);
                for (AccessControlEntry entry : indirectAcl.getEntries())
                {
                    if (entry.getPosition() == 0)
                    {
                        setAccessControlEntry(id, entry);
                    }
                }
                if (aclToInheritFrom != null)
                {
                    mergeInheritedAccessControlList(toInheritFrom, id);
                }
                return id;
            case SHARED:
                if (aclToInheritFrom != null)
                {
                    return getInheritedAccessControlList(toInheritFrom);
                }
                else
                {
                    return null;
                }
            case FIXED:
            case GLOBAL:
            case LAYERED:
            case OLD:
                return toCopy;
            default:
                throw new UnsupportedOperationException();
            }
        default:
            throw new UnsupportedOperationException();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Acl getAclCopy(Long toCopy, Long toInheritFrom, ACLCopyMode mode)
    {
        return getAclEntityCopy(toCopy, toInheritFrom, mode);
    }

    private Acl getAclEntityCopy(Long toCopy, Long toInheritFrom, ACLCopyMode mode)
    {
        Long id = getCopy(toCopy, toInheritFrom, mode);
        if (id == null)
        {
            return null;
        }
        return aclCrudDAO.getAcl(id);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Long> getADMNodesByAcl(long aclEntityId, int maxResults)
    {
        return aclCrudDAO.getADMNodesByAcl(aclEntityId, maxResults);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Acl createLayeredAcl(Long indirectedAcl)
    {
        SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
        properties.setAclType(ACLType.LAYERED);

        Acl acl = createAccessControlList(properties);
        long id = acl.getId();

        if (indirectedAcl != null)
        {
            mergeInheritedAccessControlList(indirectedAcl, id);
        }
        return acl;
    }

    private List<AclChange> disableInheritanceImpl(Long id, boolean setInheritedOnAcl, AclEntity aclIn)
    {
        List<AclChange> changes = new ArrayList<AclChange>();

        if (!aclIn.getInherits())
        {
            return Collections.<AclChange> emptyList();
        }

        // Manages caching
        getWritable(id, null, null, null, null, false, changes, WriteMode.COPY_ONLY);
        AclUpdateEntity acl = aclCrudDAO.getAclForUpdate(changes.get(0).getAfter());
        final Long inheritsFrom = acl.getInheritsFrom();
        acl.setInherits(Boolean.FALSE);
        acl.setAclChangeSetId(getCurrentChangeSetId());
        aclCrudDAO.updateAcl(acl);

        // Keep inherits from so we can reinstate if required
        // acl.setInheritsFrom(-1l);

        // Manages caching
        getWritable(acl.getId(), null, null, null, null, true, changes, WriteMode.TRUNCATE_INHERITED);

        // set Inherited - TODO: UNTESTED

        if ((inheritsFrom != null) && (inheritsFrom != -1) && setInheritedOnAcl)
        {
            // get aces for acl (via acl member)
            List<AclMember> members = aclCrudDAO.getAclMembersByAcl(inheritsFrom);

            for (AclMember member : members)
            {
                // TODO optimise
                Ace ace = aclCrudDAO.getAce(member.getAceId());
                Authority authority = aclCrudDAO.getAuthority(ace.getAuthorityId());

                SimpleAccessControlEntry entry = new SimpleAccessControlEntry();
                entry.setAccessStatus(ace.isAllowed() ? AccessStatus.ALLOWED : AccessStatus.DENIED);
                entry.setAceType(ace.getAceType());
                entry.setAuthority(authority.getAuthority());

                /* NOTE: currently unused - intended for possible future enhancement
                if (ace.getContextId() != null)
                {
                    AceContext aceContext = aclCrudDAO.getAceContext(ace.getContextId());

                    SimpleAccessControlEntryContext context = new SimpleAccessControlEntryContext();
                    context.setClassContext(aceContext.getClassContext());
                    context.setKVPContext(aceContext.getKvpContext());
                    context.setPropertyContext(aceContext.getPropertyContext());
                    entry.setContext(context);
                }
                 */

                Permission perm = aclCrudDAO.getPermission(ace.getPermissionId());
                QName permTypeQName = qnameDAO.getQName(perm.getTypeQNameId()).getSecond(); // Has an ID so must exist
                SimplePermissionReference permissionRefernce = SimplePermissionReference.getPermissionReference(permTypeQName, perm.getName());
                entry.setPermission(permissionRefernce);
                entry.setPosition(Integer.valueOf(0));

                setAccessControlEntry(id, entry);
            }
        }
        return changes;
    }

    private static final String RESOURCE_KEY_ACL_CHANGE_SET_ID = "acl.change.set.id";

    private UpdateChangeSetListener updateChangeSetListener = new UpdateChangeSetListener();
    /**
     * Wrapper to update the current changeset to get the change time correct
     * 
     * @author Derek Hulley
     * @since 4.0
     */
    private class UpdateChangeSetListener extends TransactionListenerAdapter
    {
        @Override
        public void beforeCommit(boolean readOnly)
        {
            if (readOnly)
            {
                return;
            }
            Long changeSetId = (Long) AlfrescoTransactionSupport.getResource(RESOURCE_KEY_ACL_CHANGE_SET_ID);
            if (changeSetId == null)
            {
                // There has not been a change
                return;
            }
            // Update it
            long commitTimeMs = System.currentTimeMillis();
            aclCrudDAO.updateAclChangeSet(changeSetId, commitTimeMs);
        }
    }
    /**
     * Support to get the current ACL change set and bind this to the transaction. So we only make one new version of an
     * ACL per change set. If something is in the current change set we can update it.
     */
    private long getCurrentChangeSetId()
    {
        Long changeSetId = (Long) AlfrescoTransactionSupport.getResource(RESOURCE_KEY_ACL_CHANGE_SET_ID);
        if (changeSetId == null)
        {
            changeSetId = aclCrudDAO.createAclChangeSet();

            // bind the ID and the listener
            AlfrescoTransactionSupport.bindResource(RESOURCE_KEY_ACL_CHANGE_SET_ID, changeSetId);
            AlfrescoTransactionSupport.bindListener(updateChangeSetListener);
            if (logger.isDebugEnabled())
            {
                logger.debug("New change set = " + changeSetId);
            }
        }
        return changeSetId;
    }

    private static class AcePatternMatcher
    {
        private List<? extends AccessControlEntry> patterns;

        AcePatternMatcher(List<? extends AccessControlEntry> patterns)
        {
            this.patterns = patterns;
        }

        boolean matches(AclCrudDAO aclCrudDAO, Map<String, Object> result, int position)
        {
            if (patterns == null)
            {
                return true;
            }

            for (AccessControlEntry pattern : patterns)
            {
                if (checkPattern(aclCrudDAO, result, position, pattern))
                {
                    return true;
                }
            }
            return false;
        }

        private boolean checkPattern(AclCrudDAO aclCrudDAO, Map<String, Object> result, int position, AccessControlEntry pattern)
        {
            Boolean result_aceIsAllowed = (Boolean) result.get("allowed");
            Integer result_aceType = (Integer) result.get("applies");
            String result_authority = (String) result.get("authority");
            Long result_permissionId = (Long) result.get("permissionId");
            Integer result_position = (Integer) result.get("pos");
            //Long result_aclmemId = (Long) result.get("aclmemId"); // not used

            if (pattern.getAccessStatus() != null)
            {
                if (pattern.getAccessStatus() != (result_aceIsAllowed ? AccessStatus.ALLOWED : AccessStatus.DENIED))
                {
                    return false;
                }
            }

            if (pattern.getAceType() != null)
            {
                if (pattern.getAceType() != ACEType.getACETypeFromId(result_aceType))
                {
                    return false;
                }
            }

            if (pattern.getAuthority() != null)
            {
                if ((pattern.getAuthorityType() != AuthorityType.WILDCARD) && !pattern.getAuthority().equals(result_authority))
                {
                    return false;
                }
            }

            if (pattern.getContext() != null)
            {
                throw new IllegalArgumentException("Context not yet supported");
            }

            if (pattern.getPermission() != null)
            {
                Long permId = aclCrudDAO.getPermission(pattern.getPermission()).getId();
                if (!permId.equals(result_permissionId))
                {
                    return false;
                }
            }

            if (pattern.getPosition() != null)
            {
                if (pattern.getPosition().intValue() >= 0)
                {
                    if (result_position != position)
                    {
                        return false;
                    }
                }
                else if (pattern.getPosition().intValue() == -1)
                {
                    if (result_position <= position)
                    {
                        return false;
                    }
                }
            }

            return true;
        }
    }

    static class AclChangeImpl implements AclChange
    {
        private Long before;
        private Long after;
        private ACLType typeBefore;
        private ACLType typeAfter;

        public AclChangeImpl(Long before, Long after, ACLType typeBefore, ACLType typeAfter)
        {
            this.before = before;
            this.after = after;
            this.typeAfter = typeAfter;
            this.typeBefore = typeBefore;
        }

        public Long getAfter()
        {
            return after;
        }

        public Long getBefore()
        {
            return before;
        }

        /**
         * @param after Long
         */
        public void setAfter(Long after)
        {
            this.after = after;
        }

        /**
         * @param before Long
         */
        public void setBefore(Long before)
        {
            this.before = before;
        }

        public ACLType getTypeAfter()
        {
            return typeAfter;
        }

        /**
         * @param typeAfter ACLType
         */
        public void setTypeAfter(ACLType typeAfter)
        {
            this.typeAfter = typeAfter;
        }

        public ACLType getTypeBefore()
        {
            return typeBefore;
        }

        /**
         * @param typeBefore ACLType
         */
        public void setTypeBefore(ACLType typeBefore)
        {
            this.typeBefore = typeBefore;
        }

        @Override
        public String toString()
        {
            StringBuilder builder = new StringBuilder();
            builder.append("(").append(getBefore()).append(",").append(getTypeBefore()).append(")");
            builder.append(" - > ");
            builder.append("(").append(getAfter()).append(",").append(getTypeAfter()).append(")");
            return builder.toString();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void renameAuthority(String before, String after)
    {
        aclCrudDAO.renameAuthority(before, after);
        aclCache.clear();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void fixSharedAcl(Long shared, Long defining)
    {
        if (defining == null)
        {
            throw new IllegalArgumentException("Null defining acl");
        }
        
        if (shared == null)
        {
            throw new IllegalArgumentException("Null shared acl");
        }
        List<AclChange> changes = new ArrayList<AclChange>();
        getWritable(shared, defining, null, null, defining, true, changes, WriteMode.CHANGE_INHERITED);
    }

    /* (non-Javadoc)
     * @see org.alfresco.repo.domain.permissions.AclDAO#getMaxChangeSetCommitTime()
     */
    @Override
    public Long getMaxChangeSetCommitTime()
    {
        return aclCrudDAO.getMaxChangeSetCommitTime();
    }

    /* (non-Javadoc)
     * @see org.alfresco.repo.domain.permissions.AclDAO#getMaxChangeSetIdByCommitTime(long)
     */
    @Override
    public Long getMaxChangeSetIdByCommitTime(long maxCommitTime)
    {
        return aclCrudDAO.getMaxChangeSetIdByCommitTime(maxCommitTime);
    }
}
