From 7897cab21d1e1417c826b962fe3b8188e40154f8 Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Thu, 27 Jun 2024 11:20:18 -0500 Subject: [PATCH 1/4] Add proper namespace support --- .../main/default/classes/FinalizerHandler.cls | 11 ++++++++++- .../main/default/classes/MetadataTriggerHandler.cls | 12 ++++++++++-- ...alizer__mdt-DML Finalizer Layout.layout-meta.xml | 4 ++++ ...ction__mdt-Trigger Action Layout.layout-meta.xml | 4 ++++ .../fields/Apex_Class_Namespace__c.field-meta.xml | 13 +++++++++++++ .../fields/Apex_Class_Namespace__c.field-meta.xml | 13 +++++++++++++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml create mode 100644 trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml diff --git a/trigger-actions-framework/main/default/classes/FinalizerHandler.cls b/trigger-actions-framework/main/default/classes/FinalizerHandler.cls index a038a4a..69d008e 100644 --- a/trigger-actions-framework/main/default/classes/FinalizerHandler.cls +++ b/trigger-actions-framework/main/default/classes/FinalizerHandler.cls @@ -127,11 +127,20 @@ public with sharing virtual class FinalizerHandler { ) { Object dynamicInstance; String className = finalizerMetadata.Apex_Class_Name__c; + String namespace = finalizerMetadata.Apex_Class_Namespace__c; if (FinalizerHandler.isBypassed(className)) { return; } + try { - dynamicInstance = Type.forName(className).newInstance(); + try { + dynamicInstance = Type.forName(namespace, className).newInstance(); + } catch (Exception e) { + // fallback to package namespace to not break existing implementations that are distributed in a namespace + String[] parts = String.valueOf(FinalizerHandler.class).split('\\.', 2); + namespace = parts.size() == 2 ? parts[0] : ''; + dynamicInstance = Type.forName(namespace, className).newInstance(); + } } catch (System.NullPointerException e) { handleFinalizerException(INVALID_CLASS_ERROR_FINALIZER, className); } diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index a5e871b..8641849 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls @@ -86,6 +86,7 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem private static final String QUERY_TEMPLATE = String.join( new List{ 'SELECT Apex_Class_Name__c,', + 'Apex_Class_Namespace__c,', 'Order__c,', 'Flow_Name__c,', 'Bypass_Permission__c,', @@ -385,8 +386,15 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem for (Trigger_Action__mdt triggerMetadata : actionMetadata) { Object triggerAction; try { - triggerAction = Type.forName(triggerMetadata.Apex_Class_Name__c) - .newInstance(); + String namespace = triggerMetadata.Apex_Class_Namespace__c; + try { + triggerAction = Type.forName(namespace, triggerMetadata.Apex_Class_Name__c).newInstance(); + } catch (Exception e) { + // fallback to package namespace to not break existing implementations that are distributed in a namespace + String[] parts = String.valueOf(MetadataTriggerHandler.class).split('\\.', 2); + namespace = parts.size() == 2 ? parts[0] : ''; + triggerAction = Type.forName(namespace, triggerMetadata.Apex_Class_Name__c).newInstance(); + } if (triggerMetadata.Flow_Name__c != null) { ((TriggerActionFlow) triggerAction) .flowName = triggerMetadata.Flow_Name__c; diff --git a/trigger-actions-framework/main/default/layouts/DML_Finalizer__mdt-DML Finalizer Layout.layout-meta.xml b/trigger-actions-framework/main/default/layouts/DML_Finalizer__mdt-DML Finalizer Layout.layout-meta.xml index e87be1f..da67b8b 100644 --- a/trigger-actions-framework/main/default/layouts/DML_Finalizer__mdt-DML Finalizer Layout.layout-meta.xml +++ b/trigger-actions-framework/main/default/layouts/DML_Finalizer__mdt-DML Finalizer Layout.layout-meta.xml @@ -33,6 +33,10 @@ true + + Edit + Apex_Class_Namespace__c + Required Apex_Class_Name__c diff --git a/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml b/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml index 496c3e9..e4ddc52 100644 --- a/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml +++ b/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml @@ -33,6 +33,10 @@ true + + Edit + Apex_Class_Namespace__c + Required Apex_Class_Name__c diff --git a/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml b/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml new file mode 100644 index 0000000..0a15c87 --- /dev/null +++ b/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Apex_Class_Namespace__c + Enter the apex class namespace + false + DeveloperControlled + Enter the apex class namespace + + 15 + false + Text + false + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml new file mode 100644 index 0000000..0a15c87 --- /dev/null +++ b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Apex_Class_Namespace__c + Enter the apex class namespace + false + DeveloperControlled + Enter the apex class namespace + + 15 + false + Text + false + \ No newline at end of file From 6dc7f52fd9b716609939b5860b1c61adc9a0689f Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Thu, 27 Jun 2024 11:23:25 -0500 Subject: [PATCH 2/4] SubscriberControlled --- .../fields/Apex_Class_Namespace__c.field-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml b/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml index 0a15c87..8d0febd 100644 --- a/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml +++ b/trigger-actions-framework/main/default/objects/DML_Finalizer__mdt/fields/Apex_Class_Namespace__c.field-meta.xml @@ -3,7 +3,7 @@ Apex_Class_Namespace__c Enter the apex class namespace false - DeveloperControlled + SubscriberControlled Enter the apex class namespace 15 From 6df67ed742677568df0b07f60a53d77848d80a85 Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Thu, 27 Jun 2024 11:23:36 -0500 Subject: [PATCH 3/4] SubscriberControlled --- .../fields/Apex_Class_Namespace__c.field-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml index 0a15c87..8d0febd 100644 --- a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml +++ b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Apex_Class_Namespace__c.field-meta.xml @@ -3,7 +3,7 @@ Apex_Class_Namespace__c Enter the apex class namespace false - DeveloperControlled + SubscriberControlled Enter the apex class namespace 15 From 8a67d484356fef136f16ef19cfaf077d21cceb32 Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Mon, 1 Jul 2024 07:55:29 -0500 Subject: [PATCH 4/4] Add extra fallback and unit tests --- .../main/default/classes/FinalizerHandler.cls | 33 ++++++++++++++----- .../default/classes/FinalizerHandlerTest.cls | 21 ++++++++++++ .../classes/MetadataTriggerHandler.cls | 30 +++++++++++++---- .../classes/MetadataTriggerHandlerTest.cls | 20 +++++++++++ 4 files changed, 90 insertions(+), 14 deletions(-) diff --git a/trigger-actions-framework/main/default/classes/FinalizerHandler.cls b/trigger-actions-framework/main/default/classes/FinalizerHandler.cls index 69d008e..50b14ef 100644 --- a/trigger-actions-framework/main/default/classes/FinalizerHandler.cls +++ b/trigger-actions-framework/main/default/classes/FinalizerHandler.cls @@ -126,21 +126,38 @@ public with sharing virtual class FinalizerHandler { Context context ) { Object dynamicInstance; - String className = finalizerMetadata.Apex_Class_Name__c; String namespace = finalizerMetadata.Apex_Class_Namespace__c; + String className = finalizerMetadata.Apex_Class_Name__c; if (FinalizerHandler.isBypassed(className)) { return; } try { - try { - dynamicInstance = Type.forName(namespace, className).newInstance(); - } catch (Exception e) { - // fallback to package namespace to not break existing implementations that are distributed in a namespace - String[] parts = String.valueOf(FinalizerHandler.class).split('\\.', 2); + System.Type actionType = Type.forName(namespace, className); + /** Type.forName(fullyQualifiedName) allowed some messyness and ambiguity in dealing with namespace + * If config does not provide the correct namespace (likely if upgrading from older versions of this framework) we need to fallback in two scenarios + * - package and class namespaced but namespace wasn't specified + * - namespace is actually in the class field in the form namespace.classname + */ + // try shared Namespace + if( actionType == null ) { + // Get the namespace of the current class. + String[] parts = String.valueOf(MetadataTriggerHandler.class).split('\\.', 2); namespace = parts.size() == 2 ? parts[0] : ''; - dynamicInstance = Type.forName(namespace, className).newInstance(); - } + + // try again with the new namespace + actionType = Type.forName(namespace, className); + } + // try namespace in Class_Name field + if (actionType == null) { + String[] parts = className.split('\\.', 2); + if(parts.size() == 2) { + namespace = parts[0]; + className = parts[1]; + actionType = Type.forName(namespace, className); + } + } + dynamicInstance = actionType.newInstance(); } catch (System.NullPointerException e) { handleFinalizerException(INVALID_CLASS_ERROR_FINALIZER, className); } diff --git a/trigger-actions-framework/main/default/classes/FinalizerHandlerTest.cls b/trigger-actions-framework/main/default/classes/FinalizerHandlerTest.cls index 637fbb8..4eed964 100644 --- a/trigger-actions-framework/main/default/classes/FinalizerHandlerTest.cls +++ b/trigger-actions-framework/main/default/classes/FinalizerHandlerTest.cls @@ -73,6 +73,27 @@ private with sharing class FinalizerHandlerTest { ); } + @IsTest + private static void ambiguousNamespacedFinalizerShouldExecute() { + // Invalid namespace is the same as a missing one. + // Can't have mixed namespace unit tests so using invalid to trigger the same fallback behavior + handler.allFinalizers = new List{ + new DML_Finalizer__mdt( + Apex_Class_Namespace__c = 'NOT_A_NAMEPACE', + Apex_Class_Name__c = TEST_FOO_FINALIZER, + Bypass_Permission__c = BYPASS_PERMISSION + ) + }; + + handler.handleDynamicFinalizers(); + + System.Assert.areEqual( + FOO, + finalizerLedger[0], + 'The finalizer should be dynamically instantiated and executed' + ); + } + @IsTest private static void bypassedFinalizerShouldNotExecute() { handler.allFinalizers = new List{ diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index 8641849..e0bf441 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls @@ -385,16 +385,34 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem } for (Trigger_Action__mdt triggerMetadata : actionMetadata) { Object triggerAction; + String namespace = triggerMetadata.Apex_Class_Namespace__c; + String className = triggerMetadata.Apex_Class_Name__c; try { - String namespace = triggerMetadata.Apex_Class_Namespace__c; - try { - triggerAction = Type.forName(namespace, triggerMetadata.Apex_Class_Name__c).newInstance(); - } catch (Exception e) { - // fallback to package namespace to not break existing implementations that are distributed in a namespace + System.Type actionType = Type.forName(namespace, className); + /** Type.forName(fullyQualifiedName) allowed some messyness and ambiguity in dealing with namespace + * If config does not provide the correct namespace (likely if upgrading from older versions of this framework) we need to fallback in two scenarios + * - package and class namespaced but namespace wasn't specified + * - namespace is actually in the class field in the form namespace.classname + */ + // try shared Namespace + if( actionType == null ) { + // Get the namespace of the current class. String[] parts = String.valueOf(MetadataTriggerHandler.class).split('\\.', 2); namespace = parts.size() == 2 ? parts[0] : ''; - triggerAction = Type.forName(namespace, triggerMetadata.Apex_Class_Name__c).newInstance(); + + // try again with the new namespace + actionType = Type.forName(namespace, className); } + // try namespace in Class_Name field + if (actionType == null) { + String[] parts = className.split('\\.', 2); + if(parts.size() == 2) { + namespace = parts[0]; + className = parts[1]; + actionType = Type.forName(namespace, className); + } + } + triggerAction = actionType.newInstance(); if (triggerMetadata.Flow_Name__c != null) { ((TriggerActionFlow) triggerAction) .flowName = triggerMetadata.Flow_Name__c; diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls index b089be2..6a82e26 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls @@ -737,6 +737,26 @@ private class MetadataTriggerHandlerTest { System.Assert.areEqual(null, myException, EXCEPTION_SHOULD_NOT_BE_THROWN); } + @IsTest + private static void actionShouldFallbackIfBadNamespace() { + // Invalid namespace is the same as a missing one. + // Can't have mixed namespace unit tests so using invalid to trigger the same fallback behavior + handler.beforeInsertActionMetadata = new List{ + new Trigger_Action__mdt( + Apex_Class_Namespace__c = 'NOT_A_NAMEPACE', + Apex_Class_Name__c = TEST_BEFORE_INSERT, + Before_Insert__r = setting, + Before_Insert__c = setting.Id, + Order__c = 1, + Bypass_Execution__c = false + ) + }; + + handler.beforeInsert(handler.triggerNew); + + System.Assert.isTrue(executed, ACTION_SHOULD_EXECUTE); + } + public class TestBeforeInsert implements TriggerAction.BeforeInsert { public void beforeInsert(List newList) { MetadataTriggerHandlerTest.executed = true;