/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.acls.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AuditableAcl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.OwnershipAcl;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.acls.model.UnloadedSidException;
import org.springframework.util.Assert;


/**
 * Base implementation of <code>Acl</code>.
 *
 * @author Ben Alex
 */
public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl {
    //~ Instance fields ================================================================================================

    private Acl parentAcl;
    private transient AclAuthorizationStrategy aclAuthorizationStrategy;
    private transient PermissionGrantingStrategy permissionGrantingStrategy;
    private final List<AccessControlEntry> aces = new ArrayList<AccessControlEntry>();
    private ObjectIdentity objectIdentity;
    private Serializable id;
    private Sid owner; // OwnershipAcl
    private List<Sid> loadedSids = null; // includes all SIDs the WHERE clause covered, even if there was no ACE for a SID
    private boolean entriesInheriting = true;

    //~ Constructors ===================================================================================================

    /**
     * Minimal constructor, which should be used {@link
     * org.springframework.security.acls.model.MutableAclService#createAcl(ObjectIdentity)}.
     *
     * @param objectIdentity the object identity this ACL relates to (required)
     * @param id the primary key assigned to this ACL (required)
     * @param aclAuthorizationStrategy authorization strategy (required)
     * @param auditLogger audit logger (required)
     */
    public AclImpl(ObjectIdentity objectIdentity, Serializable id, AclAuthorizationStrategy aclAuthorizationStrategy,
                    AuditLogger auditLogger) {
        Assert.notNull(objectIdentity, "Object Identity required");
        Assert.notNull(id, "Id required");
        Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required");
        Assert.notNull(auditLogger, "AuditLogger required");
        this.objectIdentity = objectIdentity;
        this.id = id;
        this.aclAuthorizationStrategy = aclAuthorizationStrategy;
        this.permissionGrantingStrategy = new DefaultPermissionGrantingStrategy(auditLogger);
    }

    /**
     * @deprecated Use the version which takes a  {@code PermissionGrantingStrategy} argument instead.
     */
    @Deprecated
    public AclImpl(ObjectIdentity objectIdentity, Serializable id, AclAuthorizationStrategy aclAuthorizationStrategy,
                    AuditLogger auditLogger, Acl parentAcl, List<Sid> loadedSids, boolean entriesInheriting, Sid owner) {
        this(objectIdentity, id, aclAuthorizationStrategy, new DefaultPermissionGrantingStrategy(auditLogger),
                parentAcl, loadedSids, entriesInheriting, owner);
    }

    /**
     * Full constructor, which should be used by persistence tools that do not
     * provide field-level access features.
     *
     * @param objectIdentity the object identity this ACL relates to
     * @param id the primary key assigned to this ACL
     * @param aclAuthorizationStrategy authorization strategy
     * @param grantingStrategy the {@code PermissionGrantingStrategy} which will be used by the {@code isGranted()} method
     * @param parentAcl the parent (may be may be {@code null})
     * @param loadedSids the loaded SIDs if only a subset were loaded (may be {@code null})
     * @param entriesInheriting if ACEs from the parent should inherit into
     *        this ACL
     * @param owner the owner (required)
     */
    public AclImpl(ObjectIdentity objectIdentity, Serializable id, AclAuthorizationStrategy aclAuthorizationStrategy,
            PermissionGrantingStrategy grantingStrategy, Acl parentAcl, List<Sid> loadedSids, boolean entriesInheriting, Sid owner) {
        Assert.notNull(objectIdentity, "Object Identity required");
        Assert.notNull(id, "Id required");
        Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required");
        Assert.notNull(owner, "Owner required");

        this.objectIdentity = objectIdentity;
        this.id = id;
        this.aclAuthorizationStrategy = aclAuthorizationStrategy;
        this.parentAcl = parentAcl; // may be null
        this.loadedSids = loadedSids; // may be null
        this.entriesInheriting = entriesInheriting;
        this.owner = owner;
        this.permissionGrantingStrategy = grantingStrategy;
    }

    /**
     * Private no-argument constructor for use by reflection-based persistence
     * tools along with field-level access.
     */
    @SuppressWarnings("unused")
    private AclImpl() {}

    //~ Methods ========================================================================================================

    public void deleteAce(int aceIndex) throws NotFoundException {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_GENERAL);
        verifyAceIndexExists(aceIndex);

        synchronized (aces) {
            this.aces.remove(aceIndex);
        }
    }

    private void verifyAceIndexExists(int aceIndex) {
        if (aceIndex < 0) {
            throw new NotFoundException("aceIndex must be greater than or equal to zero");
        }
        if (aceIndex >= this.aces.size()) {
            throw new NotFoundException("aceIndex must refer to an index of the AccessControlEntry list. " +
                    "List size is " + aces.size() + ", index was " + aceIndex);
        }
    }

    public void insertAce(int atIndexLocation, Permission permission, Sid sid, boolean granting) throws NotFoundException {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_GENERAL);
        Assert.notNull(permission, "Permission required");
        Assert.notNull(sid, "Sid required");
        if (atIndexLocation < 0) {
            throw new NotFoundException("atIndexLocation must be greater than or equal to zero");
        }
        if (atIndexLocation > this.aces.size()) {
            throw new NotFoundException("atIndexLocation must be less than or equal to the size of the AccessControlEntry collection");
        }

        AccessControlEntryImpl ace = new AccessControlEntryImpl(null, this, sid, permission, granting, false, false);

        synchronized (aces) {
            this.aces.add(atIndexLocation, ace);
        }
    }

    public List<AccessControlEntry> getEntries() {
        // Can safely return AccessControlEntry directly, as they're immutable outside the ACL package
        return new ArrayList<AccessControlEntry>(aces);
    }

    public Serializable getId() {
        return this.id;
    }

    public ObjectIdentity getObjectIdentity() {
        return objectIdentity;
    }

    public boolean isEntriesInheriting() {
        return entriesInheriting;
    }

    /**
     * Delegates to the {@link PermissionGrantingStrategy}.
     *
     * @throws UnloadedSidException if the passed SIDs are unknown to this ACL because the ACL was only loaded for a
     *         subset of SIDs
     * @see DefaultPermissionGrantingStrategy
     */
    public boolean isGranted(List<Permission> permission, List<Sid> sids, boolean administrativeMode)
            throws NotFoundException, UnloadedSidException {
        Assert.notEmpty(permission, "Permissions required");
        Assert.notEmpty(sids, "SIDs required");

        if (!this.isSidLoaded(sids)) {
            throw new UnloadedSidException("ACL was not loaded for one or more SID");
        }

        return permissionGrantingStrategy.isGranted(this, permission, sids, administrativeMode);
    }

    public boolean isSidLoaded(List<Sid> sids) {
        // If loadedSides is null, this indicates all SIDs were loaded
        // Also return true if the caller didn't specify a SID to find
        if ((this.loadedSids == null) || (sids == null) || (sids.size() == 0)) {
            return true;
        }

        // This ACL applies to a SID subset only. Iterate to check it applies.
        for (Sid sid: sids) {
            boolean found = false;

            for (Sid loadedSid : loadedSids) {
                if (sid.equals(loadedSid)) {
                    // this SID is OK
                    found = true;

                    break; // out of loadedSids for loop
                }
            }

            if (!found) {
                return false;
            }
        }

        return true;
    }

    public void setEntriesInheriting(boolean entriesInheriting) {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_GENERAL);
        this.entriesInheriting = entriesInheriting;
    }

    public void setOwner(Sid newOwner) {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_OWNERSHIP);
        Assert.notNull(newOwner, "Owner required");
        this.owner = newOwner;
    }

    public Sid getOwner() {
        return this.owner;
    }

    public void setParent(Acl newParent) {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_GENERAL);
        Assert.isTrue(newParent == null || !newParent.equals(this), "Cannot be the parent of yourself");
        this.parentAcl = newParent;
    }

    public Acl getParentAcl() {
        return parentAcl;
    }

    public void updateAce(int aceIndex, Permission permission)
        throws NotFoundException {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_GENERAL);
        verifyAceIndexExists(aceIndex);

        synchronized (aces) {
            AccessControlEntryImpl ace = (AccessControlEntryImpl) aces.get(aceIndex);
            ace.setPermission(permission);
        }
    }

    public void updateAuditing(int aceIndex, boolean auditSuccess, boolean auditFailure) {
        aclAuthorizationStrategy.securityCheck(this, AclAuthorizationStrategy.CHANGE_AUDITING);
        verifyAceIndexExists(aceIndex);

        synchronized (aces) {
            AccessControlEntryImpl ace = (AccessControlEntryImpl) aces.get(aceIndex);
            ace.setAuditSuccess(auditSuccess);
            ace.setAuditFailure(auditFailure);
        }
    }

    public boolean equals(Object obj) {
        if (obj instanceof AclImpl) {
            AclImpl rhs = (AclImpl) obj;
            if (this.aces.equals(rhs.aces)) {
                if ((this.parentAcl == null && rhs.parentAcl == null) || (this.parentAcl !=null && this.parentAcl.equals(rhs.parentAcl))) {
                    if ((this.objectIdentity == null && rhs.objectIdentity == null) || (this.objectIdentity != null && this.objectIdentity.equals(rhs.objectIdentity))) {
                        if ((this.id == null && rhs.id == null) || (this.id != null && this.id.equals(rhs.id))) {
                            if ((this.owner == null && rhs.owner == null) || (this.owner != null && this.owner.equals(rhs.owner))) {
                                if (this.entriesInheriting == rhs.entriesInheriting) {
                                    if ((this.loadedSids == null && rhs.loadedSids == null)) {
                                        return true;
                                    }
                                    if (this.loadedSids != null && (this.loadedSids.size() == rhs.loadedSids.size())) {
                                        for (int i = 0; i < this.loadedSids.size(); i++) {
                                            if (!this.loadedSids.get(i).equals(rhs.loadedSids.get(i))) {
                                                return false;
                                            }
                                        }
                                        return true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("AclImpl[");
        sb.append("id: ").append(this.id).append("; ");
        sb.append("objectIdentity: ").append(this.objectIdentity).append("; ");
        sb.append("owner: ").append(this.owner).append("; ");

        int count = 0;

        for (AccessControlEntry ace : aces) {
            count++;

            if (count == 1) {
                sb.append("\n");
            }

            sb.append(ace).append("\n");
        }

        if (count == 0) {
            sb.append("no ACEs; ");
        }

        sb.append("inheriting: ").append(this.entriesInheriting).append("; ");
        sb.append("parent: ").append((this.parentAcl == null) ? "Null" : this.parentAcl.getObjectIdentity().toString());
        sb.append("; ");
        sb.append("aclAuthorizationStrategy: ").append(this.aclAuthorizationStrategy).append("; ");
        sb.append("permissionGrantingStrategy: ").append(this.permissionGrantingStrategy);
        sb.append("]");

        return sb.toString();
    }

}
