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}
* @since 3.4
*/
@Override
public boolean areAuditValuesRequired(String path)
{
PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
Set mappedPaths = pathMapper.getMappedPathsWithPartialMatch(path);
return 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