/*
 * 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.ownable.impl;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.copy.CopyBehaviourCallback;
import org.alfresco.repo.copy.CopyDetails;
import org.alfresco.repo.copy.CopyServicePolicies;
import org.alfresco.repo.copy.DefaultCopyBehaviourCallback;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.PropertyCheck;
import org.springframework.beans.factory.InitializingBean;
/**
 * Ownership service support. Use in permissions framework as dynamic authority.
 * 
 * @author Andy Hind
 */
public class OwnableServiceImpl implements
        OwnableService, InitializingBean,
        NodeServicePolicies.OnAddAspectPolicy,
        NodeServicePolicies.OnUpdatePropertiesPolicy,
        NodeServicePolicies.OnRemoveAspectPolicy,
        NodeServicePolicies.OnDeleteNodePolicy
{
    private NodeService nodeService;
    private AuthenticationService authenticationService;
    private SimpleCache nodeOwnerCache;
    private PolicyComponent policyComponent;
    private TenantService tenantService;
    private Set storesToIgnorePolicies = Collections.emptySet();
    public OwnableServiceImpl()
    {
        super();
    }
    // IOC
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    public void setAuthenticationService(AuthenticationService authenticationService)
    {
        this.authenticationService = authenticationService;
    }
    public void setPolicyComponent(PolicyComponent policyComponent)
    {
        this.policyComponent = policyComponent;
    }
    public void setTenantService(TenantService tenantService)
    {
        this.tenantService = tenantService;
    }
    public void setStoresToIgnorePolicies(Set storesToIgnorePolicies)
    {
        this.storesToIgnorePolicies = storesToIgnorePolicies;
    }
    /**
     * @param ownerCache
     *            a transactionally-safe cache of node owners
     */
    public void setNodeOwnerCache(SimpleCache ownerCache)
    {
        this.nodeOwnerCache = ownerCache;
    }
    public void afterPropertiesSet() throws Exception
    {
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "authenticationService", authenticationService);
        PropertyCheck.mandatory(this, "nodeOwnerCache", nodeOwnerCache);
        PropertyCheck.mandatory(this, "policyComponent", policyComponent);
    }
    
    public void init()
    {
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnAddAspectPolicy.QNAME,
                ContentModel.ASPECT_OWNABLE,
                new JavaBehaviour(this, "onAddAspect"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME,
                ContentModel.ASPECT_OWNABLE,
                new JavaBehaviour(this, "onUpdateProperties"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnRemoveAspectPolicy.QNAME,
                ContentModel.ASPECT_OWNABLE,
                new JavaBehaviour(this, "onRemoveAspect"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnDeleteNodePolicy.QNAME,
                ContentModel.ASPECT_OWNABLE,
                new JavaBehaviour(this, "onDeleteNode"));
        
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnAddAspectPolicy.QNAME,
                ContentModel.ASPECT_AUDITABLE,
                new JavaBehaviour(this, "onAddAspect"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME,
                ContentModel.ASPECT_AUDITABLE,
                new JavaBehaviour(this, "onUpdateProperties"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnRemoveAspectPolicy.QNAME,
                ContentModel.ASPECT_AUDITABLE,
                new JavaBehaviour(this, "onRemoveAspect"));
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.OnDeleteNodePolicy.QNAME,
                ContentModel.ASPECT_AUDITABLE,
                new JavaBehaviour(this, "onDeleteNode"));
        
        policyComponent.bindClassBehaviour(
                CopyServicePolicies.OnCopyNodePolicy.QNAME,
                ContentModel.ASPECT_OWNABLE, 
                new JavaBehaviour(this, "onCopyNode", NotificationFrequency.EVERY_EVENT));      
        policyComponent.bindClassBehaviour(
                CopyServicePolicies.OnCopyNodePolicy.QNAME,
                ContentModel.ASPECT_AUDITABLE, 
                new JavaBehaviour(this, "onCopyNode", NotificationFrequency.EVERY_EVENT));      
    }
    // OwnableService implmentation
    public String getOwner(NodeRef nodeRef)
    {
        String userName = nodeOwnerCache.get(nodeRef);
        if (userName == null)
        {
            // If ownership is not explicitly set then we fall back to the creator
            if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_OWNABLE))
            {
                userName = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, ContentModel.PROP_OWNER));
            }
            else if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_AUDITABLE))
            {
                userName = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, ContentModel.PROP_CREATOR));
            }
            cacheOwner(nodeRef, userName);
        }
        return userName;
    }
    public void setOwner(NodeRef nodeRef, String userName)
    {
        if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_OWNABLE))
        {
            HashMap properties = new HashMap(1, 1.0f);
            properties.put(ContentModel.PROP_OWNER, userName);
            nodeService.addAspect(nodeRef, ContentModel.ASPECT_OWNABLE, properties);
        }
        else
        {
            nodeService.setProperty(nodeRef, ContentModel.PROP_OWNER, userName);
        }
        cacheOwner(nodeRef, userName);
    }
    public void takeOwnership(NodeRef nodeRef)
    {
        setOwner(nodeRef, authenticationService.getCurrentUserName());
    }
    public boolean hasOwner(NodeRef nodeRef)
    {
        return getOwner(nodeRef) != null;
    }
    public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName)
    {
        nodeOwnerCache.remove(nodeRef);
    }
    public void onRemoveAspect(NodeRef nodeRef, QName aspectTypeQName)
    {
        nodeOwnerCache.remove(nodeRef);
    }
    public void onDeleteNode(ChildAssociationRef childAssocRef, boolean isNodeArchived)
    {
        nodeOwnerCache.remove(childAssocRef.getChildRef());
    }
    public void onUpdateProperties(NodeRef nodeRef, Map before, Map after)
    {
        Serializable pb = before.get(ContentModel.PROP_OWNER);
        Serializable pa = after.get(ContentModel.PROP_OWNER);
        if (!EqualsHelper.nullSafeEquals(pb, pa))
        {
            nodeOwnerCache.remove(nodeRef);
            return;
        }
        
        pb = before.get(ContentModel.PROP_CREATOR);
        pa = after.get(ContentModel.PROP_CREATOR);
        if (pb != null && !EqualsHelper.nullSafeEquals(pb, pa))
        {
            // A 'null' creator means this is a new node
            nodeOwnerCache.remove(nodeRef);
            return;
        }
    }
    
    /**
     * When an owned or audited node is copied, control which properties
     *  go over, and which are re-created
     */
    public CopyBehaviourCallback onCopyNode(QName classRef, CopyDetails copyDetails)
    {
        return AuditableOwnableAspectCopyBehaviourCallback.INSTANCE;   
    }
    
    private void cacheOwner(NodeRef nodeRef, String userName)
    {
        // do not cache owners of nodes that are from stores that ignores policies
        // to prevent mess in nodeOwnerCache
        if (!storesToIgnorePolicies.contains(tenantService.getBaseName(nodeRef.getStoreRef()).toString()))
        {
            nodeOwnerCache.put(nodeRef, userName);
        }
    }
    
    /**
     * Extends the default copy behaviour to prevent copying of some ownable and
     *  auditable properties, but lets the aspects themselves go through.
     * 
     * @author Nick Burch
     * @since 3.4
     */
    private static class AuditableOwnableAspectCopyBehaviourCallback extends DefaultCopyBehaviourCallback
    {
        private static final CopyBehaviourCallback INSTANCE = new AuditableOwnableAspectCopyBehaviourCallback();
        
        /**
         * Don't copy certain auditable p
         */
        @Override
        public Map getCopyProperties(
                QName classQName, CopyDetails copyDetails, Map properties)
        {
            if(classQName.equals(ContentModel.ASPECT_OWNABLE))
            {
                // The owner should become the user doing the copying
                if(properties.containsKey(ContentModel.PROP_OWNER))
                {
                    properties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser());
                }
            }
            else if(classQName.equals(ContentModel.ASPECT_AUDITABLE))
            {
                // Have the key properties reset by the aspect
                properties.remove(ContentModel.PROP_CREATED);
                properties.remove(ContentModel.PROP_CREATOR);
                properties.remove(ContentModel.PROP_MODIFIED);
                properties.remove(ContentModel.PROP_MODIFIER);
            }
            
            return properties;
        }
        
        /**
         * Do copy the aspects
         * 
         * @return          Returns true always
         */
        @Override
        public boolean getMustCopy(QName classQName, CopyDetails copyDetails)
        {
            return true;
        }
    }
}