org.alfresco.repo.audit.AuditComponentImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.audit.AuditComponentImpl.java

Source

/*
 * #%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.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.
 * <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) {
        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<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);
    }

    protected <T> T trimStringsIfNecessary(T values) {
        T processed;

        if (values instanceof MLText) {
            // need to treat MLText first because it is actually a HashMap
            Map<Locale, String> 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 <V> List<V> trimStringsIfNecessary(List<V> values) {
        List<V> 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<V>(values);
                    }
                    processed.set(idx, processedAuditValue);
                }
            }

            idx++;
        }

        return processed;
    }

    private <V> Collection<V> trimStringsIfNecessary(Collection<V> values) {
        Collection<V> 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<V>(values);
                    }
                    processed.remove(auditValue);
                    processed.add(processedAuditValue);
                }
            }
        }

        return processed;
    }

    private <K, V> Map<K, V> trimStringsIfNecessary(Map<K, V> values) {
        Map<K, V> processed = values;

        for (Map.Entry<K, V> 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<K, V>(values);
                    }
                    processed.put(entry.getKey(), processedAuditValue);
                }
            }
        }

        return processed;
    }

    @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();
        }

        // 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<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);
    }
}