/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package org.alfresco.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 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
     * 
     * @param aclCache SimpleCache
     */
    public void setAclCache(SimpleCache 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 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 toAdd = new ArrayList();
        List excluded = new ArrayList();
        List changes = new ArrayList();
        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 toAdd,
            Long inheritsFrom, boolean cascade,
            List changes, WriteMode mode)
    {
        List inherited = null;
        List positions = null;
        if ((mode == WriteMode.ADD_INHERITED) || (mode == WriteMode.INSERT_INHERITED) || (mode == WriteMode.CHANGE_INHERITED) || (mode == WriteMode.CREATE_AND_INHERIT ))
        {
            inherited = new ArrayList();
            positions = new ArrayList();
            // get aces for acl (via acl member)
            List members;
            if(parent != null)
            {
                members = aclCrudDAO.getAclMembersByAcl(parent);
            }
            else
            {
                members = Collections.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(), 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 visitedAcls,
            List extends AccessControlEntry> exclude, List toAdd, Long inheritsFrom,
            List inherited, List positions,
            boolean cascade, int depth, List 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 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 acesToAdd, Long inheritsFrom,
            List inherited, List 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 toAdd = new ArrayList(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 members = aclCrudDAO.getAclMembersByAcl(id);
            if (members.size() > 0)
            {
                List> aceIdsWithDepths = new ArrayList>(members.size());
                for (AclMember member : members)
                {
                    aceIdsWithDepths.add(new Pair(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