Skip to content

Instantly share code, notes, and snippets.

@scottbcovert
Created February 25, 2016 15:17
Show Gist options
  • Save scottbcovert/641631263e98fe8468b6 to your computer and use it in GitHub Desktop.
Save scottbcovert/641631263e98fe8468b6 to your computer and use it in GitHub Desktop.
This code represents a centralized trigger framework based on concepts from Dan Appleman's 'Advanced Apex Programming' book and also leverages code written in Hari Krishnan's blog post on the topic (https://krishhari.wordpress.com/2013/07/22/an-architecture-framework-to-handle-triggers-in-the-force-com-platform/). This code is one piece of a lar…
This code represents a centralized trigger framework based on concepts from Dan Appleman's 'Advanced Apex Programming' book
and also leverages code written in Hari Krishnan's blog post on the topic (https://krishhari.wordpress.com/2013/07/22/an-architecture-framework-to-handle-triggers-in-the-force-com-platform/).
This code is one piece of a larger, open-sourced development framework available here: http://bit.ly/1mZE9b7
/**
* @author Scott Covert
* @date 11/9/2014
* @description Account Trigger Handler for After Update Trigger Context
* @see TriggerHandlerBase
*/
public class AccountAfterUpdateTriggerHandler extends TriggerHandlerBase {
/**
* @author Scott Covert
* @date 11/9/2014
* @description Main Entry point for trigger handling
* @param TriggerParameters Contains the trigger parameters
*/
public override void mainEntry(TriggerParameters tp) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.mainEntry');
process((List<Account>)tp.newList);
DiagnosticsInstrumentation.Pop();
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Process Accounts
* @param List A list of updated accounts
*/
private void process(List<Account> listNewAccounts) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.mainEntry');
sObjectsToUpdate.putAll(AccountHelper.updateWebsite(listNewAccounts));
DiagnosticsInstrumentation.Pop();
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description In Progress Entry point for trigger handling
* @param TriggerParameters Contains the trigger parameters
*/
public override void inProgressEntry(TriggerParameters tp) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.inProgressEntry');
System.debug('This is an example for reentrant code...');
DiagnosticsInstrumentation.Pop();
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Batch Apex Entry point for trigger handling
* @param TriggerParameters Contains the trigger parameters
*/
/*
public override void batchEntry(TriggerParameters tp) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.batchEntry');
System.debug('This is an example for batch entry code...');
DiagnosticsInstrumentation.Pop();
}
*/
/**
* @author Scott Covert
* @date 11/9/2014
* @description Future Entry point for trigger handling
* @param TriggerParameters Contains the trigger parameters
*/
/*
public override void futureEntry(TriggerParameters tp) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.futureEntry');
System.debug('This is an example for future entry code...');
DiagnosticsInstrumentation.Pop();
}
*/
/**
* @author Scott Covert
* @date 11/9/2014
* @description Scheduled Apex Entry point for trigger handling
* @param TriggerParameters Contains the trigger parameters
*/
/*
public override void scheduledEntry(TriggerParameters tp) {
DiagnosticsInstrumentation.Push('AccountAfterUpdateTriggerHandler.scheduledEntry');
System.debug('This is an example for scheduled entry code...');
DiagnosticsInstrumentation.Pop();
}
*/
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Helper Methods for Account Trigger Handling
*/
public with sharing class AccountHelper {
/**
* @author Scott Covert
* @date 11/9/2014
* @description Update website field of accounts to the Salesforce homepage
* @param List A list of accounts that should have their website field updated
* @param Map A map of accounts with their website field set to the Salesforce homepage
*/
public static map<Id,SObject> updateWebsite(list<Account> listNewAccounts){
map<Id,SObject> resultMap = new map<Id,SObject>();
for(Account acct : listNewAccounts) {
Account newAccount = new Account();
newAccount.Id = acct.Id;
newAccount.Website = 'www.salesforce.com';
resultMap.put(newAccount.Id, newAccount);
}
return resultMap;
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This class extends the TriggerDispatcherBase to provide the dispatching functionality for the trigger actions
* on the Account object. The event handlers support allowing and preventing actions for reentrant scenarios.
* This is controlled by the flag isBeforeXxxxx and isAfterXxxxx member variables. These variables need to be set
* to true before invoking the handlers and set to false after the invocation of the handlers. Resetting is MUST
* as otherwise unit tests MAY fail. The actual actions should be placed in the handlers (in a separate class).
*/
public class AccountTriggerDispatcher extends TriggerDispatcherBase {
/** Stores if before insert handler is processing */
private static Boolean isBeforeInsertProcessing = false;
/** Stores if before update handler is processing */
private static Boolean isBeforeUpdateProcessing = false;
/** Stores if before delete handler is processing */
private static Boolean isBeforeDeleteProcessing = false;
/** Stores if after insert handler is processing */
private static Boolean isAfterInsertProcessing = false;
/** Stores if after update handler is processing */
private static Boolean isAfterUpdateProcessing = false;
/** Stores if after delete handler is processing */
private static Boolean isAfterDeleteProcessing = false;
/** Stores if after undelete handler is processing */
private static Boolean isAfterUnDeleteProcessing = false;
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are inserted. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void beforeInsert(TriggerParameters tp) {
if(!isBeforeInsertProcessing) {
isBeforeInsertProcessing = true;
execute(new AccountBeforeInsertTriggerHandler(), tp, TriggerParameters.TriggerEvent.beforeInsert);
isBeforeInsertProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.beforeInsert);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are updated. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void beforeUpdate(TriggerParameters tp) {
if(!isBeforeUpdateProcessing) {
isBeforeUpdateProcessing = true;
execute(new AccountBeforeUpdateTriggerHandler(), tp, TriggerParameters.TriggerEvent.beforeUpdate);
isBeforeUpdateProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.beforeUpdate);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are deleted. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void beforeDelete(TriggerParameters tp) {
if(!isBeforeDeleteProcessing) {
isBeforeDeleteProcessing = true;
execute(new AccountBeforeDeleteTriggerHandler(), tp, TriggerParameters.TriggerEvent.beforeDelete);
isBeforeDeleteProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.beforeDelete);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are inserted. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void afterInsert(TriggerParameters tp) {
if(!isAfterInsertProcessing) {
isAfterInsertProcessing = true;
execute(new AccountAfterInsertTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterInsert);
isAfterInsertProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.afterInsert);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are updated. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void afterUpdate(TriggerParameters tp) {
if(!isAfterUpdateProcessing) {
isAfterUpdateProcessing = true;
execute(new AccountAfterUpdateTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterUpdate);
isAfterUpdateProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.afterUpdate);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are deleted. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void afterDelete(TriggerParameters tp) {
if(!isAfterDeleteProcessing) {
isAfterDeleteProcessing = true;
execute(new AccountAfterDeleteTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterDelete);
isAfterDeleteProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.afterDelete);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are undeleted. If there is an
* existing call running on the same context, the rentrant call will utilize the handler that was created
* in the original call.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual override void afterUnDelete(TriggerParameters tp) {
if(!isAfterUnDeleteProcessing) {
isAfterUnDeleteProcessing = true;
execute(new AccountAfterUnDeleteTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterUndelete);
isAfterUnDeleteProcessing = false;
}
else execute(null, tp, TriggerParameters.TriggerEvent.afterUndelete);
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Defines the interface for the trigger dispatching architecture.
*/
public interface ITriggerDispatcher {
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the bulk operations.
*/
void bulkBefore();
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the bulk operations.
*/
void bulkAfter();
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after completing the bulk operations.
*/
void andFinally();
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are inserted.
* @param TriggerParameters Contains the trigger parameters which includes the records that is getting inserted.
*/
void beforeInsert(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are updated.
* @param TriggerParameters Contains the trigger parameters which includes the records that is getting updated.
*/
void beforeUpdate(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are deleted.
* @param TriggerParameters Contains the trigger parameters which includes the records that is getting deleted.
*/
void beforeDelete(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are inserted.
* @param TriggerParameters Contains the trigger parameters which includes the records that got inserted.
*/
void afterInsert(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions before the records are updated.
* @param TriggerParameters Contains the trigger parameters which includes the records that got updated.
*/
void afterUpdate(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records got deleted.
* @param TriggerParameters Contains the trigger parameters which includes the records that got deleted.
*/
void afterDelete(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the trigger framework to carry out the actions after the records are undeleted.
* @param TriggerParameters Contains the trigger parameters which includes the records that got undeleted.
*/
void afterUnDelete(TriggerParameters tp);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Defines the interface for trigger handlers. Logic for the first time events are placed under the mainEntry
* method and the logic for the subsequent events raised on the same transaction (reentrant) are placed under
* the inProgressEntry method.
*/
public interface ITriggerHandler {
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called for the first time in the execution context.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
void mainEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called for the subsequent times in the same execution context.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
void inProgressEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by batch apex
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
void batchEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by asynchronous function
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
void futureEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by scheduled apex
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
void scheduledEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Updates the objects, if any.
*/
void updateObjects();
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This class implements the ITriggerDispatcher and acts as an adapter to avoid implementing all the
* ITriggerDispatcher methods.
*/
public virtual class TriggerDispatcherBase implements ITriggerDispatcher {
/** Stores before insert handler for trigger event */
private static ITriggerHandler beforeInserthandler;
/** Stores before update handler for trigger event */
private static ITriggerHandler beforeUpdatehandler;
/** Stores before delete handler for trigger event */
private static ITriggerHandler beforeDeleteHandler;
/** Stores after insert handler for trigger event */
private static ITriggerHandler afterInserthandler;
/** Stores after update handler for trigger event */
private static ITriggerHandler afterUpdatehandler;
/** Stores after delete handler for trigger event */
private static ITriggerHandler afterDeleteHandler;
/** Stores after undelete handler for trigger event */
private static ITriggerHandler afterUndeleteHandler;
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called prior to execution of a before trigger event. If you want
* to load any lookup data or cache the data, then this is the place that you need
* to put your code.
*/
public virtual void bulkBefore() {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called prior to execution of an after trigger event.
*/
public virtual void bulkAfter() {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called for records to be inserted during a BEFORE trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void beforeInsert(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called for records to be updated during a BEFORE trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void beforeUpdate(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called for records to be deleted during a BEFORE trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void beforeDelete(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called for records inserted during an AFTER trigger. Always put field validation
* in the 'After' methods in case another trigger has modified any values. The record is 'read only'
* at this point.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void afterInsert(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called iteratively for each record updated during an AFTER trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void afterUpdate(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called iteratively for each record deleted during an AFTER trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void afterDelete(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called prior to execution of a AFTER UNDELETE trigger.
* @param TriggerParameters Contains the trigger parameters
*/
public virtual void afterUnDelete(TriggerParameters tp) {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This method is called at the end of a before trigger event
*/
public virtual void andFinally() {}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by the event handlers. If this is the first call in the context, then this method will create a new
* instance of the appropriate handler and execute the mainEntry method. If there is an existing call runing
* on the same context, then this method will use the existing handler instance created by the original call
* and execute the inProgressEntry method.
* @param ITriggerHandler The trigger handler instance. The dispatcher need to pass an instance of the trigger handler, such
* as AccountAfterInsertTriggerHandler if this is the first call in a given context. If it is retry,
* then the dispatcher will need to pass null.
* @param TriggerParameters The trigger parameters passed by the framework.
* @param TriggerParameters.TriggerEvent The trigger event.
*/
protected void execute(ITriggerHandler handlerInstance, TriggerParameters tp, TriggerParameters.TriggerEvent tEvent) {
if(handlerInstance != null) {
if(tEvent == TriggerParameters.TriggerEvent.beforeInsert)
beforeInsertHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.beforeUpdate)
beforeUpdateHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.beforeDelete)
beforeDeleteHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.afterInsert)
afterInsertHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.afterUpdate)
afterUpdateHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.afterDelete)
afterDeleteHandler = handlerInstance;
if(tEvent == TriggerParameters.TriggerEvent.afterUnDelete)
afterUndeleteHandler = handlerInstance;
if (System.isBatch()){
handlerInstance.batchEntry(tp);
}
else if (System.isFuture()){
handlerInstance.futureEntry(tp);
}
else if (System.isScheduled()){
handlerInstance.scheduledEntry(tp);
}
else {
handlerInstance.mainEntry(tp);
}
handlerInstance.updateObjects();
}
else {
if(tEvent == TriggerParameters.TriggerEvent.beforeInsert)
beforeInsertHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.beforeUpdate)
beforeUpdateHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.beforeDelete)
beforeDeleteHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.afterInsert)
afterInsertHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.afterUpdate)
afterUpdateHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.afterDelete)
afterDeleteHandler.inProgressEntry(tp);
if(tEvent == TriggerParameters.TriggerEvent.afterUnDelete)
afterUndeleteHandler.inProgressEntry(tp);
}
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This factory creates the correct dispatcher and dispatches the trigger event(s) to the appropriate
* event handler(s). The dispatchers are automatically created using the Type API, hence dispatcher
* registration is not required for each dispatchers.
*/
public with sharing class TriggerFactory
{
/** Determines if a custom exception should be forced for test purposes */
@TestVisible private static Boolean FakeException = false;
/**
* @author Scott Covert
* @date 11/9/2014
* @description Creates the appropriate dispatcher and dispatches the trigger event to the dispatcher's event handler method.
* @param Schema.sObjectType Object type to process (SObject.sObjectType)
*/
public static void createTriggerDispatcher(Schema.sObjectType soType)
{
if (!ConfigService.KillSwitchEnabled && !ConfigService.BypassObjects.contains(soType)){
// Initial Push to Diagnostic Log
String beforeOrAfter = 'Before ';
if (Trigger.isAfter)
beforeOrAfter = 'After ';
String dmlState = 'Insert';
if (Trigger.isUpdate){
dmlState = 'Update';
}
else if (Trigger.isDelete){
dmlState = 'Delete';
}
else if (Trigger.isUndelete){
dmlState = 'Undelete';
}
String soTypeName = soType.getDescribe().getLocalName();
DiagnosticsInstrumentation.Push(beforeOrAfter + dmlState + ' Trigger for ' + soTypeName + ' Object');
try{
ITriggerDispatcher dispatcher = getTriggerDispatcher(soType);
if (dispatcher == null || FakeException)
throw new CustomException('No Trigger dispatcher registered for Object Type: ' + soType);
execute(dispatcher,soTypeName);
// Final Pop from Diagnostic Log
DiagnosticsInstrumentation.Pop();
}
catch(Exception ex){
DiagnosticsInstrumentation.DebugException(ex);
}
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Dispatches to the dispatcher's event handlers.
* @param ITriggerDispatcher A Trigger dispatcher that dispatches to the appropriate handlers
*/
private static void execute(ITriggerDispatcher dispatcher, String soTypeName)
{
TriggerParameters tp = new TriggerParameters(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap,
Trigger.isBefore, Trigger.isAfter, Trigger.isDelete,
Trigger.isInsert, Trigger.isUpdate, Trigger.isUnDelete, Trigger.isExecuting);
// Handle before trigger events
if (Trigger.isBefore) {
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.bulkBefore');
dispatcher.bulkBefore();
DiagnosticsInstrumentation.Pop();
if (Trigger.isDelete){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.beforeDelete');
dispatcher.beforeDelete(tp);
DiagnosticsInstrumentation.Pop();
}
else if (Trigger.isInsert){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.beforeInsert');
dispatcher.beforeInsert(tp);
DiagnosticsInstrumentation.Pop();
}
else if (Trigger.isUpdate){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.beforeUpdate');
dispatcher.beforeUpdate(tp);
DiagnosticsInstrumentation.Pop();
}
}
else // Handle after trigger events
{
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.bulkAfter');
dispatcher.bulkAfter();
DiagnosticsInstrumentation.Pop();
if (Trigger.isDelete){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.afterDelete');
dispatcher.afterDelete(tp);
DiagnosticsInstrumentation.Pop();
}
else if (Trigger.isInsert){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.afterInsert');
dispatcher.afterInsert(tp);
DiagnosticsInstrumentation.Pop();
}
else if (Trigger.isUpdate){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.afterUpdate');
dispatcher.afterUpdate(tp);
DiagnosticsInstrumentation.Pop();
}
else if (Trigger.isUnDelete){
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.afterUnDelete');
dispatcher.afterUnDelete(tp);
DiagnosticsInstrumentation.Pop();
}
}
DiagnosticsInstrumentation.Push(soTypeName + 'TriggerDispatcher.andFinally');
dispatcher.andFinally();
DiagnosticsInstrumentation.Pop();
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Gets the appropriate dispatcher based on the SObject. It constructs the instance of the dispatcher
* dynamically using the Type API. The name of the dispatcher has to follow this format:
* <ObjectName>TriggerDispatcher. For e.g. for the Account object, the dispatcher has to be named
* as AccountTriggerDispatcher. For the custom object Custom__c, the name should be CustomTriggerDispatcher.
* @param Schema.sObjectType Object type to create the dispatcher
* @return ITriggerDispatcher A trigger dispatcher if one exists, otherwise null.
*/
private static ITriggerDispatcher getTriggerDispatcher(Schema.sObjectType soType)
{
String originalTypeName = soType.getDescribe().getName();
String dispatcherTypeName = null;
if (originalTypeName.toLowerCase().endsWith('__c')) {
Integer index = originalTypeName.toLowerCase().indexOf('__c');
dispatcherTypeName = originalTypeName.substring(0, index).replace('_','') + 'TriggerDispatcher';
}
else
dispatcherTypeName = originalTypeName.replace('_','') + 'TriggerDispatcher';
Type obType = Type.forName(dispatcherTypeName);
ITriggerDispatcher dispatcher = (obType == null) ? null : (ITriggerDispatcher)obType.newInstance();
return dispatcher;
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This class implements the ITriggerHandler to provide abstract/virtual methods for the interface methods
* and so that the trigger handlers need to implement only the method that they have to. The only exception
* is the mainEntry, which is mandatory for the trigger handlers to implement.
*/
public abstract class TriggerHandlerBase implements ITriggerHandler {
/** Map of sobjects to update at the end of the trigger event */
protected Map<Id, SObject> sObjectsToUpdate = new Map<Id, SObject>();
/** Determines if a concurrency error should be simulated for test purposes */
@TestVisible private static List<Boolean> SimulateConcurrencyError;
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called for the first time in the execution context. The trigger handlers need to implement
* this method.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
public abstract void mainEntry(TriggerParameters tp);
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called for the subsequent times in the same execution context. The trigger handlers can choose
* to ignore if they don't need the reentrant feature.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
public virtual void inProgressEntry(TriggerParameters tp) {
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by batch apex. The trigger handlers can choose to ignore this method if they
* want batch entries to behave no differently than main entries.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
public virtual void batchEntry(TriggerParameters tp) {
mainEntry(tp);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by asynchronous function. The trigger handlers can choose to ignore this method if they
* want future entries to behave no differently than main entries.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
public virtual void futureEntry(TriggerParameters tp) {
mainEntry(tp);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Called by scheduled apex. The trigger handlers can choose to ignore this method if they
* want scheduled entries to behave no differently than main entries.
* @param TriggerParameters The trigger parameters such as the list of records before and after the update.
*/
public virtual void scheduledEntry(TriggerParameters tp) {
mainEntry(tp);
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description Updates the objects (if any). Concurrency errors will be silently logged while any other errors will throw an exception.
*/
public virtual void updateObjects() {
if(sObjectsToUpdate.size() > 0){
List<Database.Saveresult> dmlResults = Database.Update(sObjectsToUpdate.values(), false);
Map<Id, SObject> concurrencyFailures = new Map<Id, SObject>();
List<Database.Error> updateFailures = new List<Database.Error>();
for(Integer x = 0; x< sObjectsToUpdate.size(); x++) {
Database.Saveresult sr = dmlResults[x];
if(!sr.isSuccess() || (SimulateConcurrencyError!=null && SimulateConcurrencyError[x]))
{
for(Database.Error err : sr.getErrors())
{
if(err.getStatusCode() == StatusCode.UNABLE_TO_LOCK_ROW)
{
concurrencyFailures.put(sObjectsToUpdate.values()[x].Id,sObjectsToUpdate.values()[x]);
}
else{
updateFailures.add(err);
}
}
if(SimulateConcurrencyError!=null && SimulateConcurrencyError[x]) concurrencyFailures.put(sObjectsToUpdate.values()[x].Id,sObjectsToUpdate.values()[x]);
}
}
if(concurrencyFailures.size()>0) {
// Log Concurrency Error
String errorMessage = 'Concurrency failure on records ' + String.Join(new List<ID>(concurrencyFailures.keyset()),',');
DiagnosticsInstrumentation.Debug(errorMessage);
// Attempt Recovery
AsyncRequest__c ar = new AsyncRequest__c(AsyncType__c = RepeatingBatch.TYPE_DEFAULT,ScheduledTime__c = System.now(),Params__c=String.Join(new List<ID>(concurrencyFailures.keyset()),','));
insert ar;
ScheduleHandler.StartScheduler();
}
if(updateFailures.size()>0) {
throw new CustomException(updateFailures[0].getMessage());
}
}
}
}
/**
* @author Scott Covert
* @date 11/9/2014
* @description This class holds the trigger parameters.
*/
public class TriggerParameters {
/** An enum that represents the trigger event */
public Enum TriggerEvent { beforeInsert, beforeUpdate, beforeDelete, afterInsert, afterUpdate, afterDelete, afterUndelete }
/** Current trigger event */
public TriggerEvent tEvent;
/** Stores Trigger.old */
public List<SObject> oldList { get; private set; }
/** Stores Trigger.new */
public List<SObject> newList { get; private set; }
/** Stores Trigger.oldMap */
public Map<Id, SObject> oldMap { get; private set; }
/** Stores Trigger.newMap */
public Map<Id, SObject> newMap { get; private set; }
/** SObject for currently executing trigger */
public String triggerObject { get; private set; }
/** Stores execution state of trigger */
public Boolean isExecuting { get; private set; }
/**
* @author Scott Covert
* @date 11/9/2014
* @description Constructs the TriggerParameter object.
* @param List A list of records with the state of 'before' event.
* @param List A list of records with the state of 'after' event.
* @param Map A map of records with the state of 'before' event.
* @param Map A map of records with the state of 'after' event.
* @param Boolean A flag to indicate 'isBefore' event.
* @param Boolean A flag to indicate 'isAfter' event.
* @param Boolean A flag to indicate 'isDelete' event.
* @param Boolean A flag to indicate 'isInsert' event.
* @param Boolean A flag to indicate 'isUpdate' event.
* @param Boolean A flag to indicate 'isUnDelete' event.
* @param Boolean A flag to indicate 'isExecuting'.
*/
public TriggerParameters(List<SObject> olist, List<SObject> nlist, Map<Id, SObject> omap, Map<Id, SObject> nmap,
Boolean ib, Boolean ia, Boolean id, Boolean ii, Boolean iu, Boolean iud, Boolean ie) {
this.oldList = olist;
this.newList = nlist;
this.oldMap = omap;
this.newMap = nmap;
this.triggerObject = SObjectService.TypeName((this.oldList != null && this.oldList.size() > 0) ? this.oldList[0] : this.newList[0]);
if (ib & ii) tEvent = TriggerEvent.beforeInsert;
else if (ib && iu) tEvent = TriggerEvent.beforeUpdate;
else if (ib && id) tEvent = TriggerEvent.beforeDelete;
else if (ia && ii) tEvent = TriggerEvent.afterInsert;
else if (ia && iu) tEvent = TriggerEvent.afterUpdate;
else if (ia && id) tEvent = TriggerEvent.afterDelete;
else if (ia && iud) tEvent = TriggerEvent.afterUndelete;
isExecuting = ie;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment