/* * Copyright (C) 2005 Alfresco, Inc. * * Licensed under the Mozilla Public License version 1.1 * with a permitted attribution clause. You may obtain a * copy of the License at * * http://www.alfresco.org/legal/license.txt * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific * language governing permissions and limitations under the * License. */ package org.alfresco.repo.node.integrity; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.DictionaryException; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Component that generates and processes integrity events, enforcing the dictionary * model's node structure. *

* In order to fulfill the contract of the interface, this class registers to receive notifications * pertinent to changes in the node structure. These are then store away in the persistent * store until the request to * {@link org.alfresco.repo.integrity.IntegrityService#checkIntegrity(String) check integrity} is * made. *

* In order to ensure registration of these events, the {@link #init()} method must be called. *

* By default, this service is enabled, but can be disabled using {@link #setEnabled(boolean)}.
* Tracing of the event stacks is, for performance reasons, disabled by default but can be enabled * using {@link #setTraceOn(boolean)}.
* When enabled, the integrity check can either fail with a RuntimeException or not. In either * case, the integrity violations are logged as warnings or errors. This behaviour is controleed using * {@link #setFailOnViolation(boolean)} and is off by default. In other words, if not set, this service * will only log warnings about integrity violations. *

* Some integrity checks are not performed here as they are dealt with directly during the modification * operation in the {@link org.alfresco.service.cmr.repository.NodeService node service}. * * @see #setPolicyComponent(PolicyComponent) * @see #setDictionaryService(DictionaryService) * @see #setIntegrityDaoService(IntegrityDaoService) * @see #setMaxErrorsPerTransaction(int) * @see #setFlushSize(int) * * @author Derek Hulley */ public class IntegrityChecker implements NodeServicePolicies.OnCreateNodePolicy, NodeServicePolicies.OnUpdatePropertiesPolicy, NodeServicePolicies.OnDeleteNodePolicy, NodeServicePolicies.OnAddAspectPolicy, NodeServicePolicies.OnRemoveAspectPolicy, NodeServicePolicies.OnCreateChildAssociationPolicy, NodeServicePolicies.OnDeleteChildAssociationPolicy, NodeServicePolicies.OnCreateAssociationPolicy, NodeServicePolicies.OnDeleteAssociationPolicy { private static Log logger = LogFactory.getLog(IntegrityChecker.class); /** key against which the set of events is stored in the current transaction */ private static final String KEY_EVENT_SET = "IntegrityChecker.EventSet"; /** key to store the local flag to disable integrity errors, i.e. downgrade to warnings */ private static final String KEY_WARN_IN_TRANSACTION = "IntegrityChecker.WarnInTransaction"; private PolicyComponent policyComponent; private DictionaryService dictionaryService; private NodeService nodeService; private boolean enabled; private boolean failOnViolation; private int maxErrorsPerTransaction; private boolean traceOn; /** * Downgrade violations to warnings within the current transaction. This is temporary and * is dependent on there being a current transaction active against the * current thread. When set, this will override the global * {@link #setFailOnViolation(boolean) failure behaviour}. */ public static void setWarnInTransaction() { AlfrescoTransactionSupport.bindResource(KEY_WARN_IN_TRANSACTION, Boolean.TRUE); } /** * @return Returns true if the current transaction should only warn on violations. * If false, the global setting will take effect. * * @see #setWarnInTransaction() */ public static boolean isWarnInTransaction() { Boolean warnInTransaction = (Boolean) AlfrescoTransactionSupport.getResource(KEY_WARN_IN_TRANSACTION); if (warnInTransaction == null || warnInTransaction == Boolean.FALSE) { return false; } else { return true; } } /** */ public IntegrityChecker() { this.enabled = true; this.failOnViolation = false; this.maxErrorsPerTransaction = 10; this.traceOn = false; } /** * @param policyComponent the component to register behaviour with */ public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } /** * @param dictionaryService the dictionary against which to confirm model details */ public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } /** * @param nodeService the node service to use for browsing node structures */ public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } /** * @param enabled set to false to disable integrity checking completely */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * @param traceOn set to true to enable stack traces recording * of events */ public void setTraceOn(boolean traceOn) { this.traceOn = traceOn; } /** * @param failOnViolation set to true to force failure by * RuntimeException when a violation occurs. */ public void setFailOnViolation(boolean failOnViolation) { this.failOnViolation = failOnViolation; } /** * @param maxLogNumberPerTransaction upper limit on how many violations are * logged when multiple violations have been found. */ public void setMaxErrorsPerTransaction(int maxLogNumberPerTransaction) { this.maxErrorsPerTransaction = maxLogNumberPerTransaction; } /** * Registers the system-level policy behaviours */ public void init() { // check that required properties have been set PropertyCheck.mandatory("IntegrityChecker", "dictionaryService", dictionaryService); PropertyCheck.mandatory("IntegrityChecker", "nodeService", nodeService); PropertyCheck.mandatory("IntegrityChecker", "policyComponent", policyComponent); if (enabled) // only register behaviour if integrity checking is on { // register behaviour policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), this, new JavaBehaviour(this, "onCreateNode")); policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), this, new JavaBehaviour(this, "onUpdateProperties")); policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), this, new JavaBehaviour(this, "onDeleteNode")); policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), this, new JavaBehaviour(this, "onAddAspect")); policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onRemoveAspect"), this, new JavaBehaviour(this, "onRemoveAspect")); policyComponent.bindAssociationBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), this, new JavaBehaviour(this, "onCreateChildAssociation")); policyComponent.bindAssociationBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteChildAssociation"), this, new JavaBehaviour(this, "onDeleteChildAssociation")); policyComponent.bindAssociationBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateAssociation"), this, new JavaBehaviour(this, "onCreateAssociation")); policyComponent.bindAssociationBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteAssociation"), this, new JavaBehaviour(this, "onDeleteAssociation")); } } /** * Ensures that this service is registered with the transaction and saves the event * * @param event */ @SuppressWarnings("unchecked") private void save(IntegrityEvent event) { // optionally set trace if (traceOn) { // get a stack trace Throwable t = new Throwable(); t.fillInStackTrace(); StackTraceElement[] trace = t.getStackTrace(); event.addTrace(trace); // done } // register this service AlfrescoTransactionSupport.bindIntegrityChecker(this); // get the event list Map events = (Map) AlfrescoTransactionSupport.getResource(KEY_EVENT_SET); if (events == null) { events = new HashMap(113, 0.75F); AlfrescoTransactionSupport.bindResource(KEY_EVENT_SET, events); } // check if the event is present IntegrityEvent existingEvent = events.get(event); if (existingEvent != null) { // the event (or its equivalent is already present - transfer the trace if (traceOn) { existingEvent.getTraces().addAll(event.getTraces()); } } else { // the event doesn't already exist events.put(event, event); } if (logger.isDebugEnabled()) { logger.debug("" + (existingEvent != null ? "Event already present in" : "Added event to") + " event set: \n" + " event: " + event); } } /** * @see PropertiesIntegrityEvent * @see AssocTargetRoleIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent */ public void onCreateNode(ChildAssociationRef childAssocRef) { NodeRef childRef = childAssocRef.getChildRef(); IntegrityEvent event = null; // check properties on child node event = new PropertiesIntegrityEvent( nodeService, dictionaryService, childRef); save(event); // check that the multiplicity and other properties of the new association are allowed onCreateChildAssociation(childAssocRef); // check mandatory aspects event = new AspectsIntegrityEvent(nodeService, dictionaryService, childRef); save(event); // check for associations defined on the new node (child) QName childNodeTypeQName = nodeService.getType(childRef); ClassDefinition nodeTypeDef = dictionaryService.getClass(childNodeTypeQName); if (nodeTypeDef == null) { throw new DictionaryException("The node type is not recognized: " + childNodeTypeQName); } Map childAssocDefs = nodeTypeDef.getAssociations(); // check the multiplicity of each association with the node acting as a source for (AssociationDefinition assocDef : childAssocDefs.values()) { QName assocTypeQName = assocDef.getName(); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, childRef, assocTypeQName, false); save(event); } } /** * @see PropertiesIntegrityEvent */ public void onUpdateProperties( NodeRef nodeRef, Map before, Map after) { IntegrityEvent event = null; // check properties on node event = new PropertiesIntegrityEvent(nodeService, dictionaryService, nodeRef); save(event); } /** * No checking performed: The association changes will be handled */ public void onDeleteNode(ChildAssociationRef childAssocRef, boolean isArchivedNode) { } /** * @see PropertiesIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent */ public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) { IntegrityEvent event = null; // check properties on node event = new PropertiesIntegrityEvent(nodeService, dictionaryService, nodeRef); save(event); // check for associations defined on the aspect AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); if (aspectDef == null) { throw new DictionaryException("The aspect type is not recognized: " + aspectTypeQName); } Map assocDefs = aspectDef.getAssociations(); // check the multiplicity of each association with the node acting as a source for (AssociationDefinition assocDef : assocDefs.values()) { QName assocTypeQName = assocDef.getName(); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, nodeRef, assocTypeQName, false); save(event); } } /** * @see AspectsIntegrityEvent */ public void onRemoveAspect(NodeRef nodeRef, QName aspectTypeQName) { IntegrityEvent event = null; // check mandatory aspects event = new AspectsIntegrityEvent(nodeService, dictionaryService, nodeRef); save(event); } /** * @see AssocSourceTypeIntegrityEvent * @see AssocTargetTypeIntegrityEvent * @see AssocSourceMultiplicityIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent * @see AssocTargetRoleIntegrityEvent */ public void onCreateChildAssociation(ChildAssociationRef childAssocRef) { IntegrityEvent event = null; // check source type event = new AssocSourceTypeIntegrityEvent( nodeService, dictionaryService, childAssocRef.getParentRef(), childAssocRef.getTypeQName()); save(event); // check target type event = new AssocTargetTypeIntegrityEvent( nodeService, dictionaryService, childAssocRef.getChildRef(), childAssocRef.getTypeQName()); save(event); // check source multiplicity event = new AssocSourceMultiplicityIntegrityEvent( nodeService, dictionaryService, childAssocRef.getChildRef(), childAssocRef.getTypeQName(), false); save(event); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, childAssocRef.getParentRef(), childAssocRef.getTypeQName(), false); save(event); // check target role event = new AssocTargetRoleIntegrityEvent( nodeService, dictionaryService, childAssocRef.getParentRef(), childAssocRef.getTypeQName(), childAssocRef.getQName()); save(event); } /** * @see AssocSourceMultiplicityIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent */ public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) { IntegrityEvent event = null; // check source multiplicity event = new AssocSourceMultiplicityIntegrityEvent( nodeService, dictionaryService, childAssocRef.getChildRef(), childAssocRef.getTypeQName(), true); save(event); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, childAssocRef.getParentRef(), childAssocRef.getTypeQName(), true); save(event); } /** * @see AssocSourceTypeIntegrityEvent * @see AssocTargetTypeIntegrityEvent * @see AssocSourceMultiplicityIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent */ public void onCreateAssociation(AssociationRef nodeAssocRef) { IntegrityEvent event = null; // check source type event = new AssocSourceTypeIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getSourceRef(), nodeAssocRef.getTypeQName()); save(event); // check target type event = new AssocTargetTypeIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getTargetRef(), nodeAssocRef.getTypeQName()); save(event); // check source multiplicity event = new AssocSourceMultiplicityIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getTargetRef(), nodeAssocRef.getTypeQName(), false); save(event); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getSourceRef(), nodeAssocRef.getTypeQName(), false); save(event); } /** * @see AssocSourceMultiplicityIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent */ public void onDeleteAssociation(AssociationRef nodeAssocRef) { IntegrityEvent event = null; // check source multiplicity event = new AssocSourceMultiplicityIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getTargetRef(), nodeAssocRef.getTypeQName(), true); save(event); // check target multiplicity event = new AssocTargetMultiplicityIntegrityEvent( nodeService, dictionaryService, nodeAssocRef.getSourceRef(), nodeAssocRef.getTypeQName(), true); save(event); } /** * Runs several types of checks, querying specifically for events that * will necessitate each type of test. *

* The interface contracts also requires that all events for the transaction * get cleaned up. */ public void checkIntegrity() throws IntegrityException { if (!enabled) { return; } // process events and check for failures List failures = processAllEvents(); // clear out all events AlfrescoTransactionSupport.unbindResource(KEY_EVENT_SET); // drop out quickly if there are no failures if (failures.isEmpty()) { return; } // handle errors according to instance flags // firstly, log all failures int failureCount = failures.size(); StringBuilder sb = new StringBuilder(300 * failureCount); sb.append("Found ").append(failureCount).append(" integrity violations"); if (maxErrorsPerTransaction < failureCount) { sb.append(" - first ").append(maxErrorsPerTransaction); } sb.append(":"); int count = 0; for (IntegrityRecord failure : failures) { // break if we exceed the maximum number of log entries count++; if (count > maxErrorsPerTransaction) { break; } sb.append("\n").append(failure); } boolean warnOnly = IntegrityChecker.isWarnInTransaction(); if (failOnViolation && !warnOnly) { logger.error(sb.toString()); throw new IntegrityException(failures); } else { logger.warn(sb.toString()); // no exception } } /** * Loops through all the integrity events and checks integrity. *

* The events are stored in a set, so there are no duplicates. Since each * event performs a particular type of check, this ensures that we don't * duplicate checks. * * @return Returns a list of integrity violations, up to the * {@link #maxErrorsPerTransaction the maximum defined} */ @SuppressWarnings("unchecked") private List processAllEvents() { // the results ArrayList allIntegrityResults = new ArrayList(0); // generally empty // get all the events for the transaction (or unit of work) // duplicates have been elimiated Map events = (Map) AlfrescoTransactionSupport.getResource(KEY_EVENT_SET); if (events == null) { // no events were registered - nothing of significance happened return allIntegrityResults; } // failure results for the event List integrityRecords = new ArrayList(0); // cycle through the events, performing checking integrity for (IntegrityEvent event : events.keySet()) { try { event.checkIntegrity(integrityRecords); } catch (Throwable e) { e.printStackTrace(); // log it as an error and move to next event IntegrityRecord exceptionRecord = new IntegrityRecord("" + e.getMessage()); exceptionRecord.setTraces(Collections.singletonList(e.getStackTrace())); allIntegrityResults.add(exceptionRecord); // move on continue; } // keep track of results needing trace added if (traceOn) { // record the current event trace if present for (IntegrityRecord integrityRecord : integrityRecords) { integrityRecord.setTraces(event.getTraces()); } } // copy all the event results to the final results allIntegrityResults.addAll(integrityRecords); // clear the event results integrityRecords.clear(); if (allIntegrityResults.size() >= maxErrorsPerTransaction) { // only so many errors wanted at a time break; } } // done return allIntegrityResults; } }