/*
* #%L
* Alfresco Records Management Module
* %%
* 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.module.org_alfresco_module_rm.audit;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction;
import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService;
import org.alfresco.module.org_alfresco_module_rm.audit.event.AuditEvent;
import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
import org.alfresco.repo.audit.AuditComponent;
import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.transaction.TransactionListenerAdapter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.extensions.surf.util.I18NUtil;
import org.springframework.extensions.surf.util.ISO8601DateFormat;
import org.springframework.extensions.surf.util.ParameterCheck;
/**
* Records Management Audit Service Implementation.
*
* @author Gavin Cornwell
* @since 3.2
*/
public class RecordsManagementAuditServiceImpl extends AbstractLifecycleBean
implements RecordsManagementAuditService
{
/** I18N */
private static final String MSG_TRAIL_FILE_FAIL = "rm.audit.trail-file-fail";
private static final String MSG_AUDIT_REPORT = "rm.audit.audit-report";
/** Logger */
private static Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class);
private static final String ACCESS_AUDIT_CAPABILITY = "AccessAudit";
private static final String KEY_RM_AUDIT_NODE_RECORDS = "RMAUditNodeRecords";
protected static final String RM_AUDIT_EVENT_LOGIN_SUCCESS = "Login.Success";
protected static final String RM_AUDIT_EVENT_LOGIN_FAILURE = "Login.Failure";
protected static final String RM_AUDIT_APPLICATION_NAME = "RM";
protected static final String RM_AUDIT_PATH_ROOT = "/RM";
protected static final String RM_AUDIT_SNIPPET_EVENT = "/event";
protected static final String RM_AUDIT_SNIPPET_PERSON = "/person";
protected static final String RM_AUDIT_SNIPPET_NAME = "/name";
protected static final String RM_AUDIT_SNIPPET_NODE = "/node";
protected static final String RM_AUDIT_SNIPPET_CHANGES = "/changes";
protected static final String RM_AUDIT_SNIPPET_BEFORE = "/before";
protected static final String RM_AUDIT_SNIPPET_AFTER = "/after";
protected static final String RM_AUDIT_DATA_PERSON_FULLNAME = "/RM/event/person/fullName";
protected static final String RM_AUDIT_DATA_PERSON_ROLES = "/RM/event/person/roles";
protected static final String RM_AUDIT_DATA_EVENT_NAME = "/RM/event/name/value";
protected static final String RM_AUDIT_DATA_NODE_NODEREF = "/RM/event/node/noderef";
protected static final String RM_AUDIT_DATA_NODE_NAME = "/RM/event/node/name";
protected static final String RM_AUDIT_DATA_NODE_TYPE = "/RM/event/node/type";
protected static final String RM_AUDIT_DATA_NODE_IDENTIFIER = "/RM/event/node/identifier";
protected static final String RM_AUDIT_DATA_NODE_NAMEPATH = "/RM/event/node/namePath";
protected static final String RM_AUDIT_DATA_NODE_CHANGES_BEFORE = "/RM/event/node/changes/before/value";
protected static final String RM_AUDIT_DATA_NODE_CHANGES_AFTER = "/RM/event/node/changes/after/value";
protected static final String RM_AUDIT_DATA_LOGIN_USERNAME = "/RM/login/args/userName/value";
protected static final String RM_AUDIT_DATA_LOGIN_FULLNAME = "/RM/login/no-error/fullName";
protected static final String RM_AUDIT_DATA_LOGIN_ERROR = "/RM/login/error/value";
/* Provide Backward compatibility with DOD5015 Audit Events RM-904*/
protected static final String DOD5015_AUDIT_APPLICATION_NAME = "DOD5015";
protected static final String DOD5015_AUDIT_PATH_ROOT = "/DOD5015";
protected static final String DOD5015_AUDIT_SNIPPET_EVENT = "/event";
protected static final String DOD5015_AUDIT_SNIPPET_PERSON = "/person";
protected static final String DOD5015_AUDIT_SNIPPET_NAME = "/name";
protected static final String DOD5015_AUDIT_SNIPPET_NODE = "/node";
protected static final String DOD5015_AUDIT_SNIPPET_CHANGES = "/changes";
protected static final String DOD5015_AUDIT_SNIPPET_BEFORE = "/before";
protected static final String DOD5015_AUDIT_SNIPPET_AFTER = "/after";
protected static final String DOD5015_AUDIT_DATA_PERSON_FULLNAME = "/DOD5015/event/person/fullName";
protected static final String DOD5015_AUDIT_DATA_PERSON_ROLES = "/DOD5015/event/person/roles";
protected static final String DOD5015_AUDIT_DATA_EVENT_NAME = "/DOD5015/event/name/value";
protected static final String DOD5015_AUDIT_DATA_NODE_NODEREF = "/DOD5015/event/node/noderef";
protected static final String DOD5015_AUDIT_DATA_NODE_NAME = "/DOD5015/event/node/name";
protected static final String DOD5015_AUDIT_DATA_NODE_TYPE = "/DOD5015/event/node/type";
protected static final String DOD5015_AUDIT_DATA_NODE_IDENTIFIER = "/DOD5015/event/node/identifier";
protected static final String DOD5015_AUDIT_DATA_NODE_NAMEPATH = "/DOD5015/event/node/namePath";
protected static final String DOD5015_AUDIT_DATA_NODE_CHANGES_BEFORE = "/DOD5015/event/node/changes/before/value";
protected static final String DOD5015_AUDIT_DATA_NODE_CHANGES_AFTER = "/DOD5015/event/node/changes/after/value";
protected static final String DOD5015_AUDIT_DATA_LOGIN_USERNAME = "/DOD5015/login/args/userName/value";
protected static final String DOD5015_AUDIT_DATA_LOGIN_FULLNAME = "/DOD5015/login/no-error/fullName";
protected static final String DOD5015_AUDIT_DATA_LOGIN_ERROR = "/DOD5015/login/error/value";
/* End Backward compatibility with DOD5015 Audit Events */
protected static final String AUDIT_TRAIL_FILE_PREFIX = "audit_";
protected static final String AUDIT_TRAIL_JSON_FILE_SUFFIX = ".json";
protected static final String AUDIT_TRAIL_HTML_FILE_SUFFIX = ".html";
/** Audit auditing events */
private static final String AUDIT_EVENT_START = "audit.start";
private static final String MSG_AUDIT_START = "rm.audit.audit-start";
private static final String AUDIT_EVENT_STOP = "audit.stop";
private static final String MSG_AUDIT_STOP = "rm.audit.audit-stop";
private static final String AUDIT_EVENT_CLEAR = "audit.clear";
private static final String MSG_AUDIT_CLEAR = "rm.audit.audit-clear";
private static final String AUDIT_EVENT_VIEW = "audit.view";
private static final String MSG_AUDIT_VIEW = "rm.audit.audit-view";
private PolicyComponent policyComponent;
private DictionaryService dictionaryService;
private TransactionService transactionService;
private NodeService nodeService;
private ContentService contentService;
private AuditComponent auditComponent;
private AuditService auditService;
private RecordsManagementActionService rmActionService;
private FilePlanService filePlanService;
private NamespaceService namespaceService;
protected CapabilityService capabilityService;
private boolean shutdown = false;
private List<String> ignoredAuditProperties;
private List<QName> propertiesToBeRemoved = new ArrayList<QName>();
private RMAuditTxnListener txnListener = new RMAuditTxnListener();
/** Registered and initialised records management auditEvents */
private Map<String, AuditEvent> auditEvents = new HashMap<String, AuditEvent>();
/**
* Set the component used to bind to behaviour callbacks
*/
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
/**
* Provides user-readable names for types
*/
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
/**
* Set the component used to start new transactions
*/
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
/**
* Sets the NodeService instance
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* Sets the ContentService instance
*/
public void setContentService(ContentService contentService)
{
this.contentService = contentService;
}
/**
* The component to create audit events
*/
public void setAuditComponent(AuditComponent auditComponent)
{
this.auditComponent = auditComponent;
}
/**
* Sets the AuditService instance
*/
public void setAuditService(AuditService auditService)
{
this.auditService = auditService;
}
/**
* Sets the RecordsManagementActionService instance
*/
public void setRecordsManagementActionService(RecordsManagementActionService rmActionService)
{
this.rmActionService = rmActionService;
}
/**
* @param filePlanService file plan service
*/
public void setFilePlanService(FilePlanService filePlanService)
{
this.filePlanService = filePlanService;
}
/**
* @param namespaceService namespace service
*/
public void setNamespaceService(NamespaceService namespaceService)
{
this.namespaceService = namespaceService;
}
/**
* @param capabilityService capability service
*/
public void setCapabilityService(CapabilityService capabilityService)
{
this.capabilityService = capabilityService;
}
/**
* @param ignoredAuditProperties
*/
public void setIgnoredAuditProperties(List<String> ignoredAuditProperties)
{
this.ignoredAuditProperties = ignoredAuditProperties;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#registerAuditEvent(java.lang.String, java.lang.String)
*/
@Override
public void registerAuditEvent(String name, String label)
{
registerAuditEvent(new AuditEvent(name, label));
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#registerAuditEvent(org.alfresco.module.org_alfresco_module_rm.audit.event.AuditEvent)
*/
@Override
public void registerAuditEvent(AuditEvent auditEvent)
{
if (logger.isDebugEnabled())
{
logger.debug("Registering audit event " + auditEvent.getName());
}
this.auditEvents.put(auditEvent.getName(), auditEvent);
}
/**
* Checks that all necessary properties have been set.
*/
public void init()
{
PropertyCheck.mandatory(this, "policyComponent", policyComponent);
PropertyCheck.mandatory(this, "transactionService", transactionService);
PropertyCheck.mandatory(this, "nodeService", nodeService);
PropertyCheck.mandatory(this, "contentService", contentService);
PropertyCheck.mandatory(this, "auditComponent", auditComponent);
PropertyCheck.mandatory(this, "auditService", auditService);
PropertyCheck.mandatory(this, "rmActionService", rmActionService);
PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
PropertyCheck.mandatory(this, "filePlanService", filePlanService);
// register audit auditing events
registerAuditEvent(AUDIT_EVENT_CLEAR, MSG_AUDIT_CLEAR);
registerAuditEvent(AUDIT_EVENT_START, MSG_AUDIT_START);
registerAuditEvent(AUDIT_EVENT_STOP, MSG_AUDIT_STOP);
registerAuditEvent(AUDIT_EVENT_VIEW, MSG_AUDIT_VIEW);
// properties to be ignored by audit
for (String qname : ignoredAuditProperties)
{
this.propertiesToBeRemoved.add(QName.createQName(qname, this.namespaceService));
}
}
/**
* @see org.springframework.extensions.surf.util.AbstractLifecycleBean#onBootstrap(org.springframework.context.ApplicationEvent)
*/
@Override
protected void onBootstrap(ApplicationEvent event)
{
shutdown = false;
}
/**
* @see org.springframework.extensions.surf.util.AbstractLifecycleBean#onShutdown(org.springframework.context.ApplicationEvent)
*/
@Override
protected void onShutdown(ApplicationEvent event)
{
shutdown = true;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#isAuditLogEnabled(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public boolean isAuditLogEnabled(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
return auditService.isAuditEnabled(
RM_AUDIT_APPLICATION_NAME,
RM_AUDIT_PATH_ROOT);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#startAuditLog(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public void startAuditLog(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
auditService.enableAudit(
RM_AUDIT_APPLICATION_NAME,
RM_AUDIT_PATH_ROOT);
if (logger.isInfoEnabled())
{
logger.info("Started Records Management auditing");
}
auditEvent(filePlan, AUDIT_EVENT_START, null, null, true);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#stopAuditLog(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public void stopAuditLog(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
auditEvent(filePlan, AUDIT_EVENT_STOP, null, null, true);
auditService.disableAudit(
RM_AUDIT_APPLICATION_NAME,
RM_AUDIT_PATH_ROOT);
if (logger.isInfoEnabled())
{
logger.info("Stopped Records Management auditing");
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#clearAuditLog(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public void clearAuditLog(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
auditService.clearAudit(RM_AUDIT_APPLICATION_NAME, null, null);
if (logger.isInfoEnabled())
{
logger.debug("Records Management audit log has been cleared");
}
auditEvent(filePlan, AUDIT_EVENT_CLEAR, null, null, true);
}
/**
* {@inheritDoc}
*/
@Override
public Date getDateAuditLogLastStarted(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
// TODO: return proper date, for now it's today's date
return getStartOfDay(new Date());
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#getDateAuditLogLastStopped(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public Date getDateAuditLogLastStopped(NodeRef filePlan)
{
ParameterCheck.mandatory("filePlan", filePlan);
// TODO use file plan to scope audit log
// TODO: return proper date, for now it's today's date
return getEndOfDay(new Date());
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#auditEvent(org.alfresco.service.cmr.repository.NodeRef, java.lang.String)
*/
@Override
public void auditEvent(NodeRef nodeRef, String eventName)
{
auditEvent(nodeRef, eventName, null, null, false, false);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#auditEvent(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map, java.util.Map)
*/
@Override
public void auditEvent(NodeRef nodeRef, String eventName, Map<QName, Serializable> before, Map<QName, Serializable> after)
{
auditEvent(nodeRef, eventName, before, after, false, false);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#auditEvent(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map, java.util.Map, boolean)
*/
@Override
public void auditEvent(NodeRef nodeRef, String eventName, Map<QName, Serializable> before, Map<QName, Serializable> after, boolean immediate)
{
auditEvent(nodeRef, eventName, before, after, immediate, false);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService#auditEvent(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map, java.util.Map, boolean)
*/
@Override
public void auditEvent(NodeRef nodeRef, String eventName, Map<QName, Serializable> before, Map<QName, Serializable> after, boolean immediate, boolean removeIfNoPropertyChanged)
{
// deal with immediate auditing if required
if (immediate)
{
Map<String, Serializable> auditMap = buildAuditMap(nodeRef, eventName, before, after, removeIfNoPropertyChanged);
auditComponent.recordAuditValues(RM_AUDIT_PATH_ROOT, auditMap);
}
else
{
Set<RMAuditNode> auditDetails = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS);
AlfrescoTransactionSupport.bindListener(txnListener);
// RM-936: Eliminate multiple audit maps from being generated when events with the same name are required to be fired multiple times in the same transaction.
// Check if auditDetails already contains an auditedNode with the same combination of nodeRef and eventName.
boolean auditNodeAlreadyExists = false;
for (RMAuditNode existingRMAuditNode : auditDetails)
{
if (existingRMAuditNode.getNodeRef().equals(nodeRef) && existingRMAuditNode.getEventName().equals(eventName))
{
// If there exists such an auditNode, update its 'after' properties with the latest set of properties and leave its 'before' properties unchanged so that it
// retains the original set of properties. The first 'before' and last 'after' will be diff'ed when comes to building the auditMap later when the transaction
// commits.
existingRMAuditNode.setNodePropertiesAfter(after);
auditNodeAlreadyExists = true;
break;
}
}
if (!auditNodeAlreadyExists)
{
// Create a new auditNode if it doesn't already exist
RMAuditNode auditedNode = new RMAuditNode();
auditedNode.setNodeRef(nodeRef);
auditedNode.setEventName(eventName);
auditedNode.setNodePropertiesBefore(before);
auditedNode.setNodePropertiesAfter(after);
auditedNode.setRemoveIfNoPropertyChanged(removeIfNoPropertyChanged);
auditDetails.add(auditedNode);
}
}
}
/**
* Helper method to build audit map
*
* @param nodeRef
* @param eventName
* @return
* @since 2.0.3
*/
private Map<String, Serializable> buildAuditMap(NodeRef nodeRef, String eventName, Map<QName, Serializable> propertiesBefore, Map<QName, Serializable> propertiesAfter, boolean removeOnNoPropertyChange)
{
Map<String, Serializable> auditMap = new HashMap<String, Serializable>(13);
auditMap.put(
AuditApplication.buildPath(
RM_AUDIT_SNIPPET_EVENT,
RM_AUDIT_SNIPPET_NAME),
eventName);
if (nodeRef != null)
{
auditMap.put(
AuditApplication.buildPath(
RM_AUDIT_SNIPPET_EVENT,
RM_AUDIT_SNIPPET_NODE),
nodeRef);
}
// Filter out any properties to be audited if specified in the Spring configuration.
if (!ignoredAuditProperties.isEmpty())
{
removeAuditProperties(propertiesBefore, propertiesAfter);
}
// Property changes
Pair<Map<QName, Serializable>, Map<QName, Serializable>> deltaPair = PropertyMap.getBeforeAndAfterMapsForChanges(propertiesBefore, propertiesAfter);
// If both the first and second Map in the deltaPair are empty and removeOnNoPropertyChange is true, the entire auditMap is discarded so it won't be audited.
if (deltaPair.getFirst().isEmpty() && deltaPair.getSecond().isEmpty() && removeOnNoPropertyChange)
{
auditMap.clear();
}
else
{
auditMap.put(
AuditApplication.buildPath(
RM_AUDIT_SNIPPET_EVENT,
RM_AUDIT_SNIPPET_NODE,
RM_AUDIT_SNIPPET_CHANGES,
RM_AUDIT_SNIPPET_BEFORE),
(Serializable) deltaPair.getFirst());
auditMap.put(
AuditApplication.buildPath(
RM_AUDIT_SNIPPET_EVENT,
RM_AUDIT_SNIPPET_NODE,
RM_AUDIT_SNIPPET_CHANGES,
RM_AUDIT_SNIPPET_AFTER),
(Serializable) deltaPair.getSecond());
}
return auditMap;
}
/**
* Helper method to remove system properties from maps
*
* @param properties
*/
private void removeAuditProperties(Map<QName, Serializable> before, Map<QName, Serializable> after)
{
if (before != null)
{
before.keySet().removeAll(this.propertiesToBeRemoved);
}
if (after != null)
{
after.keySet().removeAll(this.propertiesToBeRemoved);
}
}
/**
* A <b>stateless</b> transaction listener for RM auditing. This component picks up the data of modified nodes and generates the audit information.
* <p/>
* This class is not static so that the instances will have access to the action's implementation.
*
* @author Derek Hulley
* @since 3.2
*/
private class RMAuditTxnListener extends TransactionListenerAdapter
{
private final Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class);
/*
* Equality and hashcode generation are left unimplemented; we expect to only have a single
* instance of this class per action.
*/
/**
* Get the action parameters from the transaction and audit them.
*/
@Override
public void afterCommit()
{
final Set<RMAuditNode> auditedNodes = TransactionalResourceHelper.getSet(KEY_RM_AUDIT_NODE_RECORDS);
// Start a *new* read-write transaction to audit in
RetryingTransactionCallback<Void> auditCallback = new RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
auditInTxn(auditedNodes);
return null;
}
};
transactionService.getRetryingTransactionHelper().doInTransaction(auditCallback, false, true);
}
/**
* Do the actual auditing, assuming the presence of a viable transaction
*
* @param auditedNodes details of the nodes that were modified
*/
private void auditInTxn(Set<RMAuditNode> auditedNodes) throws Throwable
{
// Go through all the audit information and audit it
boolean auditedSomething = false;
for (RMAuditNode auditedNode : auditedNodes)
{
NodeRef nodeRef = auditedNode.getNodeRef();
// If the node is gone, then do nothing
if (nodeRef != null && !nodeService.exists(nodeRef))
{
continue;
}
// build the audit map
Map<String, Serializable> auditMap = buildAuditMap(nodeRef,
auditedNode.getEventName(),
auditedNode.getNodePropertiesBefore(),
auditedNode.getNodePropertiesAfter(),
auditedNode.getRemoveIfNoPropertyChanged());
// Audit it
if (logger.isDebugEnabled())
{
logger.debug("RM Audit: Auditing values: \n" + auditMap);
}
auditMap = auditComponent.recordAuditValues(RM_AUDIT_PATH_ROOT, auditMap);
if (auditMap.isEmpty())
{
if (logger.isDebugEnabled())
{
logger.debug("RM Audit: Nothing was audited.");
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("RM Audit: Audited values: \n" + auditMap);
}
// We must commit the transaction to get the values in
auditedSomething = true;
}
}
// Check if anything was audited
if (!auditedSomething)
{
// Nothing was audited, so do nothing
RetryingTransactionHelper.getActiveUserTransaction().setRollbackOnly();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public File getAuditTrailFile(RecordsManagementAuditQueryParameters params, ReportFormat format)
{
ParameterCheck.mandatory("params", params);
Writer fileWriter = null;
try
{
File auditTrailFile = TempFileProvider.createTempFile(AUDIT_TRAIL_FILE_PREFIX,
format == ReportFormat.HTML ? AUDIT_TRAIL_HTML_FILE_SUFFIX : AUDIT_TRAIL_JSON_FILE_SUFFIX);
fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(auditTrailFile),"UTF8"));
// Get the results, dumping to file
getAuditTrailImpl(params, null, fileWriter, format);
// Done
return auditTrailFile;
}
catch (IOException e)
{
throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, e);
}
finally
{
// close the writer
if (fileWriter != null)
{
try { fileWriter.close(); } catch (IOException closeEx) {}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public List<RecordsManagementAuditEntry> getAuditTrail(RecordsManagementAuditQueryParameters params)
{
ParameterCheck.mandatory("params", params);
List<RecordsManagementAuditEntry> entries = new ArrayList<RecordsManagementAuditEntry>(50);
try
{
getAuditTrailImpl(params, entries, null, null);
// Done
return entries;
}
catch (IOException e)
{
// Should be
throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, e);
}
}
/**
* Get the audit trail, optionally dumping the results the the given writer dumping to a list.
*
* @param params the search parameters
* @param results the list to which individual results will be dumped
* @param writer Writer to write the audit trail
* @param reportFormat Format to write the audit trail in, ignored if writer is <code>null</code>
*/
private void getAuditTrailImpl(
final RecordsManagementAuditQueryParameters params,
final List<RecordsManagementAuditEntry> results,
final Writer writer,
final ReportFormat reportFormat)
throws IOException
{
if (logger.isDebugEnabled())
{
logger.debug("Retrieving audit trail in '" + reportFormat + "' format using parameters: " + params);
}
// define the callback
AuditQueryCallback callback = new AuditQueryCallback()
{
private boolean firstEntry = true;
@Override
public boolean valuesRequired()
{
return true;
}
/**
* Just log the error, but continue
*/
@Override
public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
{
logger.warn(errorMsg, error);
return true;
}
@Override
@SuppressWarnings("unchecked")
public boolean handleAuditEntry(
Long entryId,
String applicationName,
String user,
long time,
Map<String, Serializable> values)
{
// Check for context shutdown
if (shutdown)
{
return false;
}
Date timestamp = new Date(time);
String eventName = null;
String fullName = null;
String userRoles = null;
NodeRef nodeRef = null;
String nodeName = null;
String nodeType = null;
String nodeIdentifier = null;
String namePath = null;
Map<QName, Serializable> beforeProperties = null;
Map<QName, Serializable> afterProperties = null;
if (values.containsKey(RM_AUDIT_DATA_EVENT_NAME))
{
// This data is /RM/event/...
eventName = (String) values.get(RM_AUDIT_DATA_EVENT_NAME);
fullName = (String) values.get(RM_AUDIT_DATA_PERSON_FULLNAME);
userRoles = (String) values.get(RM_AUDIT_DATA_PERSON_ROLES);
nodeRef = (NodeRef) values.get(RM_AUDIT_DATA_NODE_NODEREF);
nodeName = (String) values.get(RM_AUDIT_DATA_NODE_NAME);
QName nodeTypeQname = (QName) values.get(RM_AUDIT_DATA_NODE_TYPE);
nodeIdentifier = (String) values.get(RM_AUDIT_DATA_NODE_IDENTIFIER);
namePath = (String) values.get(RM_AUDIT_DATA_NODE_NAMEPATH);
beforeProperties = (Map<QName, Serializable>) values.get(RM_AUDIT_DATA_NODE_CHANGES_BEFORE);
afterProperties = (Map<QName, Serializable>) values.get(RM_AUDIT_DATA_NODE_CHANGES_AFTER);
// Convert some of the values to recognizable forms
nodeType = null;
if (nodeTypeQname != null)
{
TypeDefinition typeDef = dictionaryService.getType(nodeTypeQname);
nodeType = (typeDef != null) ? typeDef.getTitle(dictionaryService) : null;
}
}
else if (values.containsKey(DOD5015_AUDIT_DATA_EVENT_NAME))
{
// This data is /RM/event/...
eventName = (String) values.get(DOD5015_AUDIT_DATA_EVENT_NAME);
fullName = (String) values.get(DOD5015_AUDIT_DATA_PERSON_FULLNAME);
userRoles = (String) values.get(DOD5015_AUDIT_DATA_PERSON_ROLES);
nodeRef = (NodeRef) values.get(DOD5015_AUDIT_DATA_NODE_NODEREF);
nodeName = (String) values.get(DOD5015_AUDIT_DATA_NODE_NAME);
QName nodeTypeQname = (QName) values.get(DOD5015_AUDIT_DATA_NODE_TYPE);
nodeIdentifier = (String) values.get(DOD5015_AUDIT_DATA_NODE_IDENTIFIER);
namePath = (String) values.get(DOD5015_AUDIT_DATA_NODE_NAMEPATH);
beforeProperties = (Map<QName, Serializable>) values.get( DOD5015_AUDIT_DATA_NODE_CHANGES_BEFORE);
afterProperties = (Map<QName, Serializable>) values.get(DOD5015_AUDIT_DATA_NODE_CHANGES_AFTER);
// Convert some of the values to recognizable forms
nodeType = null;
if (nodeTypeQname != null)
{
TypeDefinition typeDef = dictionaryService.getType(nodeTypeQname);
nodeType = (typeDef != null) ? typeDef.getTitle(dictionaryService) : null;
}
}
else if (values.containsKey(RM_AUDIT_DATA_LOGIN_USERNAME))
{
user = (String) values.get(RM_AUDIT_DATA_LOGIN_USERNAME);
if (values.containsKey(RM_AUDIT_DATA_LOGIN_ERROR))
{
eventName = RM_AUDIT_EVENT_LOGIN_FAILURE;
// The user didn't log in
fullName = user;
}
else
{
eventName = RM_AUDIT_EVENT_LOGIN_SUCCESS;
fullName = (String) values.get(RM_AUDIT_DATA_LOGIN_FULLNAME);
}
}
else if (values.containsKey(DOD5015_AUDIT_DATA_LOGIN_USERNAME))
{
user = (String) values.get(DOD5015_AUDIT_DATA_LOGIN_USERNAME);
if (values.containsKey(DOD5015_AUDIT_DATA_LOGIN_ERROR))
{
eventName = RM_AUDIT_EVENT_LOGIN_FAILURE;
// The user didn't log in
fullName = user;
}
else
{
eventName = RM_AUDIT_EVENT_LOGIN_SUCCESS;
fullName = (String) values.get(DOD5015_AUDIT_DATA_LOGIN_FULLNAME);
}
}
else
{
// This is not recognisable data
logger.warn(
"Unable to process audit entry for RM. Unexpected data: \n" +
" Entry: " + entryId + "\n" +
" Data: " + values);
// Skip it
return true;
}
if(nodeRef != null && nodeService.exists(nodeRef) &&
!AccessStatus.ALLOWED.equals(
capabilityService.getCapabilityAccessState(nodeRef, ACCESS_AUDIT_CAPABILITY)))
{
return true;
}
// TODO: Refactor this to use the builder pattern
RecordsManagementAuditEntry entry = new RecordsManagementAuditEntry(
timestamp,
user,
fullName,
// A concatenated string of roles
userRoles,
nodeRef,
nodeName,
nodeType,
eventName,
nodeIdentifier,
namePath,
beforeProperties,
afterProperties);
// write out the entry to the file in requested format
writeEntryToFile(entry);
if (results != null)
{
results.add(entry);
}
if (logger.isDebugEnabled())
{
logger.debug(" " + entry);
}
// Keep going
return true;
}
private void writeEntryToFile(RecordsManagementAuditEntry entry)
{
if (writer == null)
{
return;
}
try
{
if (!firstEntry)
{
if (reportFormat == ReportFormat.HTML)
{
writer.write("\n");
}
else
{
writer.write(",");
}
}
else
{
firstEntry = false;
}
// write the entry to the file
if (reportFormat == ReportFormat.JSON)
{
writer.write("\n\t\t");
}
writeAuditTrailEntry(writer, entry, reportFormat);
}
catch (IOException ioe)
{
throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, ioe);
}
}
};
String user = params.getUser();
Long fromTime = getFromDateTime(params.getDateFrom());
Long toTime = getToDateTime(params.getDateTo());
NodeRef nodeRef = params.getNodeRef();
int maxEntries = params.getMaxEntries();
// Reverse order if the results are limited
boolean forward = maxEntries > 0 ? false : true;
// start the audit trail report
writeAuditTrailHeader(writer, params, reportFormat);
if (logger.isDebugEnabled())
{
logger.debug("RM Audit: Issuing query: " + params);
}
// Build audit query parameters
AuditQueryParameters dod5015AuditQueryParams = new AuditQueryParameters();
dod5015AuditQueryParams.setForward(forward);
dod5015AuditQueryParams.setApplicationName(DOD5015_AUDIT_APPLICATION_NAME);
dod5015AuditQueryParams.setUser(user);
dod5015AuditQueryParams.setFromTime(fromTime);
dod5015AuditQueryParams.setToTime(toTime);
if (nodeRef != null)
{
dod5015AuditQueryParams.addSearchKey(DOD5015_AUDIT_DATA_NODE_NODEREF, nodeRef);
}
//
AuditQueryParameters auditQueryParams = new AuditQueryParameters();
auditQueryParams.setForward(forward);
auditQueryParams.setApplicationName(RM_AUDIT_APPLICATION_NAME);
auditQueryParams.setUser(user);
auditQueryParams.setFromTime(fromTime);
auditQueryParams.setToTime(toTime);
if (nodeRef != null)
{
auditQueryParams.addSearchKey(RM_AUDIT_DATA_NODE_NODEREF, nodeRef);
}
else if (params.getEvent() != null)
{
auditQueryParams.addSearchKey(RM_AUDIT_DATA_EVENT_NAME, params.getEvent());
}
// Get audit entries
auditService.auditQuery(callback, dod5015AuditQueryParams, maxEntries);
auditService.auditQuery(callback, auditQueryParams, maxEntries);
// finish off the audit trail report
writeAuditTrailFooter(writer, reportFormat);
// audit that the audit has been view'ed
if (nodeRef == null)
{
// grab the default file plan, but don't fail if it can't be found!
nodeRef = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
}
auditEvent(nodeRef, AUDIT_EVENT_VIEW, null, null, true);
}
/**
* Calculates the start of the given date.
* For example, if you had the date time of 12 Aug 2013 12:10:15.158
* the result would be 12 Aug 2013 00:00:00.000.
*
* @param date The date for which the start should be calculated.
* @return Returns the start of the given date.
*/
private Date getStartOfDay(Date date)
{
return DateUtils.truncate(date == null ? new Date() : date, Calendar.DATE);
}
/**
* Gets the start of the from date
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditServiceImpl.getStartOfDay()
*
* @param date The date for which the start should be retrieved.
* @return Returns null if the given date is null, otherwise the start of the given day.
*/
private Date getFromDate(Date date)
{
return date == null ? null : getStartOfDay(date);
}
/**
* Returns the number of milliseconds for the "from date".
*
* @param date The date for which the number of milliseconds should retrieved.
* @return Returns null if the given date is null, otherwise the number of milliseconds for the given date.
*/
private Long getFromDateTime(Date date)
{
Long fromDateTime = null;
Date fromDate = getFromDate(date);
if (fromDate != null)
{
fromDateTime = Long.valueOf(fromDate.getTime());
}
return fromDateTime;
}
/**
* Calculates the end of the given date.
* For example, if you had the date time of 12 Aug 2013 12:10:15.158
* the result would be 12 Aug 2013 23:59:59.999.
*
* @param date The date for which the end should be calculated.
* @return Returns the end of the given date.
*/
private Date getEndOfDay(Date date)
{
return DateUtils.addMilliseconds(DateUtils.ceiling(date == null ? new Date() : date, Calendar.DATE), -1);
}
/**
* Gets the end of the from date
* @see org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditServiceImpl.getEndOfDay()
*
* @param date The date for which the end should be retrieved.
* @return Returns null if the given date is null, otherwise the end of the given day.
*/
private Date getToDate(Date date)
{
return date == null ? null : getEndOfDay(date);
}
/**
* Returns the number of milliseconds for the "to date".
*
* @param date The date for which the number of milliseconds should retrieved.
* @return Returns null if the given date is null, otherwise the number of milliseconds for the given date.
*/
private Long getToDateTime(Date date)
{
Long toDateTime = null;
Date toDate = getToDate(date);
if (toDate != null)
{
toDateTime = Long.valueOf(toDate.getTime());
}
return toDateTime;
}
/**
* {@inheritDoc}
*/
@Override
public NodeRef fileAuditTrailAsRecord(RecordsManagementAuditQueryParameters params,
NodeRef destination, ReportFormat format)
{
ParameterCheck.mandatory("params", params);
ParameterCheck.mandatory("destination", destination);
// NOTE: the underlying RM services will check all the remaining pre-conditions
NodeRef record = null;
// get the audit trail for the provided parameters
File auditTrail = this.getAuditTrailFile(params, format);
if (logger.isDebugEnabled())
{
logger.debug("Filing audit trail in file " + auditTrail.getAbsolutePath() +
" as a record in record folder: " + destination);
}
try
{
Map<QName, Serializable> properties = new HashMap<QName, Serializable>(1);
properties.put(ContentModel.PROP_NAME, auditTrail.getName());
// file the audit log as an undeclared record
record = this.nodeService.createNode(destination,
ContentModel.ASSOC_CONTAINS,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
QName.createValidLocalName(auditTrail.getName())),
ContentModel.TYPE_CONTENT, properties).getChildRef();
// Set the content
ContentWriter writer = this.contentService.getWriter(record, ContentModel.PROP_CONTENT, true);
writer.setMimetype(format == ReportFormat.HTML ? MimetypeMap.MIMETYPE_HTML : MimetypeMap.MIMETYPE_JSON);
writer.setEncoding("UTF-8");
writer.putContent(auditTrail);
}
finally
{
if (logger.isDebugEnabled())
{
logger.debug("Audit trail report saved to temporary file: " + auditTrail.getAbsolutePath());
}
else
{
auditTrail.delete();
}
}
return record;
}
/**
* {@inheritDoc}
*/
@Override
public List<AuditEvent> getAuditEvents()
{
List<AuditEvent> listAuditEvents = new ArrayList<AuditEvent>(this.auditEvents.size());
listAuditEvents.addAll(this.auditEvents.values());
Collections.sort(listAuditEvents);
return listAuditEvents;
}
/**
* Writes the start of the audit trail stream to the given writer
*
* @param writer The writer to write to
* @params params The parameters being used
* @param reportFormat The format to write the header in
* @throws IOException
*/
private void writeAuditTrailHeader(Writer writer,
RecordsManagementAuditQueryParameters params,
ReportFormat reportFormat) throws IOException
{
if (writer == null)
{
return;
}
if (reportFormat == ReportFormat.HTML)
{
// write header as HTML
writer.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
writer.write("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n");
writer.write("<title>");
writer.write(I18NUtil.getMessage(MSG_AUDIT_REPORT));
writer.write("</title></head>\n");
writer.write("<style>\n");
writer.write("body { font-family: arial,verdana; font-size: 81%; color: #333; }\n");
writer.write(".label { margin-right: 5px; font-weight: bold; }\n");
writer.write(".value { margin-right: 40px; }\n");
writer.write(".audit-info { background-color: #efefef; padding: 10px; margin-bottom: 4px; }\n");
writer.write(".audit-entry { border: 1px solid #bbb; margin-top: 15px; }\n");
writer.write(".audit-entry-header { background-color: #bbb; padding: 8px; }\n");
writer.write(".audit-entry-node { padding: 10px; }\n");
writer.write(".changed-values-table { margin-left: 6px; margin-bottom: 2px;width: 99%; }\n");
writer.write(".changed-values-table th { text-align: left; background-color: #eee; padding: 4px; }\n");
writer.write(".changed-values-table td { width: 33%; padding: 4px; border-top: 1px solid #eee; }\n");
writer.write("</style>\n");
writer.write("<body>\n<h2>");
writer.write(I18NUtil.getMessage(MSG_AUDIT_REPORT));
writer.write("</h2>\n");
writer.write("<div class=\"audit-info\">\n");
writer.write("<span class=\"label\">From:</span>");
writer.write("<span class=\"value\">");
Date from = params.getDateFrom();
writer.write(from == null ? "<Not Set>" : StringEscapeUtils.escapeHtml(from.toString()));
writer.write("</span>");
writer.write("<span class=\"label\">To:</span>");
writer.write("<span class=\"value\">");
Date to = params.getDateTo();
writer.write(to == null ? "<Not Set>" : StringEscapeUtils.escapeHtml(to.toString()));
writer.write("</span>");
writer.write("<span class=\"label\">Property:</span>");
writer.write("<span class=\"value\">");
QName prop = params.getProperty();
writer.write(prop == null ? "All" : StringEscapeUtils.escapeHtml(getPropertyLabel(prop)));
writer.write("</span>");
writer.write("<span class=\"label\">User:</span>");
writer.write("<span class=\"value\">");
writer.write(params.getUser() == null ? "All" : StringEscapeUtils.escapeHtml(params.getUser()));
writer.write("</span>");
writer.write("<span class=\"label\">Event:</span>");
writer.write("<span class=\"value\">");
writer.write(params.getEvent() == null ? "All" : StringEscapeUtils.escapeHtml(getAuditEventLabel(params.getEvent())));
writer.write("</span>\n");
writer.write("</div>\n");
}
else
{
// write header as JSON
writer.write("{\n\t\"data\":\n\t{");
writer.write("\n\t\t\"started\": \"");
writer.write(ISO8601DateFormat.format(getStartOfDay(params.getDateFrom())));
writer.write("\",\n\t\t\"stopped\": \"");
writer.write(ISO8601DateFormat.format(getEndOfDay(params.getDateTo())));
writer.write("\",\n\t\t\"enabled\": ");
writer.write(Boolean.toString(isEnabled()));
writer.write(",\n\t\t\"entries\":[");
}
}
/**
* Writes an audit trail entry to the given writer
*
* @param writer The writer to write to
* @param entry The entry to write
* @param reportFormat The format to write the header in
* @throws IOException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void writeAuditTrailEntry(Writer writer, RecordsManagementAuditEntry entry,
ReportFormat reportFormat) throws IOException
{
if (writer == null)
{
return;
}
if (reportFormat == ReportFormat.HTML)
{
writer.write("<div class=\"audit-entry\">\n");
writer.write("<div class=\"audit-entry-header\">");
writer.write("<span class=\"label\">Timestamp:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(entry.getTimestamp().toString()));
writer.write("</span>");
writer.write("<span class=\"label\">User:</span>");
writer.write("<span class=\"value\">");
writer.write(entry.getFullName() != null ?
StringEscapeUtils.escapeHtml(entry.getFullName()) :
StringEscapeUtils.escapeHtml(entry.getUserName()));
writer.write("</span>");
if (entry.getUserRole() != null && entry.getUserRole().length() > 0)
{
writer.write("<span class=\"label\">Role:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(entry.getUserRole()));
writer.write("</span>");
}
if (entry.getEvent() != null && entry.getEvent().length() > 0)
{
writer.write("<span class=\"label\">Event:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(getAuditEventLabel(entry.getEvent())));
writer.write("</span>\n");
}
writer.write("</div>\n");
writer.write("<div class=\"audit-entry-node\">");
if (entry.getIdentifier() != null && entry.getIdentifier().length() > 0)
{
writer.write("<span class=\"label\">Identifier:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(entry.getIdentifier()));
writer.write("</span>");
}
if (entry.getNodeType() != null && entry.getNodeType().length() > 0)
{
writer.write("<span class=\"label\">Type:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(entry.getNodeType()));
writer.write("</span>");
}
if (entry.getPath() != null && entry.getPath().length() > 0)
{
// we need to strip off the first part of the path
String path = entry.getPath();
String displayPath = path;
int idx = path.indexOf('/', 1);
if (idx != -1)
{
displayPath = "/File Plan" + path.substring(idx);
}
writer.write("<span class=\"label\">Location:</span>");
writer.write("<span class=\"value\">");
writer.write(StringEscapeUtils.escapeHtml(displayPath));
writer.write("</span>");
}
writer.write("</div>\n");
if (entry.getChangedProperties() != null)
{
writer.write("<table class=\"changed-values-table\" cellspacing=\"0\">");
writer.write("<tr><th>Property</th><th>Previous Value</th><th>New Value</th></tr>");
// create an entry for each property that changed
for (QName valueName : entry.getChangedProperties().keySet())
{
Pair<Serializable, Serializable> values = entry.getChangedProperties().get(valueName);
writer.write("<tr><td>");
writer.write(getPropertyLabel(valueName));
writer.write("</td><td>");
// inspect the property to determine it's data type
QName propDataType = DataTypeDefinition.TEXT;
PropertyDefinition propDef = dictionaryService.getProperty(valueName);
if (propDef != null)
{
propDataType = propDef.getDataType().getName();
}
if(DataTypeDefinition.MLTEXT.equals(propDataType))
{
writer.write(values.getFirst() == null ? "<none>" : StringEscapeUtils.escapeHtml(convertToMlText((Map)values.getFirst()).getDefaultValue()));
writer.write("</td><td>");
writer.write(values.getSecond() == null ? "<none>" : StringEscapeUtils.escapeHtml(convertToMlText((Map)values.getSecond()).getDefaultValue()));
}
else
{
Serializable oldValue = values.getFirst();
writer.write(oldValue == null ? "<none>" : StringEscapeUtils.escapeHtml(oldValue.toString()));
writer.write("</td><td>");
Serializable newValue = values.getSecond();
writer.write(newValue == null ? "<none>" : StringEscapeUtils.escapeHtml(newValue.toString()));
}
writer.write("</td></tr>");
}
writer.write("</table>\n");
}
writer.write("</div>");
}
else
{
try
{
JSONObject json = new JSONObject();
json.put("timestamp", entry.getTimestampString());
json.put("userName", entry.getUserName());
json.put("userRole", entry.getUserRole() == null ? "": entry.getUserRole());
json.put("fullName", entry.getFullName() == null ? "": entry.getFullName());
json.put("nodeRef", entry.getNodeRef() == null ? "": entry.getNodeRef());
// TODO: Find another way for checking the event
if (entry.getEvent().equals("Create Person") && entry.getNodeRef() != null)
{
NodeRef nodeRef = entry.getNodeRef();
String userName = null;
if(nodeService.exists(nodeRef))
{
userName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME);
}
json.put("nodeName", userName == null ? "": userName);
json.put("createPerson", true);
}
else
{
json.put("nodeName", entry.getNodeName() == null ? "": entry.getNodeName());
}
// TODO: Find another way for checking the event
if (entry.getEvent().equals("Delete RM Object"))
{
json.put("deleteObject", true);
}
json.put("nodeType", entry.getNodeType() == null ? "": entry.getNodeType());
json.put("event", entry.getEvent() == null ? "": getAuditEventLabel(entry.getEvent()));
json.put("identifier", entry.getIdentifier() == null ? "": entry.getIdentifier());
json.put("path", entry.getPath() == null ? "": entry.getPath());
JSONArray changedValues = new JSONArray();
if (entry.getChangedProperties() != null)
{
// create an entry for each property that changed
for (QName valueName : entry.getChangedProperties().keySet())
{
Pair<Serializable, Serializable> values = entry.getChangedProperties().get(valueName);
JSONObject changedValue = new JSONObject();
changedValue.put("name", getPropertyLabel(valueName));
// inspect the property to determine it's data type
QName propDataType = DataTypeDefinition.TEXT;
PropertyDefinition propDef = dictionaryService.getProperty(valueName);
if (propDef != null)
{
propDataType = propDef.getDataType().getName();
}
// handle output of mltext properties
if(DataTypeDefinition.MLTEXT.equals(propDataType))
{
changedValue.put("previous", values.getFirst() == null ? "" : convertToMlText((Map)values.getFirst()).getDefaultValue());
changedValue.put("new", values.getSecond() == null ? "" : convertToMlText((Map)values.getSecond()).getDefaultValue());
}
else
{
changedValue.put("previous", values.getFirst() == null ? "" : values.getFirst().toString());
changedValue.put("new", values.getSecond() == null ? "" : values.getSecond().toString());
}
changedValues.put(changedValue);
}
}
json.put("changedValues", changedValues);
writer.write(json.toString());
}
catch (JSONException je)
{
writer.write("{}");
}
}
}
/**
* Helper method to convert value to MLText
*
* @param map map of locale's and values
* @return {@link MLText} multilingual text value
*/
private MLText convertToMlText(Map<Locale, String> map)
{
MLText mlText = new MLText();
mlText.putAll(map);
return mlText;
}
/**
* Writes the end of the audit trail stream to the given writer
*
* @param writer The writer to write to
* @param reportFormat The format to write the footer in
* @throws IOException
*/
private void writeAuditTrailFooter(Writer writer, ReportFormat reportFormat) throws IOException
{
if (writer == null)
{
return;
}
if (reportFormat == ReportFormat.HTML)
{
// write footer as HTML
writer.write("\n</body></html>");
}
else
{
// write footer as JSON
writer.write("\n\t\t]\n\t}\n}");
}
}
/**
* Returns the display label for a property QName
*
* @param property The property to get label for
* @param ddService DictionaryService instance
* @param namespaceService NamespaceService instance
* @return The label
*/
private String getPropertyLabel(QName property)
{
String label = null;
PropertyDefinition propDef = this.dictionaryService.getProperty(property);
if (propDef != null)
{
label = propDef.getTitle(dictionaryService);
}
if (label == null)
{
label = property.getLocalName();
}
return label;
}
/**
* Returns the display label for the given audit event key
*
* @param eventKey The audit event key
* @return The display label or null if the key does not exist
*/
private String getAuditEventLabel(String eventKey)
{
String label = eventKey;
AuditEvent event = this.auditEvents.get(eventKey);
if (event != null)
{
label = event.getLabel();
}
return label;
}
/**
* A class to carry audit information through the transaction.
*
* @author Derek Hulley
* @since 3.2
*/
private static class RMAuditNode
{
private NodeRef nodeRef;
private String eventName;
private Map<QName, Serializable> nodePropertiesBefore;
private Map<QName, Serializable> nodePropertiesAfter;
private boolean removeIfNoPropertyChanged = false;
public NodeRef getNodeRef()
{
return nodeRef;
}
public void setNodeRef(NodeRef nodeRef)
{
this.nodeRef = nodeRef;
}
public String getEventName()
{
return eventName;
}
public void setEventName(String eventName)
{
this.eventName = eventName;
}
public Map<QName, Serializable> getNodePropertiesBefore()
{
return nodePropertiesBefore;
}
public void setNodePropertiesBefore(Map<QName, Serializable> nodePropertiesBefore)
{
this.nodePropertiesBefore = nodePropertiesBefore;
}
public Map<QName, Serializable> getNodePropertiesAfter()
{
return nodePropertiesAfter;
}
public void setNodePropertiesAfter(Map<QName, Serializable> nodePropertiesAfter)
{
this.nodePropertiesAfter = nodePropertiesAfter;
}
public boolean getRemoveIfNoPropertyChanged()
{
return removeIfNoPropertyChanged;
}
public void setRemoveIfNoPropertyChanged(boolean removeIfNoPropertyChanged)
{
this.removeIfNoPropertyChanged = removeIfNoPropertyChanged;
}
}
/** Deprecated Method Implementations **/
/**
* Helper method to get the default file plan
*
* @return NodRef default file plan
*/
private NodeRef getDefaultFilePlan()
{
NodeRef defaultFilePlan = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
if (defaultFilePlan == null)
{
throw new AlfrescoRuntimeException("Default file plan could not be found.");
}
return defaultFilePlan;
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public boolean isEnabled()
{
return isAuditLogEnabled(getDefaultFilePlan());
}
/**
* {@inheritDoc}
*/
@Deprecated
public void start()
{
startAuditLog(getDefaultFilePlan());
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public void stop()
{
stopAuditLog(getDefaultFilePlan());
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public Date getDateLastStarted()
{
return getDateAuditLogLastStarted(getDefaultFilePlan());
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public Date getDateLastStopped()
{
return getDateAuditLogLastStopped(getDefaultFilePlan());
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public void clear()
{
clearAuditLog(getDefaultFilePlan());
}
/**
* {@inheritDoc}
* @since 3.2
* @deprecated since 2.1
*/
@Override
@Deprecated
public void auditRMAction(
RecordsManagementAction action,
NodeRef nodeRef,
Map<String, Serializable> parameters)
{
auditEvent(nodeRef, action.getName());
}
}