/* * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * * 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 . */ package org.alfresco.repo.audit.access; import static org.alfresco.repo.audit.model.AuditApplication.AUDIT_PATH_SEPARATOR; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.alfresco.repo.audit.model.AuditApplication; import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCancelCheckOut; import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckIn; import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckOut; import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy; import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; import org.alfresco.repo.policy.PolicyScope; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.version.VersionServicePolicies.OnCreateVersionPolicy; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.InvalidQNameException; import org.alfresco.service.namespace.NamespaceException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; /** * Changes made to a {@code Node} in a single transaction. For example the creation of * a Node also involves updating properties, but the main action remains create node. * * @author Alan Davis */ /*package*/ class NodeChange implements BeforeDeleteNodePolicy, OnAddAspectPolicy, OnCreateNodePolicy, OnMoveNodePolicy, OnRemoveAspectPolicy, OnUpdatePropertiesPolicy, OnContentReadPolicy, OnContentUpdatePolicy, OnCreateVersionPolicy, OnCopyCompletePolicy, OnCheckOut, OnCheckIn, OnCancelCheckOut { private static final String USER = "user"; private static final String ACTION = "action"; private static final String SUB_ACTIONS = "sub-actions"; private static final String NODE = "node"; private static final String PATH = "path"; private static final String TYPE = "type"; private static final String FROM = "from"; private static final String TO = "to"; private static final String ADD = "add"; private static final String DELETE = "delete"; private static final String COPY = "copy"; private static final String MOVE = "move"; private static final String PROPERTIES = "properties"; private static final String ASPECTS = "aspects"; private static final String VERSION_PROPERTIES = "version-properties"; public static final String SUB_ACTION = "sub-action"; private static final String DELETE_NODE = "deleteNode"; private static final String CREATE_NODE = "createNode"; private static final String MOVE_NODE = "moveNode"; private static final String UPDATE_NODE_PROPERTIES = "updateNodeProperties"; private static final String DELETE_NODE_ASPECT = "deleteNodeAspect"; private static final String ADD_NODE_ASPECT = "addNodeAspect"; private static final String CREATE_CONTENT = "createContent"; private static final String UPDATE_CONTENT = "updateContent"; private static final String READ_CONTENT = "readContent"; private static final String CREATE_VERSION = "createVersion"; private static final String COPY_NODE = "copyNode"; private static final String CHECK_IN = "checkIn"; private static final String CHECK_OUT = "checkOut"; private static final String CANCEL_CHECK_OUT = "cancelCheckOut"; private static final String INVALID_PATH_CHAR_REPLACEMENT = "-"; public static Collection SUMMARY_KEYS = new ArrayList(); static { SUMMARY_KEYS.add(buildPath(PROPERTIES, ADD)); SUMMARY_KEYS.add(buildPath(PROPERTIES, DELETE)); SUMMARY_KEYS.add(buildPath(PROPERTIES, FROM)); SUMMARY_KEYS.add(buildPath(PROPERTIES, TO)); SUMMARY_KEYS.add(buildPath(ASPECTS, ADD)); SUMMARY_KEYS.add(buildPath(ASPECTS, DELETE)); } public static final String SUB_ACTION_PREFIX = SUB_ACTION + AUDIT_PATH_SEPARATOR; private final NodeInfoFactory nodeInfoFactory; private final NamespaceService namespaceService; private NodeInfo nodeInfo; private String action; private boolean auditSubActions = false; private Set subActions = new LinkedHashSet(); private List> subActionAuditMaps; private NodeInfo copyFrom; private NodeInfo moveFrom; private Map fromProperties; private Map toProperties; private HashSet addedAspects; private HashSet deletedAspects; private HashMap versionProperties; /*package*/ NodeChange(NodeInfoFactory nodeInfoFactory, NamespaceService namespaceService, NodeRef nodeRef) { this.nodeInfoFactory = nodeInfoFactory; this.nodeInfo = nodeInfoFactory.newNodeInfo(nodeRef); this.namespaceService = namespaceService; } /** * @return a derived action for a transaction based on the sub-actions that have taken place. */ public String getDerivedAction() { // Derive higher level action String action; if (subActions.contains(CHECK_OUT)) { action = "CHECK OUT"; } else if (subActions.contains(CHECK_IN)) { action = "CHECK IN"; } else if (subActions.contains(CANCEL_CHECK_OUT)) { action = "CANCEL CHECK OUT"; } else if (subActions.contains(COPY_NODE)) { action = "COPY"; } else if (subActions.contains(CREATE_NODE)) { action = "CREATE"; } else if (subActions.size() == 1 && subActions.contains(READ_CONTENT)) { // Reads in combinations with other actions tend to only facilitate the other action. action = "READ"; } else if (subActions.contains(DELETE_NODE)) { action = "DELETE"; } else if (subActions.contains(CREATE_VERSION)) // && !subActions.contains(CREATE_NODE) { action = "CREATE VERSION"; } else if (subActions.contains(UPDATE_CONTENT)) // && !subActions.contains(CREATE_NODE) { action = "UPDATE CONTENT"; } else if (subActions.contains(MOVE_NODE)) { action = "MOVE"; } else { // Default to first sub-action action = this.action; } return action; } /** * @return {@code true} if the node has been created and then deleted. */ public boolean isTemporaryNode() { // No need to check the order as a new node would be given a ned node ref. return subActions.contains(CREATE_NODE) && subActions.contains(DELETE_NODE); } private NodeChange setAction(String action) { this.action = action; return this; } private void appendSubAction(NodeChange subNodeChange) { // Remember sub-actions so we can check them later to derive the high level action subActions.add(subNodeChange.action); // Default the action to the first sub-action; if (action == null) { action = subNodeChange.action; } // Audit sub actions if required. if (auditSubActions) { if (subActionAuditMaps == null) { subActionAuditMaps = new ArrayList>(); } subActionAuditMaps.add(subNodeChange.getAuditData(true)); } } public NodeChange setAuditSubActions(boolean auditSubActions) { this.auditSubActions = auditSubActions; return this; } private NodeChange setCopyFrom(NodeRef copyFrom) { this.copyFrom = nodeInfoFactory.newNodeInfo(copyFrom); return this; } private NodeChange setMoveFrom(ChildAssociationRef childAssocRef) { // Don't overwrite original value if multiple calls. if (moveFrom == null) { moveFrom = nodeInfoFactory.newNodeInfo(childAssocRef); } return this; } private NodeChange setMoveTo(ChildAssociationRef childAssocRef) { nodeInfo = nodeInfoFactory.newNodeInfo(childAssocRef); // Clear values if we are back to where we started. if (nodeInfo.equals(moveFrom)) { moveFrom = null; } return this; } private NodeChange setFromProperties(Map fromProperties) { // Don't overwrite original value if multiple calls. if (this.fromProperties == null) { this.fromProperties = fromProperties; } return this; } private NodeChange setToProperties(Map toProperties) { this.toProperties = toProperties; return this; } /** * Add an aspect - if just deleted, remove the delete, otherwise record the add. */ private NodeChange addAspect(QName aspect) { if (addedAspects == null) { addedAspects = new HashSet(); } // Consider sequences // add = add // del add = --- // add del add = add // add add = add if (deletedAspects != null && deletedAspects.contains(aspect)) { deletedAspects.remove(aspect); } else { addedAspects.add(aspect); } return this; } /** * Delete an aspect - if just added, remove the add, otherwise record the delete. */ private NodeChange deleteAspect(QName aspect) { if (deletedAspects == null) { deletedAspects = new HashSet(); } // Consider sequences // del = del // add del = --- // del add del = del // del del = del if (addedAspects != null && addedAspects.contains(aspect)) { addedAspects.remove(aspect); } else { deletedAspects.add(aspect); } return this; } private NodeChange setVersionProperties( HashMap versionProperties) { if (this.versionProperties == null) { this.versionProperties = new HashMap(); } this.versionProperties.putAll(versionProperties); return this; } @Override public void beforeDeleteNode(NodeRef nodeRef) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(DELETE_NODE)); } @Override public void onCreateNode(ChildAssociationRef childAssocRef) { NodeRef nodeRef = childAssocRef.getChildRef(); Map fromProperties = Collections.emptyMap(); Map toProperties = nodeInfoFactory.getProperties(nodeRef); this.fromProperties = null; // Sometimes onCreateNode policy is out of order (e.g. create a person) setFromProperties(fromProperties); setToProperties(toProperties); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(CREATE_NODE). setFromProperties(fromProperties). setToProperties(toProperties)); } @Override public void onMoveNode(ChildAssociationRef fromChildAssocRef, ChildAssociationRef toChildAssocRef) { setMoveFrom(fromChildAssocRef); setMoveTo(toChildAssocRef); // Note: A change of the child node name will be picked up as a property name change. appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, toChildAssocRef.getChildRef()). setAction(MOVE_NODE). setMoveFrom(fromChildAssocRef). setMoveTo(toChildAssocRef)); } @Override public void onUpdateProperties(NodeRef nodeRef, Map fromProperties, Map toProperties) { // Sometime we don't get the fromProperties, so just use the latest we have if (fromProperties.isEmpty() && this.toProperties != null) { fromProperties = this.toProperties; } setFromProperties(fromProperties); setToProperties(toProperties); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(UPDATE_NODE_PROPERTIES). setFromProperties(fromProperties). setToProperties(toProperties)); } @Override public void onRemoveAspect(NodeRef nodeRef, QName aspect) { deleteAspect(aspect); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(DELETE_NODE_ASPECT). deleteAspect(aspect)); } @Override public void onAddAspect(NodeRef nodeRef, QName aspect) { addAspect(aspect); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(ADD_NODE_ASPECT). addAspect(aspect)); } @Override public void onContentUpdate(NodeRef nodeRef, boolean newContent) { if (newContent) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(CREATE_CONTENT)); } else { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(UPDATE_CONTENT)); } } @Override public void onContentRead(NodeRef nodeRef) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(READ_CONTENT)); } @Override public void onCreateVersion(QName classRef, NodeRef nodeRef, Map versionProperties, PolicyScope nodeDetails) { setVersionProperties((HashMap)versionProperties); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(CREATE_VERSION). setVersionProperties((HashMap)versionProperties)); // Note nodeDetails are not used } public void onCopyComplete(QName classRef, NodeRef sourceNodeRef, NodeRef targetNodeRef, boolean copyToNewNode, Map copyMap) { setCopyFrom(sourceNodeRef); appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, targetNodeRef). setAction(COPY_NODE). setCopyFrom(sourceNodeRef)); } public void onCheckOut(NodeRef workingCopy) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, workingCopy). setAction(CHECK_OUT)); } public void onCheckIn(NodeRef nodeRef) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(CHECK_IN)); } public void onCancelCheckOut(NodeRef nodeRef) { appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). setAction(CANCEL_CHECK_OUT)); } public Map getAuditData(boolean subAction) { Map auditMap = new HashMap( 2 * (1 + // action 1 + // user 1 + // sub actions 3 + // node, path, type 3 + // copy source's node, path, type 3 + // move source's node, path, type (fromProperties != null ? fromProperties.size() + toProperties.size() + 4 : 0) + // individual property changes // grouped from, to, add and delete changes (addedAspects != null ? addedAspects.size() : 0) + (deletedAspects != null ? deletedAspects.size() : 0) + (versionProperties != null ? versionProperties.size()+1 : 0) + getSubAuditDataSize())); // For a transaction, set the action if (!subAction) { setAction(getDerivedAction()); } auditMap.put(ACTION, action); if (!subAction) // no need to repeat for sub actions { auditMap.put(USER, AuthenticationUtil.getFullyAuthenticatedUser()); addSubActionsToAuditMap(auditMap); auditMap.put(NODE, nodeInfo.getNodeRef()); auditMap.put(PATH, nodeInfo.getPrefixPath()); auditMap.put(TYPE, nodeInfo.getPrefixType()); } if (copyFrom != null) { auditMap.put(buildPath(COPY, FROM, NODE), copyFrom.getNodeRef()); auditMap.put(buildPath(COPY, FROM, PATH), copyFrom.getPrefixPath()); auditMap.put(buildPath(COPY, FROM, TYPE), copyFrom.getPrefixType()); } if (moveFrom != null) { auditMap.put(buildPath(MOVE, FROM, NODE), moveFrom.getNodeRef()); auditMap.put(buildPath(MOVE, FROM, PATH), moveFrom.getPrefixPath()); auditMap.put(buildPath(MOVE, FROM, TYPE), moveFrom.getPrefixType()); } if (fromProperties != null) { addPropertyChangesToAuditMap(auditMap, subAction); } if (addedAspects != null && !addedAspects.isEmpty()) { addAspectChangesToAuditMap(auditMap, ADD, addedAspects, subAction); } if (deletedAspects != null && !deletedAspects.isEmpty()) { addAspectChangesToAuditMap(auditMap, DELETE, deletedAspects, subAction); } if (versionProperties != null && !versionProperties.isEmpty()) { addVersionPropertiesToAuditMap(auditMap, versionProperties, subAction); } addSubActionAuditMapsToAuditMap(auditMap); return auditMap; } private void addSubActionsToAuditMap(Map auditMap) { StringBuilder sb = new StringBuilder(); for (String subAction: subActions) { if (sb.length() > 0) { sb.append(' '); } sb.append(subAction); } auditMap.put(SUB_ACTIONS, sb.toString()); } private void addPropertyChangesToAuditMap(Map auditMap, boolean subAction) { HashMap from = new HashMap(fromProperties.size()); HashMap to = new HashMap(toProperties.size()); HashMap add = new HashMap(toProperties.size()); HashMap delete = new HashMap(fromProperties.size()); // Initially check for changes to existing keys and values. // Record individual value changes and group (from, to, delete) changes in their own maps. for (Map.Entry entry : fromProperties.entrySet()) { // Audit QNames with the prefix set. The key can be used in original Set and // Map operations as only the namesapace and local name are used in equals and // hashcode methods. QName key = getPrefixedQName(entry.getKey()); String name = replaceInvalidPathChars(key.toPrefixString()); Serializable beforeValue = entry.getValue(); Serializable afterValue = null; boolean exists = toProperties.containsKey(key); boolean same = false; if (exists) { // Audit nothing if both values are null or equal. afterValue = toProperties.get(key); if ((beforeValue == afterValue) || (beforeValue != null && beforeValue.equals(afterValue))) same = true; } if (!same) { if (exists) { auditMap.put(buildPath(PROPERTIES, FROM, name), beforeValue); auditMap.put(buildPath(PROPERTIES, TO, name), afterValue); from.put(key, beforeValue); to.put(key, afterValue); } else { auditMap.put(buildPath(PROPERTIES, DELETE, name), beforeValue); delete.put(key, beforeValue); } } } // Check for new values. Record individual values and group as a single map. Set newKeys = new HashSet(toProperties.keySet()); newKeys.removeAll(fromProperties.keySet()); for (QName key: newKeys) { // Audit QNames with the prefix set. key = getPrefixedQName(key); Serializable afterValue = toProperties.get(key); String name = replaceInvalidPathChars(key.toPrefixString()); auditMap.put(buildPath(PROPERTIES, ADD, name), afterValue); add.put(key, afterValue); } // Record maps of additions, deletes and paired from and to values. if (!subAction) { if (!add.isEmpty()) { auditMap.put(buildPath(PROPERTIES, ADD), add); } if (!delete.isEmpty()) { auditMap.put(buildPath(PROPERTIES, DELETE), delete); } if (!from.isEmpty()) { auditMap.put(buildPath(PROPERTIES, FROM), from); } if (!to.isEmpty()) { auditMap.put(buildPath(PROPERTIES, TO), to); } } } private void addAspectChangesToAuditMap(Map auditMap, String addOrDelete, HashSet aspects, boolean subAction) { // Audit Set where the QName has the prefix set. HashSet prefixedAspects = new HashSet(aspects.size()); for (QName aspect: aspects) { // Audit QNames with the prefix set. aspect = getPrefixedQName(aspect); prefixedAspects.add(aspect); String name = replaceInvalidPathChars(aspect.toPrefixString()); auditMap.put(buildPath(ASPECTS, addOrDelete, name), null); } if (!subAction) { auditMap.put(buildPath(ASPECTS, addOrDelete), prefixedAspects); } } private void addVersionPropertiesToAuditMap(Map auditMap, HashMap properties, boolean subAction) { for (Map.Entry entry: properties.entrySet()) { // The key may be the string version ({URI}localName) of a QName. // If so get the prefixed version. String key = entry.getKey(); if (key.indexOf(QName.NAMESPACE_PREFIX) == 0) { try { QName qNameKey = QName.createQName(key); qNameKey = getPrefixedQName(qNameKey); key = qNameKey.toPrefixString(); } catch (InvalidQNameException e) { // Its just a String. } } String name = replaceInvalidPathChars(key); auditMap.put(buildPath(VERSION_PROPERTIES, name), entry.getValue()); } if (!subAction) { auditMap.put(VERSION_PROPERTIES, properties); } } private int getSubAuditDataSize() { int size = 0; // No point doing sub actions if only one! if (subActionAuditMaps != null && subActionAuditMaps.size() > 1) { for (Map subActionAuditMap: subActionAuditMaps) { size += subActionAuditMap.size(); } } return size; } private void addSubActionAuditMapsToAuditMap(Map auditMap) { // No point doing sub actions if only one! if (subActionAuditMaps != null && subActionAuditMaps.size() > 1) { String format = "%0"+Integer.toString(auditMap.size()).length()+"d"; int i = 0; for (Map subActionAuditMap : subActionAuditMaps) { String seq = String.format(format, i); for (Map.Entry entry : subActionAuditMap.entrySet()) { auditMap.put(buildPath(SUB_ACTION, seq, entry.getKey()), entry.getValue()); } i++; } } } /** * Returns a path to be used in an audit map. Unlike {@link AuditApplication#buildPath(String...)} * the returned value is relative (no leading slash). * @param components String.. components of the path. * @return a component path of the supplied values. */ private static String buildPath(String... components) { StringBuilder sb = new StringBuilder(); for (String component: components) { if (sb.length() > 0) { sb.append(AUDIT_PATH_SEPARATOR); } sb.append(component); } return sb.toString(); } /** * @return a new QName that has the prefix set or the original if it is unknown. */ private QName getPrefixedQName(QName qName) { try { qName = qName.getPrefixedQName(namespaceService); } catch (NamespaceException e) { // ignore - go with what we have } return qName; } /** * @return a String where all invalid audit path characters are replaced by a '-'. */ private String replaceInvalidPathChars(String path) { return AuditApplication.AUDIT_INVALID_PATH_COMP_CHAR_PATTERN.matcher(path).replaceAll(INVALID_PATH_CHAR_REPLACEMENT); } }