/*
 * #%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 .
 * #L%
 */
package org.alfresco.repo.audit;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.audit.extractor.DataExtractor;
import org.alfresco.repo.audit.generator.DataGenerator;
import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.audit.model.AuditModelRegistry;
import org.alfresco.repo.audit.model.AuditModelRegistryImpl;
import org.alfresco.repo.audit.model.AuditApplication.DataExtractorDefinition;
import org.alfresco.repo.domain.audit.AuditDAO;
import org.alfresco.repo.domain.propval.PropertyValueDAO;
import org.alfresco.repo.domain.schema.SchemaBootstrap;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PathMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.ParameterCheck;
/**
 * Component that records audit values as well as providing the query implementation.
 * 
 * To turn on logging of all potentially auditable data, turn on logging for:
 * {@link #INBOUND_LOGGER org.alfresco.repo.audit.inbound}.
 * 
 * TODO: Respect audit internal - at the moment audit internal is fixed to false.
 * 
 * @author Derek Hulley
 * @since 3.2 (in its current form)
 */
public class AuditComponentImpl implements AuditComponent
{
    private static final String INBOUND_LOGGER = "org.alfresco.repo.audit.inbound";
    
    private static Log logger = LogFactory.getLog(AuditComponentImpl.class);
    private static Log loggerInbound = LogFactory.getLog(INBOUND_LOGGER);
    private AuditModelRegistryImpl auditModelRegistry;
    private PropertyValueDAO propertyValueDAO;
    private AuditDAO auditDAO;
    private TransactionService transactionService;
    private AuditFilter auditFilter;
    private UserAuditFilter userAuditFilter;
    
    /**
     * Default constructor
     */
    public AuditComponentImpl()
    {
    }
    
    /**
     * Set the registry holding the audit models
     * @since 3.2
     */
    public void setAuditModelRegistry(AuditModelRegistryImpl auditModelRegistry)
    {
        this.auditModelRegistry = auditModelRegistry;
    }
    /**
     * Set the DAO for manipulating property values
     * @since 3.2
     */
    public void setPropertyValueDAO(PropertyValueDAO propertyValueDAO)
    {
        this.propertyValueDAO = propertyValueDAO;
    }
    
