Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.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.tenant.TenantService; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper; 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.alfresco.util.transaction.TransactionListener; import org.alfresco.util.transaction.TransactionSupportUtil; 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. * <p> * 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 * check integrity is * made. * <p> * In order to ensure registration of these events, the {@link #init()} method must be called. * <p> * By default, this service is enabled, but can be disabled using {@link #setEnabled(boolean)}.<br> * Tracing of the event stacks is, for performance reasons, disabled by default but can be enabled * using {@link #setTraceOn(boolean)}.<br> * When enabled, the integrity check can either fail with a <tt>RuntimeException</tt> 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. * <p> * 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 #setMaxErrorsPerTransaction(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, TransactionListener { 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 TenantService tenantService; private boolean enabled; private boolean failOnViolation; private int maxErrorsPerTransaction; private boolean traceOn; private List<String> storesToIgnore = new ArrayList<String>(0); /** * Downgrade violations to warnings within the current transaction. This is temporary and * is <u>dependent on there being a current transaction</u> active against the * current thread. When set, this will override the global * {@link #setFailOnViolation(boolean) failure behaviour}. */ public static void setWarnInTransaction() { TransactionSupportUtil.bindResource(KEY_WARN_IN_TRANSACTION, Boolean.TRUE); } /** * @return Returns true if the current transaction should only warn on violations. * If <code>false</code>, the global setting will take effect. * * @see #setWarnInTransaction() */ public static boolean isWarnInTransaction() { Boolean warnInTransaction = (Boolean) TransactionSupportUtil.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; } public void setTenantService(TenantService tenantService) { this.tenantService = tenantService; } /** * @param enabled set to false to disable integrity checking completely */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * @param traceOn set to <code>true</code> to enable stack traces recording * of events */ public void setTraceOn(boolean traceOn) { this.traceOn = traceOn; } /** * @param failOnViolation set to <code>true</code> to force failure by * <tt>RuntimeException</tt> 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; } /** * @param storesToIgnore stores (eg. workspace://version2Store) which will be * ignored by integrity checker. Note: assumes associations are within a store. */ public void setStoresToIgnore(List<String> storesToIgnore) { this.storesToIgnore = storesToIgnore; } /** * 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 IntegrityEvent */ @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<IntegrityEvent, IntegrityEvent> events = (Map<IntegrityEvent, IntegrityEvent>) AlfrescoTransactionSupport .getResource(KEY_EVENT_SET); if (events == null) { events = new HashMap<IntegrityEvent, IntegrityEvent>(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(); if (!storesToIgnore.contains(tenantService.getBaseName(childRef.getStoreRef()).toString())) { 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, false); // 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<QName, AssociationDefinition> 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<QName, Serializable> before, Map<QName, Serializable> after) { if (!storesToIgnore.contains(tenantService.getBaseName(nodeRef.getStoreRef()).toString())) { 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) { if (!storesToIgnore.contains(tenantService.getBaseName(nodeRef.getStoreRef()).toString())) { 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<QName, AssociationDefinition> 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) { if (!storesToIgnore.contains(tenantService.getBaseName(nodeRef.getStoreRef()).toString())) { IntegrityEvent event = null; // check mandatory aspects event = new AspectsIntegrityEvent(nodeService, dictionaryService, nodeRef); save(event); } } /** * This handles the creation of secondary child associations. * * @see AssocSourceTypeIntegrityEvent * @see AssocTargetTypeIntegrityEvent * @see AssocSourceMultiplicityIntegrityEvent * @see AssocTargetMultiplicityIntegrityEvent * @see AssocTargetRoleIntegrityEvent */ public void onCreateChildAssociation(ChildAssociationRef childAssocRef, boolean isNew) { if (isNew) { return; } if (!storesToIgnore .contains(tenantService.getBaseName(childAssocRef.getChildRef().getStoreRef()).toString())) { 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) { if (!storesToIgnore .contains(tenantService.getBaseName(childAssocRef.getChildRef().getStoreRef()).toString())) { 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) { if (!storesToIgnore .contains(tenantService.getBaseName(nodeAssocRef.getSourceRef().getStoreRef()).toString())) { 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) { if (!storesToIgnore .contains(tenantService.getBaseName(nodeAssocRef.getSourceRef().getStoreRef()).toString())) { 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. * <p> * 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<IntegrityRecord> 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(sb.toString(), failures); } else { logger.warn(sb.toString()); // no exception } } /** * Loops through all the integrity events and checks integrity. * <p> * 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<IntegrityRecord> processAllEvents() { // the results ArrayList<IntegrityRecord> allIntegrityResults = new ArrayList<IntegrityRecord>(0); // generally empty // get all the events for the transaction (or unit of work) // duplicates have been elimiated Map<IntegrityEvent, IntegrityEvent> events = (Map<IntegrityEvent, IntegrityEvent>) AlfrescoTransactionSupport .getResource(KEY_EVENT_SET); if (events == null) { // no events were registered - nothing of significance happened return allIntegrityResults; } // failure results for the event List<IntegrityRecord> integrityRecords = new ArrayList<IntegrityRecord>(0); // cycle through the events, performing checking integrity for (IntegrityEvent event : events.keySet()) { try { event.checkIntegrity(integrityRecords); } catch (Throwable e) { // This means that integrity checking itself failed. This is serious. // There are some exceptions that can be handled by transaction retries, so // we attempt to handle these and let them get out to trigger the retry. // Thanks to Carina Lansing. Throwable retryThrowable = RetryingTransactionHelper.extractRetryCause(e); if (retryThrowable != null) { // The transaction will be retrying on this, so there's no need for the aggressive // reporting that would normally happen if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException(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; } @Override public void beforeCommit(boolean readOnly) { checkIntegrity(); } @Override public void beforeCompletion() { // NO-OP } @Override public void afterCommit() { // NO-OP } @Override public void afterRollback() { // NO-OP } }