From 0330f2cdb765383c9e3be28f3c4a38c4b0ac22c7 Mon Sep 17 00:00:00 2001 From: David Caruana Date: Wed, 12 Apr 2006 16:15:20 +0000 Subject: [PATCH] Transaction-level Policies. Modify AuditableAspect and ContentHits example to make use of transaction policies so they only trigger once. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2651 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/application-context.xml | 1 + config/alfresco/core-services-context.xml | 17 - config/alfresco/policy-context.xml | 34 ++ .../alfresco/repo/audit/AuditableAspect.java | 64 +-- .../repo/node/NodeServicePolicies.java | 6 +- .../org/alfresco/repo/policy/Behaviour.java | 19 +- .../alfresco/repo/policy/JavaBehaviour.java | 24 + .../java/org/alfresco/repo/policy/Policy.java | 11 + .../repo/policy/PolicyComponentImpl.java | 70 ++- .../PolicyComponentTransactionTest.java | 417 ++++++++++++++++++ .../repo/policy/PolicyDefinition.java | 17 + .../alfresco/repo/policy/PolicyFactory.java | 64 ++- .../policy/TransactionBehaviourQueue.java | 252 +++++++++++ .../TransactionInvocationHandlerFactory.java | 177 ++++++++ 14 files changed, 1102 insertions(+), 71 deletions(-) create mode 100644 config/alfresco/policy-context.xml create mode 100644 source/java/org/alfresco/repo/policy/PolicyComponentTransactionTest.java create mode 100644 source/java/org/alfresco/repo/policy/TransactionBehaviourQueue.java create mode 100644 source/java/org/alfresco/repo/policy/TransactionInvocationHandlerFactory.java diff --git a/config/alfresco/application-context.xml b/config/alfresco/application-context.xml index 28052756d1..5a8d463010 100644 --- a/config/alfresco/application-context.xml +++ b/config/alfresco/application-context.xml @@ -19,6 +19,7 @@ + diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index a2b9ab0a0f..127112d856 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -531,23 +531,6 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/audit/AuditableAspect.java b/source/java/org/alfresco/repo/audit/AuditableAspect.java index 5fc43e5797..617e3ad442 100644 --- a/source/java/org/alfresco/repo/audit/AuditableAspect.java +++ b/source/java/org/alfresco/repo/audit/AuditableAspect.java @@ -26,7 +26,9 @@ import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; @@ -100,9 +102,9 @@ public class AuditableAspect public void init() { // Create behaviours - onCreateAudit = new JavaBehaviour(this, "onCreateAudit"); - onAddAudit = new JavaBehaviour(this, "onAddAudit"); - onUpdateAudit = new JavaBehaviour(this, "onUpdateAudit"); + onCreateAudit = new JavaBehaviour(this, "onCreateAudit", NotificationFrequency.FIRST_EVENT); + onAddAudit = new JavaBehaviour(this, "onAddAudit", NotificationFrequency.FIRST_EVENT); + onUpdateAudit = new JavaBehaviour(this, "onUpdateAudit", NotificationFrequency.TRANSACTION_COMMIT); // Bind behaviours to node policies policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), ContentModel.ASPECT_AUDITABLE, onCreateAudit); @@ -132,37 +134,36 @@ public class AuditableAspect */ public void onAddAudit(NodeRef nodeRef, QName aspect) { + // Get the current properties + Map properties = this.nodeService.getProperties(nodeRef); + + // Set created / updated date + Date now = new Date(System.currentTimeMillis()); + properties.put(ContentModel.PROP_CREATED, now); + properties.put(ContentModel.PROP_MODIFIED, now); + + // Set creator (but do not override, if explicitly set) + String creator = (String)properties.get(ContentModel.PROP_CREATOR); + if (creator == null || creator.length() == 0) + { + creator = getUsername(); + properties.put(ContentModel.PROP_CREATOR, creator); + } + properties.put(ContentModel.PROP_MODIFIER, creator); + try { - this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); - - // Get the current properties - Map properties = this.nodeService.getProperties(nodeRef); - - // Set created / updated date - Date now = new Date(System.currentTimeMillis()); - properties.put(ContentModel.PROP_CREATED, now); - properties.put(ContentModel.PROP_MODIFIED, now); - - // Set creator (but do not override, if explicitly set) - String creator = (String)properties.get(ContentModel.PROP_CREATOR); - if (creator == null || creator.length() == 0) - { - creator = getUsername(); - properties.put(ContentModel.PROP_CREATOR, creator); - } - properties.put(ContentModel.PROP_MODIFIER, creator); - - // Set the updated property values + // Set the updated property values (but do not cascade to update audit behaviour) + onUpdateAudit.disable(); this.nodeService.setProperties(nodeRef, properties); - - if (logger.isDebugEnabled()) - logger.debug("Auditable node " + nodeRef + " created [created,modified=" + now + ";creator,modifier=" + creator + "]"); } finally { - this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); + onUpdateAudit.enable(); } + + if (logger.isDebugEnabled()) + logger.debug("Auditable node " + nodeRef + " created [created,modified=" + now + ";creator,modifier=" + creator + "]"); } /** @@ -172,11 +173,9 @@ public class AuditableAspect */ public void onUpdateAudit(NodeRef nodeRef) { + // Get the current properties try { - this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); - - // Get the current properties Map properties = this.nodeService.getProperties(nodeRef); // Set updated date @@ -193,9 +192,10 @@ public class AuditableAspect if (logger.isDebugEnabled()) logger.debug("Auditable node " + nodeRef + " updated [modified=" + now + ";modifier=" + modifier + "]"); } - finally + catch(InvalidNodeRefException e) { - this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); + if (logger.isDebugEnabled()) + logger.debug("Warning: Auditable node " + nodeRef + " no longer exists - cannot update"); } } diff --git a/source/java/org/alfresco/repo/node/NodeServicePolicies.java b/source/java/org/alfresco/repo/node/NodeServicePolicies.java index 0ad8e9210f..3745bae39d 100644 --- a/source/java/org/alfresco/repo/node/NodeServicePolicies.java +++ b/source/java/org/alfresco/repo/node/NodeServicePolicies.java @@ -21,8 +21,8 @@ import java.util.Map; import org.alfresco.repo.policy.AssociationPolicy; import org.alfresco.repo.policy.ClassPolicy; -import org.alfresco.service.cmr.repository.ChildAssociationRef; 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.StoreRef; import org.alfresco.service.namespace.QName; @@ -117,6 +117,10 @@ public interface NodeServicePolicies NodeRef nodeRef, Map before, Map after); + + static Arg ARG_0 = Arg.KEY; + static Arg ARG_1 = Arg.START_VALUE; + static Arg ARG_2 = Arg.END_VALUE; } public interface BeforeDeleteNodePolicy extends ClassPolicy diff --git a/source/java/org/alfresco/repo/policy/Behaviour.java b/source/java/org/alfresco/repo/policy/Behaviour.java index 2faa7a2dbb..adeb6a9b7d 100644 --- a/source/java/org/alfresco/repo/policy/Behaviour.java +++ b/source/java/org/alfresco/repo/policy/Behaviour.java @@ -29,6 +29,17 @@ package org.alfresco.repo.policy; */ public interface Behaviour { + + /** + * When should behaviour be notified? + */ + public enum NotificationFrequency + { + EVERY_EVENT, + FIRST_EVENT, + TRANSACTION_COMMIT + } + /** * Gets the requested policy interface onto the behaviour * @@ -51,5 +62,11 @@ public interface Behaviour * @return is the behaviour enabled (for this thread only) */ public boolean isEnabled(); - + + /** + * @return the notification + */ + public NotificationFrequency getNotificationFrequency(); + } + diff --git a/source/java/org/alfresco/repo/policy/JavaBehaviour.java b/source/java/org/alfresco/repo/policy/JavaBehaviour.java index d9bfe91f33..6adc0a90b2 100644 --- a/source/java/org/alfresco/repo/policy/JavaBehaviour.java +++ b/source/java/org/alfresco/repo/policy/JavaBehaviour.java @@ -43,6 +43,9 @@ public class JavaBehaviour implements Behaviour // The method name private String method; + + // Notification Frequency + private NotificationFrequency frequency; // Cache of interface proxies (by interface class) private Map proxies = new HashMap(); @@ -58,11 +61,23 @@ public class JavaBehaviour implements Behaviour * @param method the method name */ public JavaBehaviour(Object instance, String method) + { + this(instance, method, NotificationFrequency.EVERY_EVENT); + } + + /** + * Construct. + * + * @param instance the object instance holding the method + * @param method the method name + */ + public JavaBehaviour(Object instance, String method, NotificationFrequency frequency) { ParameterCheck.mandatory("Instance", instance); ParameterCheck.mandatory("Method", method); this.instance = instance; this.method = method; + this.frequency = frequency; } @@ -114,6 +129,15 @@ public class JavaBehaviour implements Behaviour return stack.search(hashCode()) == -1; } + /* (non-Javadoc) + * @see org.alfresco.repo.policy.Behaviour#getNotificationFrequency() + */ + public NotificationFrequency getNotificationFrequency() + { + return frequency; + } + + @Override public String toString() { diff --git a/source/java/org/alfresco/repo/policy/Policy.java b/source/java/org/alfresco/repo/policy/Policy.java index 828ac234fc..177e324144 100644 --- a/source/java/org/alfresco/repo/policy/Policy.java +++ b/source/java/org/alfresco/repo/policy/Policy.java @@ -30,4 +30,15 @@ public interface Policy * derived policies */ static String NAMESPACE = NamespaceService.ALFRESCO_URI; + + /** + * Argument Configuration + */ + public enum Arg + { + KEY, + START_VALUE, + END_VALUE + } + } diff --git a/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java b/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java index 1ec87220c2..9cbab19f92 100644 --- a/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java +++ b/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.alfresco.repo.policy.Policy.Arg; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -47,6 +48,7 @@ public class PolicyComponentImpl implements PolicyComponent // Policy interface annotations private static String ANNOTATION_NAMESPACE = "NAMESPACE"; + private static String ANNOTATION_ARG ="ARG_"; // Dictionary Service private DictionaryService dictionary; @@ -94,6 +96,17 @@ public class PolicyComponentImpl implements PolicyComponent } + /** + * Sets the transaction-based policy invocation handler + * + * @param factory + */ + public void setTransactionInvocationHandlerFactory(TransactionInvocationHandlerFactory factory) + { + PolicyFactory.setTransactionInvocationHandlerFactory(factory); + } + + /* (non-Javadoc) * @see org.alfresco.repo.policy.PolicyComponent#registerClassPolicy() */ @@ -509,8 +522,38 @@ public class PolicyComponentImpl implements PolicyComponent } String name = methods[0].getName(); + // Extract Policy Arguments + Class[] paramTypes = methods[0].getParameterTypes(); + Arg[] args = new Arg[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) + { + // Extract Policy Arg + args[i] = (i == 0) ? Arg.KEY : Arg.START_VALUE; + try + { + Field argMetadata = policyIF.getField(ANNOTATION_ARG + i); + if (!Arg.class.isAssignableFrom(argMetadata.getType())) + { + throw new PolicyException("ARG_" + i + " metadata incorrectly specified in policy " + policyIF.getCanonicalName()); + } + args[i] = (Arg)argMetadata.get(null); + if (i == 0 && (!args[i].equals(Arg.KEY))) + { + throw new PolicyException("ARG_" + i + " specified in policy " + policyIF.getCanonicalName() + " must be a key"); + } + } + catch(NoSuchFieldException e) + { + // Assume default ARG configuration + } + catch(IllegalAccessException e) + { + // Shouldn't get here (interface definitions must be accessible) + } + } + // Create Policy Definition - return new PolicyDefinitionImpl(QName.createQName(namespaceURI, name), policyIF); + return new PolicyDefinitionImpl(QName.createQName(namespaceURI, name), policyIF, args); } @@ -564,11 +607,13 @@ public class PolicyComponentImpl implements PolicyComponent { private QName policy; private Class policyIF; + private Arg[] args; - /*package*/ PolicyDefinitionImpl(QName policy, Class policyIF) + /*package*/ PolicyDefinitionImpl(QName policy, Class policyIF, Arg[] args) { this.policy = policy; this.policyIF = policyIF; + this.args = args; } /* (non-Javadoc) @@ -605,6 +650,27 @@ public class PolicyComponentImpl implements PolicyComponent return PolicyType.Association; } } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyDefinition#getArgument(int) + */ + public Arg getArgument(int index) + { + if (index < 0 || index > args.length -1) + { + throw new IllegalArgumentException("Argument index " + index + " is invalid"); + } + return args[index]; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyDefinition#getArguments() + */ + public Arg[] getArguments() + { + return args; + } + } diff --git a/source/java/org/alfresco/repo/policy/PolicyComponentTransactionTest.java b/source/java/org/alfresco/repo/policy/PolicyComponentTransactionTest.java new file mode 100644 index 0000000000..806cc834d3 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyComponentTransactionTest.java @@ -0,0 +1,417 @@ +/* + * 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.policy; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.repo.dictionary.DictionaryBootstrap; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.policy.Policy.Arg; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + + +/** + * Test Transaction-level Policies + */ +public class PolicyComponentTransactionTest extends TestCase +{ + private static final String TEST_MODEL = "org/alfresco/repo/policy/policycomponenttest_model.xml"; + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/policycomponenttest/1.0"; + private static QName BASE_TYPE = QName.createQName(TEST_NAMESPACE, "base"); + + private static ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext(); + private PolicyComponent policyComponent; + private TransactionService trxService; + private AuthenticationComponent authenticationComponent; + private ClassPolicyDelegate sideEffectDelegate; + + + @Override + protected void setUp() throws Exception + { + // initialise policy test model + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + bootstrapModels.add(TEST_MODEL); + bootstrap.setModels(bootstrapModels); + bootstrap.setDictionaryDAO((DictionaryDAO)applicationContext.getBean("dictionaryDAO")); + bootstrap.bootstrap(); + + // retrieve policy component + this.policyComponent = (PolicyComponent)applicationContext.getBean("policyComponent"); + this.trxService = (TransactionService) applicationContext.getBean("transactionComponent"); + this.authenticationComponent = (AuthenticationComponent)applicationContext.getBean("authenticationComponent"); + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // Register Policy + sideEffectDelegate = policyComponent.registerClassPolicy(SideEffectTestPolicy.class); + + // Bind Behaviour to side effect policy + QName policyName = QName.createQName(TEST_NAMESPACE, "sideEffect"); + Behaviour baseBehaviour = new JavaBehaviour(this, "sideEffectTest", NotificationFrequency.TRANSACTION_COMMIT); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, baseBehaviour); + } + + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + } + + + public void testStartTransactionPolicy() + throws Exception + { + ClassPolicyDelegate startDelegate = policyComponent.registerClassPolicy(StartTestPolicy.class); + + // Register Policy + QName policyName = QName.createQName(TEST_NAMESPACE, "start"); + PolicyDefinition definition = policyComponent.getRegisteredPolicy(PolicyType.Class, policyName); + assertNotNull(definition); + Arg arg0 = definition.getArgument(0); + assertEquals(Arg.KEY, arg0); + Arg arg1 = definition.getArgument(1); + assertEquals(Arg.KEY, arg1); + Arg arg2 = definition.getArgument(2); + assertEquals(Arg.START_VALUE, arg2); + Arg arg3 = definition.getArgument(3); + assertEquals(Arg.END_VALUE, arg3); + + // Bind Behaviour + Behaviour baseBehaviour = new JavaBehaviour(this, "startTransactionTest", NotificationFrequency.FIRST_EVENT); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, baseBehaviour); + + // Invoke Behaviour + UserTransaction userTransaction1 = trxService.getUserTransaction(); + try + { + userTransaction1.begin(); + + List results = new ArrayList(); + + StartTestPolicy basePolicy = startDelegate.get(BASE_TYPE); + String baseResult1 = basePolicy.start("1", "2", "value1a", "value2a", false, results); + TestResult result1 = new TestResult("startTransactionTest", "1", "2", "value1a", "value2a"); + assertEquals(result1.toString(), baseResult1); + assertEquals(1, results.size()); + assertEquals(result1, results.get(0)); + String baseResult2 = basePolicy.start("2", "1", "value1b", "value2b", false, results); + TestResult result2 = new TestResult("startTransactionTest", "2", "1", "value1b", "value2b"); + assertEquals(result2.toString(), baseResult2); + assertEquals(2, results.size()); + assertEquals(result2, results.get(1)); + String baseResult3 = basePolicy.start("1", "2", "value1c", "value2c", false, results); + assertEquals(result1.toString(), baseResult3); + assertEquals(2, results.size()); + + userTransaction1.commit(); + + assertEquals(2, results.size()); + assertEquals(result1, results.get(0)); + assertEquals(result2, results.get(1)); + } + catch(Exception e) + { + try { userTransaction1.rollback(); } catch (IllegalStateException ee) {} + throw e; + } + + // Invoke Behaviour + UserTransaction userTransaction2 = trxService.getUserTransaction(); + try + { + userTransaction2.begin(); + + List results = new ArrayList(); + + StartTestPolicy basePolicy = startDelegate.get(BASE_TYPE); + String baseResult1 = basePolicy.start("1", "2", "value1a", "value2a", true, results); + TestResult result1 = new TestResult("startTransactionTest", "1", "2", "value1a", "value2a"); + assertEquals(result1.toString(), baseResult1); + assertEquals(1, results.size()); + assertEquals(result1, results.get(0)); + String baseResult2 = basePolicy.start("2", "1", "value1b", "value2b", true, results); + TestResult result2 = new TestResult("startTransactionTest", "2", "1", "value1b", "value2b"); + assertEquals(result2.toString(), baseResult2); + assertEquals(2, results.size()); + assertEquals(result2, results.get(1)); + String baseResult3 = basePolicy.start("1", "2", "value1c", "value2c", true, results); + assertEquals(result1.toString(), baseResult3); + assertEquals(2, results.size()); + + TestResult result3 = new TestResult("sideEffectTest", "1", "2", "value1a", "value2a"); + TestResult result4 = new TestResult("sideEffectTest", "2", "1", "value1b", "value2b"); + + userTransaction2.commit(); + + assertEquals(4, results.size()); + assertEquals(result1, results.get(0)); + assertEquals(result2, results.get(1)); + assertEquals(result3, results.get(2)); + assertEquals(result4, results.get(3)); + } + catch(Exception e) + { + try { userTransaction2.rollback(); } catch (IllegalStateException ee) {} + throw e; + } + } + + + public void testEndTransactionPolicy() + throws Exception + { + ClassPolicyDelegate endDelegate = policyComponent.registerClassPolicy(EndTestPolicy.class); + + QName policyName = QName.createQName(TEST_NAMESPACE, "end"); + PolicyDefinition definition = policyComponent.getRegisteredPolicy(PolicyType.Class, policyName); + assertNotNull(definition); + Arg arg0 = definition.getArgument(0); + assertEquals(Arg.KEY, arg0); + Arg arg1 = definition.getArgument(1); + assertEquals(Arg.KEY, arg1); + Arg arg2 = definition.getArgument(2); + assertEquals(Arg.START_VALUE, arg2); + Arg arg3 = definition.getArgument(3); + assertEquals(Arg.END_VALUE, arg3); + + // Bind Behaviour + Behaviour baseBehaviour = new JavaBehaviour(this, "endTransactionTest", NotificationFrequency.TRANSACTION_COMMIT); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, baseBehaviour); + + UserTransaction userTransaction1 = trxService.getUserTransaction(); + try + { + userTransaction1.begin(); + + List results = new ArrayList(); + + // Invoke Behaviour + EndTestPolicy basePolicy = endDelegate.get(BASE_TYPE); + String baseResult1 = basePolicy.end("1", "2", "value1a", "value2a", false, results); + assertEquals(null, baseResult1); + assertEquals(0, results.size()); + String baseResult2 = basePolicy.end("2", "1", "value1b", "value2b", false, results); + assertEquals(null, baseResult2); + assertEquals(0, results.size()); + String baseResult3 = basePolicy.end("1", "2", "value1a", "value2c", false, results); + assertEquals(null, baseResult3); + assertEquals(0, results.size()); + + TestResult result1 = new TestResult("endTransactionTest", "1", "2", "value1a", "value2c"); + TestResult result2 = new TestResult("endTransactionTest", "2", "1", "value1b", "value2b"); + + userTransaction1.commit(); + + assertEquals(2, results.size()); + assertEquals(result1, results.get(0)); + assertEquals(result2, results.get(1)); + } + catch(Exception e) + { + try { userTransaction1.rollback(); } catch (IllegalStateException ee) {} + throw e; + } + + UserTransaction userTransaction2 = trxService.getUserTransaction(); + try + { + userTransaction2.begin(); + + List results = new ArrayList(); + + // Invoke Behaviour + EndTestPolicy basePolicy = endDelegate.get(BASE_TYPE); + String baseResult1 = basePolicy.end("1", "2", "value1a", "value2a", true, results); + assertEquals(null, baseResult1); + assertEquals(0, results.size()); + String baseResult2 = basePolicy.end("2", "1", "value1b", "value2b", true, results); + assertEquals(null, baseResult2); + assertEquals(0, results.size()); + String baseResult3 = basePolicy.end("1", "2", "value1a", "value2c", true, results); + assertEquals(null, baseResult3); + assertEquals(0, results.size()); + + TestResult result1 = new TestResult("endTransactionTest", "1", "2", "value1a", "value2c"); + TestResult result2 = new TestResult("endTransactionTest", "2", "1", "value1b", "value2b"); + TestResult result3 = new TestResult("sideEffectTest", "1", "2", "value1a", "value2c"); + TestResult result4 = new TestResult("sideEffectTest", "2", "1", "value1b", "value2b"); + + userTransaction2.commit(); + + assertEquals(4, results.size()); + assertEquals(result1, results.get(0)); + assertEquals(result2, results.get(1)); + assertEquals(result3, results.get(2)); + assertEquals(result4, results.get(3)); + + } + catch(Exception e) + { + try { userTransaction2.rollback(); } catch (IllegalStateException ee) {} + throw e; + } + } + + + // + // Behaviour Implementations + // + + public String startTransactionTest(String key1, String key2, String arg1, String arg2, boolean sideEffect, List results) + { + TestResult result = new TestResult("startTransactionTest", key1, key2, arg1, arg2); + results.add(result); + if (sideEffect) + { + SideEffectTestPolicy policy = sideEffectDelegate.get(BASE_TYPE); + policy.sideEffect(key1, key2, arg1, arg2, results); + } + return result.toString(); + } + + public String endTransactionTest(String key1, String key2, String arg1, String arg2, boolean sideEffect, List results) + { + TestResult result = new TestResult("endTransactionTest", key1, key2, arg1, arg2); + results.add(result); + if (sideEffect) + { + SideEffectTestPolicy policy = sideEffectDelegate.get(BASE_TYPE); + policy.sideEffect(key1, key2, arg1, arg2, results); + } + return result.toString(); + } + + public String sideEffectTest(String key1, String key2, String arg1, String arg2, List results) + { + TestResult result = new TestResult("sideEffectTest", key1, key2, arg1, arg2); + results.add(result); + return result.toString(); + } + + + // + // Policy Definitions + // + + public interface StartTestPolicy extends ClassPolicy + { + public String start(String key1, String key2, String arg1, String arg2, boolean sideEffect, List results); + + static String NAMESPACE = TEST_NAMESPACE; + static Arg ARG_0 = Arg.KEY; + static Arg ARG_1 = Arg.KEY; + static Arg ARG_2 = Arg.START_VALUE; + static Arg ARG_3 = Arg.END_VALUE; + } + + public interface EndTestPolicy extends ClassPolicy + { + public String end(String key1, String key2, String arg1, String arg2, boolean sideEffect, List results); + + static String NAMESPACE = TEST_NAMESPACE; + static Arg ARG_0 = Arg.KEY; + static Arg ARG_1 = Arg.KEY; + static Arg ARG_2 = Arg.START_VALUE; + static Arg ARG_3 = Arg.END_VALUE; + } + + public interface SideEffectTestPolicy extends ClassPolicy + { + public String sideEffect(String key1, String key2, String arg1, String arg2, List resultTest); + + static String NAMESPACE = TEST_NAMESPACE; + static Arg ARG_0 = Arg.KEY; + static Arg ARG_1 = Arg.KEY; + } + + + /** + * Result of Policy Invocation + */ + private class TestResult + { + private String trxId; + private String behaviour; + private String key1; + private String key2; + private String arg1; + private String arg2; + + /** + * Construct + * + * @param behaviour + * @param key1 + * @param key2 + * @param arg1 + * @param arg2 + */ + public TestResult(String behaviour, String key1, String key2, String arg1, String arg2) + { + this.trxId = AlfrescoTransactionSupport.getTransactionId(); + this.behaviour = behaviour; + this.key1 = key1; + this.key2 = key2; + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof TestResult) + { + TestResult that = (TestResult) obj; + return (this.trxId.equals(that.trxId) && + this.behaviour.equals(that.behaviour) && + this.key1.equals(that.key1) && + this.key2.equals(that.key2) && + this.arg1.equals(that.arg1) && + this.arg2.equals(that.arg2)); + } + else + { + return false; + } + } + + @Override + public String toString() + { + return "trxId=" + trxId + ", behaviour=" + behaviour + ", key1=" + key1 + ", key2=" + key2 + ", arg1=" + arg1 + ", arg2=" + arg2; + } + } + +} + diff --git a/source/java/org/alfresco/repo/policy/PolicyDefinition.java b/source/java/org/alfresco/repo/policy/PolicyDefinition.java index f2857dbee6..074f7ae2ed 100644 --- a/source/java/org/alfresco/repo/policy/PolicyDefinition.java +++ b/source/java/org/alfresco/repo/policy/PolicyDefinition.java @@ -16,6 +16,7 @@ */ package org.alfresco.repo.policy; +import org.alfresco.repo.policy.Policy.Arg; import org.alfresco.service.namespace.QName; @@ -46,7 +47,23 @@ public interface PolicyDefinition

/** * Gets the Policy type + * * @return the policy type */ public PolicyType getType(); + + /** + * Gets Policy Argument definition for the specified argument index + * + * @param index argument index + * @return ARG.KEY or ARG.START_VALUE or ARG.END_VALUE + */ + public Arg getArgument(int index); + + /** + * Gets Policy Argument definitions for all arguments in order of arguments + * @return + */ + public Arg[] getArguments(); + } diff --git a/source/java/org/alfresco/repo/policy/PolicyFactory.java b/source/java/org/alfresco/repo/policy/PolicyFactory.java index 661fb373f2..56ec034774 100644 --- a/source/java/org/alfresco/repo/policy/PolicyFactory.java +++ b/source/java/org/alfresco/repo/policy/PolicyFactory.java @@ -25,6 +25,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; + /** * A Policy Factory is responsible for creating Policy implementations. @@ -41,6 +43,12 @@ import java.util.List; // The policy interface class private Class

policyClass; + + // NOOP Invocation Handler + private static InvocationHandler NOOPHandler = new NOOPHandler(); + + // Transaction Invocation Handler Factory + private static TransactionInvocationHandlerFactory transactionHandlerFactory = null; /** @@ -55,6 +63,17 @@ import java.util.List; this.index = index; } + + /** + * Sets the Transaction Invocation Handler + * + * @param handlerFactory + */ + protected static void setTransactionInvocationHandlerFactory(TransactionInvocationHandlerFactory factory) + { + transactionHandlerFactory = factory; + } + /** * Gets the Policy class created by this factory @@ -95,6 +114,16 @@ import java.util.List; { Behaviour behaviour = behaviourDef.getBehaviour(); P policyIF = behaviour.getInterface(policyClass); + if (!(behaviour.getNotificationFrequency().equals(NotificationFrequency.EVERY_EVENT))) + { + // wrap behaviour in transaction proxy which deals with delaying invocation until necessary + if (transactionHandlerFactory == null) + { + throw new PolicyException("Transaction-level policies not supported as transaction support for the Policy Component has not been initialised."); + } + InvocationHandler trxHandler = transactionHandlerFactory.createHandler(behaviour, behaviourDef.getPolicyDefinition(), policyIF); + policyIF = (P)Proxy.newProxyInstance(policyClass.getClassLoader(), new Class[]{policyClass}, trxHandler); + } policyInterfaces.add(policyIF); } @@ -119,7 +148,7 @@ import java.util.List; else if (policyList.size() == 0) { return (P)Proxy.newProxyInstance(policyClass.getClassLoader(), - new Class[]{policyClass}, new NOOPHandler()); + new Class[]{policyClass}, NOOPHandler); } else { @@ -157,7 +186,7 @@ import java.util.List; return null; } } - + /** * Multi-policy Invocation Handler. @@ -166,8 +195,7 @@ import java.util.List; * * @param

policy interface */ - @SuppressWarnings("hiding") - private static class MultiHandler