    /**
     * Set the DAO for accessing audit data
     * @since 3.2
     */
    public void setAuditDAO(AuditDAO auditDAO)
    {
        this.auditDAO = auditDAO;
    }
    /**
     * Set the service used to start new transactions
     */
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }
    
    /**
     * Set the component used to filter which audit events to record
     */
    public void setAuditFilter(AuditFilter auditFilter)
    {
        this.auditFilter = auditFilter;
    }
    public void setUserAuditFilter(UserAuditFilter userAuditFilter)
    {
        this.userAuditFilter = userAuditFilter;
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public int deleteAuditEntries(String applicationName, Long fromTime, Long toTime)
    {
        ParameterCheck.mandatory("applicationName", applicationName);
        AlfrescoTransactionSupport.checkTransactionReadState(true);
        
        AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName);
        if (application == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("No audit application named '" + applicationName + "' has been registered.");
            }
            return 0;
        }
        
        Long applicationId = application.getApplicationId();
        
        int deleted = auditDAO.deleteAuditEntries(applicationId, fromTime, toTime);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Delete audit " + deleted + " entries for " + applicationName + 
                    " (" + fromTime + " to " + toTime);
        }
        return deleted;
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    @Override
    public int deleteAuditEntries(List auditEntryIds)
    {
        // Shortcut, if necessary
        if (auditEntryIds.size() == 0)
        {
            return 0;
        }
        return auditDAO.deleteAuditEntries(auditEntryIds);
    }
    /**
     * @param application       the audit application object
     * @return                  Returns a copy of the set of disabled paths associated with the application
     */
    @SuppressWarnings("unchecked")
    private Set getDisabledPaths(AuditApplication application)
    {
        try
        {
            Long disabledPathsId = application.getDisabledPathsId();
            Set disabledPaths = (Set) propertyValueDAO.getPropertyById(disabledPathsId);
            return new HashSet(disabledPaths);
        }
        catch (Throwable e)
        {
            // Might be an invalid ID, somehow
            auditModelRegistry.loadAuditModels();
            throw new AlfrescoRuntimeException("Unabled to get AuditApplication disabled paths: " + application, e);
        }
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public boolean isAuditEnabled()
    {
        return auditModelRegistry.isAuditEnabled();                
    }
    /**
     * {@inheritDoc}
     * @since 3.4
     */
    @Override
    public void setAuditEnabled(boolean enable)
    {
        boolean alreadyEnabled = auditModelRegistry.isAuditEnabled();
        if (alreadyEnabled != enable)
        {
            // It is changing
            auditModelRegistry.stop();
            auditModelRegistry.setProperty(
                        AuditModelRegistry.AUDIT_PROPERTY_AUDIT_ENABLED,
                        Boolean.toString(enable).toLowerCase());
            auditModelRegistry.start();
        }
    }
    /**
     * {@inheritDoc}
     * @since 3.4
     */
    public Map getAuditApplications()
    {
        return auditModelRegistry.getAuditApplications();
    }
    /**
     * {@inheritDoc}
     * 
     * Note that if DEBUG is on for the the {@link #INBOUND_LOGGER}, then true
     * will always be returned.
     * 
     * @since 3.2
     */
    public boolean areAuditValuesRequired()
    {
        return (loggerInbound.isDebugEnabled()) ||
                (isAuditEnabled() && !auditModelRegistry.getAuditPathMapper().isEmpty());
    }
    /**
     * {@inheritDoc}
     * 
     * Note that if DEBUG is on for the the {@link #INBOUND_LOGGER}, then true
     * will always be returned.
     * 
     * @since 3.4
     */
    @Override
    public boolean areAuditValuesRequired(String path)
    {
        PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
        Set mappedPaths = pathMapper.getMappedPathsWithPartialMatch(path);
        return loggerInbound.isDebugEnabled() || mappedPaths.size() > 0;
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public boolean isAuditPathEnabled(String applicationName, String path)
    {
        ParameterCheck.mandatory("applicationName", applicationName);
        AlfrescoTransactionSupport.checkTransactionReadState(false);
        
        AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName);
        if (application == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("No audit application named '" + applicationName + "' has been registered.");
            }
            return false;
        }
        // Ensure that the path gets a valid value
        if (path == null)
        {
            path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey();
        }
        else
        {
            // Check the path against the application
            application.checkPath(path);
        }
        Set disabledPaths = getDisabledPaths(application);
        
        // Check if there are any entries that match or supercede the given path
        String disablingPath = null;;
        for (String disabledPath : disabledPaths)
        {
            if (path.startsWith(disabledPath))
            {
                disablingPath = disabledPath;
                break;
            }
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Audit path enabled check: \n" +
                    "   Application:    " + applicationName + "\n" +
                    "   Path:           " + path + "\n" +
                    "   Disabling Path: " + disablingPath);
        }
        return disablingPath == null;
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public void enableAudit(String applicationName, String path)
    {
        ParameterCheck.mandatory("applicationName", applicationName);
        AlfrescoTransactionSupport.checkTransactionReadState(true);
        
        AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName);
        if (application == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("No audit application named '" + applicationName + "' has been registered.");
            }
            return;
        }
        // Ensure that the path gets a valid value
        if (path == null)
        {
            path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey();
        }
        else
        {
            // Check the path against the application
            application.checkPath(path);
        }
        Long disabledPathsId = application.getDisabledPathsId();
        Set disabledPaths = getDisabledPaths(application);
        
        // Remove any paths that start with the given path
        boolean changed = false;
        Iterator iterateDisabledPaths = disabledPaths.iterator();
        while (iterateDisabledPaths.hasNext())
        {
            String disabledPath = iterateDisabledPaths.next();
            if (disabledPath.startsWith(path))
            {
                iterateDisabledPaths.remove();
                changed = true;
            }
        }
        // Persist, if necessary
        if (changed)
        {
            propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths);
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Audit disabled paths updated: \n" +
                        "   Application: " + applicationName + "\n" +
                        "   Disabled:    " + disabledPaths);
            }
        }
        // Done
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public void disableAudit(String applicationName, String path)
    {
        ParameterCheck.mandatory("applicationName", applicationName);
        AlfrescoTransactionSupport.checkTransactionReadState(true);
        
        AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName);
        if (application == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("No audit application named '" + applicationName + "' has been registered.");
            }
            return;
        }
        // Ensure that the path gets a valid value
        if (path == null)
        {
            path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey();
        }
        else
        {
            // Check the path against the application
            application.checkPath(path);
        }
        
        Long disabledPathsId = application.getDisabledPathsId();
        Set disabledPaths = getDisabledPaths(application);
        
        // Shortcut if the disabled paths contain the exact path
        if (disabledPaths.contains(path))
        {
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Audit disable path already present: \n" +
                        "   Path:       " + path);
            }
            return;
        }
        
        // Bring the set up to date by stripping out unwanted paths
        Iterator iterateDisabledPaths = disabledPaths.iterator();
        while (iterateDisabledPaths.hasNext())
        {
            String disabledPath = iterateDisabledPaths.next();
            if (disabledPath.startsWith(path))
            {
                // We will be superceding this
                iterateDisabledPaths.remove();
            }
            else if (path.startsWith(disabledPath))
            {
                // There is already a superceding path
                if (logger.isDebugEnabled())
                {
                    logger.debug(
                            "Audit disable path superceded: \n" +
                            "   Path:          " + path + "\n" +
                            "   Superceded by: " + disabledPath);
                }
                return;
            }
        }
        // Add our path in
        disabledPaths.add(path);
        // Upload the new set
        propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Audit disabled paths updated: \n" +
                    "   Application: " + applicationName + "\n" +
                    "   Disabled:    " + disabledPaths);
        }
    }
    /**
     * {@inheritDoc}
     * @since 3.2
     */
    public void resetDisabledPaths(String applicationName)
    {
        ParameterCheck.mandatory("applicationName", applicationName);
        AlfrescoTransactionSupport.checkTransactionReadState(true);
        
        AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName);
        if (application == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("No audit application named '" + applicationName + "' has been registered.");
            }
            return;
        }
        Long disabledPathsId = application.getDisabledPathsId();
        propertyValueDAO.updateProperty(disabledPathsId, (Serializable) Collections.emptySet());
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Removed all disabled paths for application " + applicationName);
        }
    }
    @Override
    public Map recordAuditValues(String rootPath, Map values)
    {
        return recordAuditValuesWithUserFilter(rootPath, values, true);
    }
    
    protected  T trimStringsIfNecessary (T values)
    {
        T processed;
        
        if (values instanceof MLText)
        {
            // need to treat MLText first because it is actually a HashMap
            Map localizedStrings = trimStringsIfNecessary((MLText)values);
            if (localizedStrings != values)
            {
                // processed so far is only defensive copy of a Map, not a MLText
                processed = (T)new MLText();
                ((MLText)processed).putAll(localizedStrings);
            }
            else
            {
                // no changes
                processed = values;
            }
        }
        else if (values instanceof Map, ?>)
        {
            processed = (T)trimStringsIfNecessary((Map, ?>)values);
        }
        else if (values instanceof List>)
        {
            // need to treat list specially to preserve order
            processed = (T)trimStringsIfNecessary((List>)values);
        }
        else if (values instanceof Collection>)
        {
            // any other collection treated as unordered with no guarantee processed data will be in same order
            processed = (T)trimStringsIfNecessary((Collection>)values);
        }
        else if (values instanceof String)
        {
            processed = (T)SchemaBootstrap.trimStringForTextFields((String) values);
        }
        else 
        {
            // don't know how to process
            processed = values;
        }
        
        return processed;
    }
    
    private  List trimStringsIfNecessary (List values)
    {
        List processed = values;
        
        int idx = 0;
        for (V auditValue : values)
        {
            if (auditValue != null )
            {
                V processedAuditValue = trimStringsIfNecessary(auditValue);
                
                if (processedAuditValue != auditValue && !auditValue.equals(processedAuditValue))
                {
                    if (processed == values)
                    {
                        // defensive copy
                        processed = new ArrayList(values);
                    }
                    processed.set(idx, processedAuditValue);
                }
            }
            
            idx++;
        }
        
        return processed;
    }
    
    private  Collection trimStringsIfNecessary (Collection values)
    {
        Collection processed = values;
        
        for (V auditValue : values)
        {
            if (auditValue != null )
            {
                V processedAuditValue = trimStringsIfNecessary(auditValue);
                
                if (processedAuditValue != auditValue && !auditValue.equals(processedAuditValue))
                {
                    if (processed == values)
                    {
                        // defensive copy
                        processed = new HashSet(values);
                    }
                    processed.remove(auditValue);
                    processed.add(processedAuditValue);
                }
            }
        }
        
        return processed;
    }
    
    private  Map trimStringsIfNecessary (Map values)
    {
        Map processed = values;
        
        for (Map.Entry entry : values.entrySet())
        {
            V auditValue = entry.getValue();
            
            if (auditValue != null )
            {
                V processedAuditValue = trimStringsIfNecessary(auditValue);
                
                if (processedAuditValue != auditValue && !auditValue.equals(processedAuditValue))
                {
                    if (processed == values)
                    {
                        // defensive copy
                        processed = new HashMap(values);
                    }
                    processed.put(entry.getKey(), processedAuditValue);
                }
            }
        }
        
        return processed;
    }
    
    @Override
    public Map recordAuditValuesWithUserFilter(String rootPath, Map values, boolean useUserFilter)
    {
        ParameterCheck.mandatory("rootPath", rootPath);
        AuditApplication.checkPathFormat(rootPath);
        
        String username = AuthenticationUtil.getFullyAuthenticatedUser();
        if (values == null || values.isEmpty() || !areAuditValuesRequired()
                || !(userAuditFilter.acceptUser(username) || !useUserFilter) || !auditFilter.accept(rootPath, values))
        {
            return Collections.emptyMap();
        }
        
        // MNT-12196
        values = trimStringsIfNecessary(values);
        
        // Log inbound values
        if (loggerInbound.isDebugEnabled())
        {
            StringBuilder sb = new StringBuilder(values.size()*64);
            sb.append("\n")
              .append("Inbound audit values:");
            for (Map.Entry entry : values.entrySet())
            {
                String pathElement = entry.getKey();
                String path = AuditApplication.buildPath(rootPath, pathElement);
                Serializable value = entry.getValue();
                sb.append("\n\t").append(path).append("=").append(value);
            }
            loggerInbound.debug(sb.toString());
        }
        // Build the key paths using the session root path
        Map pathedValues = new HashMap(values.size() * 2);
        for (Map.Entry entry : values.entrySet())
        {
            String pathElement = entry.getKey();
            String path = AuditApplication.buildPath(rootPath, pathElement);
            pathedValues.put(path, entry.getValue());
        }
        
        // Translate the values map
        PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
        final Map mappedValues = pathMapper.convertMap(pathedValues);
        if (mappedValues.isEmpty())
        {
            return mappedValues;
        }
        
        // We have something to record.  Start a transaction, if necessary
        TxnReadState txnState = AlfrescoTransactionSupport.getTransactionReadState();
        switch (txnState)
        {
        case TXN_NONE:
        case TXN_READ_ONLY:
            // New transaction
            RetryingTransactionCallback