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

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
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.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.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.
 * <p>
 * To turn on logging of all <i>potentially auditable</i> data, turn on logging for:<br/>
 * <strong>{@link #INBOUND_LOGGER org.alfresco.repo.audit.inbound}</strong>.
 * <p/>
 * 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)
    {
        Long applicationId = getApplicationId(applicationName);
        if (applicationId == null)
        {
            return 0;
        }

        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 5.2.2
     */
    public int deleteAuditEntriesByIdRange(String applicationName, Long fromId, Long toId)
    {
        Long applicationId = getApplicationId(applicationName);
        if (applicationId == null)
        {
            return 0;
        }

        int deleted = auditDAO.deleteAuditEntriesByIdRange(applicationId, fromId, toId);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Delete audit " + deleted + " entries for " + applicationName +
                            " (" + fromId + " to " + toId);
        }
        return deleted;
    }

    private Long getApplicationId(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 null;
        }

        return application.getApplicationId();
    }

    /**
     * {@inheritDoc}
     * @since 3.2
     */
    @Override
    public int deleteAuditEntries(List<Long> 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<String> getDisabledPaths(AuditApplication application)
    {
        try
        {
            Long disabledPathsId = application.getDisabledPathsId();
            Set<String> disabledPaths = (Set<String>) propertyValueDAO.getPropertyById(disabledPathsId);
            return new HashSet<String>(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<String, AuditApplication> getAuditApplications()
    {
        return auditModelRegistry.getAuditApplications();
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Note that if DEBUG is on for the the {@link #INBOUND_LOGGER}, then <tt>true</tt>
     * will always be returned.
     * 
     * @since 3.2
     */
    public boolean areAuditValuesRequired()
    {
        return (loggerInbound.isDebugEnabled()) ||
                (isAuditEnabled() && !auditModelRegistry.getAuditPathMapper().isEmpty());
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Note that if DEBUG is on for the the {@link #INBOUND_LOGGER}, then <tt>true</tt>
     * will always be returned.
     * 
     * @since 3.4
     */
    @Override
    public boolean areAuditValuesRequired(String path)
    {
        PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
        Set<String> 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<String> 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<String> disabledPaths = getDisabledPaths(application);
        
        // Remove any paths that start with the given path
        boolean changed = false;
        Iterator<String> 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<String> 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<String> 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<String, Serializable> recordAuditValues(String rootPath, Map<String, Serializable> values)
    {
        return recordAuditValuesWithUserFilter(rootPath, values, true);
    }

    @Override
    public Map<String, Serializable> recordAuditValuesWithUserFilter(String rootPath, Map<String, Serializable> 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();
        }
        
        // Log inbound values
        if (loggerInbound.isDebugEnabled())
        {
            StringBuilder sb = new StringBuilder(values.size()*64);
            sb.append("\n")
              .append("Inbound audit values:");
            for (Map.Entry<String, Serializable> 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<String, Serializable> pathedValues = new HashMap<String, Serializable>(values.size() * 2);
        for (Map.Entry<String, Serializable> 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<String, Serializable> 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<Map<String, Serializable>> callback =
                    new RetryingTransactionCallback<Map<String,Serializable>>()
            {
                public Map<String, Serializable> execute() throws Throwable
                {
                    return recordAuditValuesImpl(mappedValues);
                }
            };
            RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
            txnHelper.setForceWritable(true);
            return txnHelper.doInTransaction(callback, false, true);
        case TXN_READ_WRITE:
            return recordAuditValuesImpl(mappedValues);
        default:
            throw new IllegalStateException("Unknown txn state: " + txnState);
        }
    }
    
    /**
     * @since 3.2
     */
    public Map<String, Serializable> recordAuditValuesImpl(Map<String, Serializable> mappedValues)
    {
        // Group the values by root path
        Map<String, Map<String, Serializable>> mappedValuesByRootKey = new HashMap<String, Map<String,Serializable>>();
        for (Map.Entry<String, Serializable> entry : mappedValues.entrySet())
        {
            String path = entry.getKey();
            String rootKey = AuditApplication.getRootKey(path);
            Map<String, Serializable> rootKeyMappedValues = mappedValuesByRootKey.get(rootKey);
            if (rootKeyMappedValues == null)
            {
                rootKeyMappedValues = new HashMap<String, Serializable>(7);
                mappedValuesByRootKey.put(rootKey, rootKeyMappedValues);
            }
            rootKeyMappedValues.put(path, entry.getValue());
        }

        Map<String, Serializable> allAuditedValues = new HashMap<String, Serializable>(mappedValues.size()*2+1);
        // Now audit for each of the root keys
        for (Map.Entry<String, Map<String, Serializable>> entry : mappedValuesByRootKey.entrySet())
        {
            String rootKey = entry.getKey();
            Map<String, Serializable> rootKeyMappedValues = entry.getValue();
            // Get the application
            AuditApplication application = auditModelRegistry.getAuditApplicationByKey(rootKey);
            if (application == null)
            {
                // There is no application that uses the root key
                logger.debug(
                        "There is no application for root key: " + rootKey);
                continue;
            }
            // Get the disabled paths
            Set<String> disabledPaths = getDisabledPaths(application);
            // Do a quick elimination if the root path is disabled
            if (disabledPaths.contains(AuditApplication.buildPath(rootKey)))
            {
                // The root key has been disabled for this application
                if (logger.isDebugEnabled())
                {
                    logger.debug(
                            "Audit values root path has been excluded by disabled paths: \n" +
                            "   Application: " + application + "\n" +
                            "   Root Path:   " + AuditApplication.buildPath(rootKey));
                }
                continue;
            }
            // Do the audit
            Map<String, Serializable> rootKeyAuditValues = audit(application, disabledPaths, rootKeyMappedValues);
            allAuditedValues.putAll(rootKeyAuditValues);
        }
        // Done
        return allAuditedValues;
    }

    /**
     * Audit values for a given application.  No path checking is done.
     * 
     * @param application           the audit application to audit to
     * @param disabledPaths         the application's disabled paths
     * @param values                the values to store keyed by <b>full paths</b>.
     * @return                      Returns all values as audited
     */
    private Map<String, Serializable> audit(
            final AuditApplication application,
            Set<String> disabledPaths,
            final Map<String, Serializable> values)
    {
        // Get the model ID for the application
        Long applicationId = application.getApplicationId();
        if (applicationId == null)
        {
            throw new AuditException("No persisted instance exists for audit application: " + application);
        }

        // Check if there is anything to audit
        if (values.size() == 0)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Audit values have all been excluded by disabled paths: \n" +
                        "   Application: " + application + "\n" +
                        "   Values:      " + values);
            }
            return Collections.emptyMap();
        }

        Set<String> generatorKeys = values.keySet();
        // Eliminate any paths that have been disabled
        Iterator<String> generatorKeysIterator = generatorKeys.iterator();
        while(generatorKeysIterator.hasNext())
        {
            String generatorKey = generatorKeysIterator.next();
            for (String disabledPath : disabledPaths)
            {
                if (generatorKey.startsWith(disabledPath))
                {
                    // The pathed value is excluded
                    generatorKeysIterator.remove();
                }
            }
        }
        
        // Generate data
        Map<String, DataGenerator> generators = application.getDataGenerators(generatorKeys);
        Map<String, Serializable> auditData = generateData(generators);

        // Now extract values
        Map<String, Serializable> extractedData = AuthenticationUtil.runAs(new RunAsWork<Map<String, Serializable>>()
        {
            public Map<String, Serializable> doWork() throws Exception
            {
                return extractData(application, values);
            }
        }, AuthenticationUtil.getSystemUserName());

        // Combine extracted and generated values (extracted data takes precedence)
        auditData.putAll(extractedData);

        // Filter data
        // See MNT-14136, MNT-8401
        for (Map.Entry<String, Serializable> value : auditData.entrySet())
        {
            String root = value.getKey();
            int index = root.lastIndexOf("/");
            Map<String, Serializable> argc = new HashMap<String, Serializable>(1);
            argc.put(root.substring(index, root.length()).substring(1), value.getValue());
            if (!auditFilter.accept(root.substring(0, index), argc))
            {
                return Collections.emptyMap();
            }
        }

        // Time and username are intrinsic
        long time = System.currentTimeMillis();
        String username = AuthenticationUtil.getFullyAuthenticatedUser();
        
        Long entryId = null;
        if (!auditData.isEmpty())
        {
            // Persist the values (if not just gathering data in a pre call for use in a post call)
            boolean justGatherPreCallData = application.isApplicationJustGeneratingPreCallData();
            if (!justGatherPreCallData)
            {
                entryId = auditDAO.createAuditEntry(applicationId, time, username, auditData);
            }
            // Done
            if (logger.isDebugEnabled())
            {
                StringBuilder sb = new StringBuilder();
                sb.append(
                        ((justGatherPreCallData) ? "\nPreCallData: \n" : "\nNew audit entry: \n") +
                        "\tApplication ID: " + applicationId + "\n" +
                        ((justGatherPreCallData) ? "" : "\tEntry ID:       " + entryId + "\n") +
                        "\tValues:         " + "\n");
                for (Map.Entry<String, Serializable> entry : values.entrySet())
                {
                    sb.append("\t\t").append(entry).append("\n");
                }
                sb.append("\n\tAudit Data: \n");
                for (Map.Entry<String, Serializable> entry : auditData.entrySet())
                {
                    sb.append("\t\t").append(entry).append("\n");
                }
                logger.debug(sb.toString());
            }
        }
        else
        {
            // Done ... nothing
            if (logger.isDebugEnabled())
            {
                StringBuilder sb = new StringBuilder();
                sb.append(
                        "\nNothing audited: \n" +
                        "\tApplication ID: " + applicationId + "\n" +
                        "\tEntry ID:       " + entryId + "\n" +
                        "\tValues:         " + "\n");
                for (Map.Entry<String, Serializable> entry : values.entrySet())
                {
                    sb.append("\t\t").append(entry).append("\n");
                }
                logger.debug(sb.toString());
            }
        }
        
        return auditData;
    }
    
    /**
     * Extracts data from a given map using data extractors from the given application.
     * 
     * @param application           the application providing the data extractors
     * @param values                the data values from which to generate data
     * @return                      Returns a map of derived data keyed by full path
     * 
     * @since 3.2
     */
    private Map<String, Serializable> extractData(
            AuditApplication application,
            Map<String, Serializable> values)
    {
        Map<String, Serializable> newData = new HashMap<String, Serializable>(values.size());
        
        List<DataExtractorDefinition> extractors = application.getDataExtractors();
        for (DataExtractorDefinition extractorDef : extractors)
        {
            DataExtractor extractor = extractorDef.getDataExtractor();
            String triggerPath = extractorDef.getDataTrigger();
            String sourcePath = extractorDef.getDataSource();
            String targetPath = extractorDef.getDataTarget();
            
            // Check if it is triggered
            if (!values.containsKey(triggerPath))
            {
                continue;               // It is not triggered
            }
            
            // We observe the key, not the actual value
            if (!values.containsKey(sourcePath))
            {
                continue;               // There is no data to extract
            }
            
            Serializable value = values.get(sourcePath);
            
            // Check if the extraction is supported
            if (!extractor.isSupported(value))
            {
                continue;
            }
            // Use the extractor to pull the value out
            final Serializable data;
            try
            {
                data = extractor.extractData(value);
            }
            catch (Throwable e)
            {
                throw new AlfrescoRuntimeException(
                        "Failed to extract audit data: \n" +
                        "   Path:      " + sourcePath + "\n" +
                        "   Raw value: " + value + "\n" +
                        "   Extractor: " + extractor,
                        e);
            }
            // Add it to the map
            newData.put(targetPath, data);
        }
        // Done
        if (logger.isDebugEnabled())
        {
            StringBuilder sb = new StringBuilder();
            sb.append(
                    "\nExtracted audit data: \n" +
                    "\tApplication:    " + application + "\n" +
                    "\tValues:         " + "\n");
            for (Map.Entry<String, Serializable> entry : values.entrySet())
            {
                sb.append("\t\t").append(entry).append("\n");
            }
            sb.append("\n\tNew Data: \n");
            for (Map.Entry<String, Serializable> entry : newData.entrySet())
            {
                sb.append("\t\t").append(entry).append("\n");
            }
            logger.debug(sb.toString());
        }
        return newData;
    }
    
    /**
     * @param generators            the data generators
     * @return                      Returns a map of generated data keyed by full path
     * 
     * @since 3.2
     */
    private Map<String, Serializable> generateData(Map<String, DataGenerator> generators)
    {
        Map<String, Serializable> newData = new HashMap<String, Serializable>(generators.size() + 5);
        for (Map.Entry<String, DataGenerator> entry : generators.entrySet())
        {
            String path = entry.getKey();
            DataGenerator generator = entry.getValue();
            final Serializable data;
            try
            {
                data = generator.getData();
            }
            catch (Throwable e)
            {
                throw new AlfrescoRuntimeException(
                        "Failed to generate audit data: \n" +
                        "   Path:      " + path + "\n" +
                        "   Generator: " + generator,
                        e);
            }
            // Add it to the map
            newData.put(path, data);
        }
        // Done
        return newData;
    }

    /**
     * {@inheritDoc}
     */
    public void auditQuery(AuditQueryCallback callback, AuditQueryParameters parameters, int maxResults)
    {
        ParameterCheck.mandatory("callback", callback);
        ParameterCheck.mandatory("parameters", parameters);
        
        // Shortcuts
        if (parameters.isZeroResultQuery())
        {
            return;
        }
        
        auditDAO.findAuditEntries(callback, parameters, maxResults);
    }

    /**
     * {@inheritDoc}
     */
    public HashMap<String, Long> getAuditMinMaxByApp(String applicationName, List<String> extremes)
    {
        // Get the id for the application
        AuditApplication app = auditModelRegistry.getAuditApplicationByName(applicationName);
        Long applicationId = app.getApplicationId();
        if (applicationId == null)
        {
            throw new AuditException("No persisted instance exists for audit application: " + app);
        }

        return auditDAO.getAuditMinMaxByApp(applicationId, extremes);
    }
}