implements InvocationHandler, PolicyList + private static class MultiHandler

implements InvocationHandler, PolicyList { private Collection

policyInterfaces; @@ -186,13 +214,13 @@ import java.util.List; */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // Handle PolicyList level methods - if (method.getDeclaringClass().equals(PolicyList.class)) - { - return method.invoke(this, args); - } - - // Handle Object level methods + // Handle PolicyList level methods + if (method.getDeclaringClass().equals(PolicyList.class)) + { + return method.invoke(this, args); + } + + // Handle Object level methods if (method.getName().equals("toString")) { return toString() + ": wrapped " + policyInterfaces.size() + " policies"; @@ -222,13 +250,13 @@ import java.util.List; } } - /* (non-Javadoc) - * @see org.alfresco.repo.policy.PolicyList#getPolicies() - */ - public Collection getPolicies() - { - return policyInterfaces; - } + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyList#getPolicies() + */ + public Collection getPolicies() + { + return policyInterfaces; + } } } diff --git a/source/java/org/alfresco/repo/policy/TransactionBehaviourQueue.java b/source/java/org/alfresco/repo/policy/TransactionBehaviourQueue.java new file mode 100644 index 0000000000..bf46e807aa --- /dev/null +++ b/source/java/org/alfresco/repo/policy/TransactionBehaviourQueue.java @@ -0,0 +1,252 @@ +/* + * 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.policy; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.policy.Policy.Arg; +import org.alfresco.repo.rule.RuleTransactionListener; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.util.GUID; + + +/** + * Transaction Behaviour Queue. + * + * Responsible for keeping a record of behaviours to execute at the end of a transaction. + */ +public class TransactionBehaviourQueue implements TransactionListener +{ + /** Id used in equals and hash */ + private String id = GUID.generate(); + + // Transaction Keys for Behaviour Execution state + private static final String QUEUE_CONTEXT_KEY = TransactionBehaviourQueue.class.getName() + ".context"; + + + /** + * Queue a behaviour for end-of-transaction execution + * + * @param

+ * @param behaviour + * @param definition + * @param policyInterface + * @param method + * @param args + */ + @SuppressWarnings("unchecked") + public

void queue(Behaviour behaviour, PolicyDefinition

definition, P policyInterface, Method method, Object[] args) + { + // Construct queue context, if required + QueueContext queueContext = (QueueContext)AlfrescoTransactionSupport.getResource(QUEUE_CONTEXT_KEY); + if (queueContext == null) + { + queueContext = new QueueContext(); + AlfrescoTransactionSupport.bindResource(QUEUE_CONTEXT_KEY, queueContext); + AlfrescoTransactionSupport.bindListener(this); + } + + // Determine if behaviour instance has already been queued + Integer instanceKey = createInstanceKey(behaviour, definition.getArguments(), args); + ExecutionContext executionContext = queueContext.index.get(instanceKey); + if (executionContext == null) + { + // Create execution context for behaviour + executionContext = new ExecutionContext

(); + executionContext.method = method; + executionContext.args = args; + executionContext.policyInterface = policyInterface; + + // Defer or execute now? + if (!queueContext.committed) + { + // queue behaviour for deferred execution + queueContext.queue.offer(executionContext); + } + else + { + // execute now + execute(executionContext); + } + queueContext.index.put(instanceKey, executionContext); + } + else + { + // Update behaviour instance execution context, in particular, argument state that is marked END_TRANSACTION + Arg[] argDefs = definition.getArguments(); + for (int i = 0; i < argDefs.length; i++) + { + if (argDefs[i].equals(Arg.END_VALUE)) + { + executionContext.args[i] = args[i]; + } + } + } + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.transaction.TransactionListener#flush() + */ + public void flush() + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.transaction.TransactionListener#beforeCommit(boolean) + */ + @SuppressWarnings("unchecked") + public void beforeCommit(boolean readOnly) + { + QueueContext queueContext = (QueueContext)AlfrescoTransactionSupport.getResource(QUEUE_CONTEXT_KEY); + ExecutionContext context = queueContext.queue.poll(); + while (context != null) + { + execute(context); + context = queueContext.queue.poll(); + } + queueContext.committed = true; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.transaction.TransactionListener#beforeCompletion() + */ + public void beforeCompletion() + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.transaction.TransactionListener#afterCommit() + */ + public void afterCommit() + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.transaction.TransactionListener#afterRollback() + */ + public void afterRollback() + { + } + + /** + * Create an instance key for the behaviour based on the "key" arguments passed in + * + * @param argDefs definitions of behaviour arguments + * @param args the argument values + * @return the key + */ + private Integer createInstanceKey(Behaviour behaviour, Arg[] argDefs, Object[] args) + { + int key = behaviour.hashCode(); + for (int i = 0; i < argDefs.length; i++) + { + if (argDefs[i].equals(Arg.KEY)) + { + key = (37 * key) + args[i].hashCode(); + } + } + return new Integer(key); + } + + /** + * Execute behaviour as described in execution context + * + * @param context + */ + private void execute(ExecutionContext context) + { + try + { + context.method.invoke(context.policyInterface, context.args); + } + catch (IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("Failed to execute transaction-level behaviour " + context.method + " in transaction " + AlfrescoTransactionSupport.getTransactionId(), e); + } + catch (IllegalAccessException e) + { + throw new AlfrescoRuntimeException("Failed to execute transaction-level behaviour " + context.method + " in transaction " + AlfrescoTransactionSupport.getTransactionId(), e); + } + catch (InvocationTargetException e) + { + throw new AlfrescoRuntimeException("Failed to execute transaction-level behaviour " + context.method + " in transaction " + AlfrescoTransactionSupport.getTransactionId(), e.getTargetException()); + } + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + return this.id.hashCode(); + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof RuleTransactionListener) + { + TransactionBehaviourQueue that = (TransactionBehaviourQueue) obj; + return (this.id.equals(that.id)); + } + else + { + return false; + } + } + + /** + * Behaviour execution Context + * + * @param

+ */ + private class ExecutionContext

+ { + Method method; + Object[] args; + P policyInterface; + } + + + /** + * Queue Context + */ + private class QueueContext + { + // TODO: Tune sizes + Queue queue = new LinkedList(); + Map index = new HashMap(); + boolean committed = false; + } + +} diff --git a/source/java/org/alfresco/repo/policy/TransactionInvocationHandlerFactory.java b/source/java/org/alfresco/repo/policy/TransactionInvocationHandlerFactory.java new file mode 100644 index 0000000000..96670cc389 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/TransactionInvocationHandlerFactory.java @@ -0,0 +1,177 @@ +/* + * 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.policy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.policy.Policy.Arg; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; + + +/** + * Factory for creating transaction-aware behaviour invocation handlers. + */ +public class TransactionInvocationHandlerFactory +{ + /** Transaction Key for Behaviour Execution state */ + private final static String EXECUTED_KEY = TransactionHandler.class.getName() + ".executed"; + + /** Transaction behaviour Queue */ + private TransactionBehaviourQueue queue; + + + /** + * Construct + * + * @param queue behaviour queue + */ + public TransactionInvocationHandlerFactory(TransactionBehaviourQueue queue) + { + this.queue = queue; + } + + + /** + * Create Invocation Handler + * + * @param

+ * @param behaviour + * @param definition + * @param policyInterface + * @return invocation handler + */ + public

InvocationHandler createHandler(Behaviour behaviour, PolicyDefinition

definition, P policyInterface) + { + return new TransactionHandler

(behaviour, definition, policyInterface); + } + + + /** + * Transaction Invocation Handler. + * + * @param

policy interface + */ + private class TransactionHandler

implements InvocationHandler + { + private Behaviour behaviour; + private PolicyDefinition

definition; + private P policyInterface; + + /** + * Construct + */ + public TransactionHandler(Behaviour behaviour, PolicyDefinition

definition, P policyInterface) + { + this.behaviour = behaviour; + this.definition = definition; + this.policyInterface = policyInterface; + } + + /* (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + @SuppressWarnings("unchecked") + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + // Handle Object level methods + if (method.getName().equals("toString")) + { + return policyInterface.toString(); + } + else if (method.getName().equals("hashCode")) + { + return policyInterface.hashCode(); + } + else if (method.getName().equals("equals")) + { + return policyInterface.equals(args[0]); + } + + // Invoke policy based on its notification frequency + Object result = null; + if (behaviour.getNotificationFrequency().equals(NotificationFrequency.FIRST_EVENT)) + { + Map executedBehaviours = (Map)AlfrescoTransactionSupport.getResource(EXECUTED_KEY); + if (executedBehaviours == null) + { + executedBehaviours = new HashMap(); + AlfrescoTransactionSupport.bindResource(EXECUTED_KEY, executedBehaviours); + } + + Integer behaviourKey = createInstanceKey(behaviour, definition.getArguments(), args); + if (executedBehaviours.containsKey(behaviourKey) == false) + { + // Invoke behavior for first time and mark as executed + try + { + result = method.invoke(policyInterface, args); + executedBehaviours.put(behaviourKey, result); + } + catch (InvocationTargetException e) + { + throw e.getTargetException(); + } + } + else + { + // Return result of previous execution + result = executedBehaviours.get(behaviourKey); + } + } + else if (behaviour.getNotificationFrequency().equals(NotificationFrequency.TRANSACTION_COMMIT)) + { + // queue policy invocation for end of transaction + queue.queue(behaviour, definition, policyInterface, method, args); + } + else + { + // Note: shouldn't get here + throw new PolicyException("Invalid Notification frequency " + behaviour.getNotificationFrequency()); + } + + return result; + } + + + /** + * Create an instance key for the behaviour based on the "key" arguments passed in + * + * @param argDefs definitions of behaviour arguments + * @param args the argument values + * @return the key + */ + private Integer createInstanceKey(Behaviour behaviour, Arg[] argDefs, Object[] args) + { + int key = behaviour.hashCode(); + for (int i = 0; i < argDefs.length; i++) + { + if (argDefs[i].equals(Arg.KEY)) + { + key = (37 * key) + args[i].hashCode(); + } + } + return new Integer(key); + } + + } + +} \ No newline at end of file