Untitled
unknown
plain_text
2 years ago
10 kB
1
Indexable
public virtual class TriggerHandler { // static map of handlername, times run() was invoked private static Map<String, LoopCount> loopCountMap; private static Set<String> bypassedHandlers; // the current context of the trigger, overridable in tests @TestVisible private TriggerContext context; // the current context of the trigger, overridable in tests @TestVisible private Boolean isTriggerExecuting; // static initialization static { loopCountMap = new Map<String, LoopCount>(); bypassedHandlers = new Set<String>(); } // constructor public TriggerHandler() { this.setTriggerContext(); } /*************************************** * public instance methods ***************************************/ // main method that will be called during execution public void run() { if (!validateRun()) { return; } addToLoopCount(); // dispatch to the correct handler method switch on this.context { when BEFORE_INSERT { this.beforeInsert(); } when BEFORE_UPDATE { this.beforeUpdate(); } when BEFORE_DELETE { this.beforeDelete(); } when AFTER_INSERT { this.afterInsert(); } when AFTER_UPDATE { this.afterUpdate(); } when AFTER_DELETE { this.afterDelete(); } when AFTER_UNDELETE { this.afterUndelete(); } } } public void setMaxLoopCount(Integer max) { String handlerName = getHandlerName(); if (!TriggerHandler.loopCountMap.containsKey(handlerName)) { TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max)); } else { TriggerHandler.loopCountMap.get(handlerName).setMax(max); } } public void clearMaxLoopCount() { this.setMaxLoopCount(-1); } /** * Detects whether any values in context records have changed for given fields as strings * Returns list of SObject records that have changes in the specified fields **/ public List<SObject> getChangedRecords(Set<String> fieldNames) { List<SObject> changedRecords = new List<SObject>(); for (SObject newRecord : Trigger.new) { Id recordId = (Id) newRecord.get('Id'); if (Trigger.oldMap == null || !Trigger.oldMap.containsKey(recordId)) { continue; } SObject oldRecord = Trigger.oldMap.get(recordId); for (String fieldName : fieldNames) { if (oldRecord.get(fieldName) != newRecord.get(fieldName)) { changedRecords.add(newRecord); break; // prevents the records from being added multiple times } } } return changedRecords; } /** * Detects whether any values in context records have changed for given fields as tokens * Returns list of SObject records that have changes in the specified fields **/ public List<SObject> getChangedRecords(Set<Schema.SObjectField> fieldTokens) { List<SObject> changedRecords = new List<SObject>(); for (SObject newRecord : Trigger.new) { Id recordId = (Id) newRecord.get('Id'); if (Trigger.oldMap == null || !Trigger.oldMap.containsKey(recordId)) { continue; } SObject oldRecord = Trigger.oldMap.get(recordId); for (Schema.SObjectField fieldToken : fieldTokens) { if (oldRecord.get(fieldToken) != newRecord.get(fieldToken)) { changedRecords.add(newRecord); break; // prevents the records from being added multiple times } } } return changedRecords; } /** * Detects whether given fields were set from null to any value **/ public List<SObject> getRecordsWhenFieldsIsSet(Set<Schema.SObjectField> fieldTokens) { List<SObject> changedRecords = new List<SObject>(); for (SObject newRecord : Trigger.new) { Id recordId = (Id) newRecord.get('Id'); if (Trigger.oldMap == null || !Trigger.oldMap.containsKey(recordId)) { continue; } SObject oldRecord = Trigger.oldMap.get(recordId); for (Schema.SObjectField fieldToken : fieldTokens) { if (oldRecord.get(fieldToken) == null && oldRecord.get(fieldToken) != newRecord.get(fieldToken)) { changedRecords.add(newRecord); break; // prevents the records from being added multiple times } } } return changedRecords; } /*************************************** * public static methods ***************************************/ public static void bypass(String handlerName) { TriggerHandler.bypassedHandlers.add(handlerName); } public static void clearBypass(String handlerName) { TriggerHandler.bypassedHandlers.remove(handlerName); } public static Boolean isBypassed(String handlerName) { return TriggerHandler.bypassedHandlers.contains(handlerName); } public static void clearAllBypasses() { TriggerHandler.bypassedHandlers.clear(); } /*************************************** * private instancemethods ***************************************/ @TestVisible private void setTriggerContext() { this.setTriggerContext(null, false); } @TestVisible private void setTriggerContext(String ctx, Boolean testMode) { if (!Trigger.isExecuting && !testMode) { this.isTriggerExecuting = false; return; } else { this.isTriggerExecuting = true; } if ((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) || (ctx != null && ctx == 'before insert')) { this.context = TriggerContext.BEFORE_INSERT; } else if ((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) || (ctx != null && ctx == 'before update')) { this.context = TriggerContext.BEFORE_UPDATE; } else if ((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) || (ctx != null && ctx == 'before delete')) { this.context = TriggerContext.BEFORE_DELETE; } else if ((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) || (ctx != null && ctx == 'after insert')) { this.context = TriggerContext.AFTER_INSERT; } else if ((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) || (ctx != null && ctx == 'after update')) { this.context = TriggerContext.AFTER_UPDATE; } else if ((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) || (ctx != null && ctx == 'after delete')) { this.context = TriggerContext.AFTER_DELETE; } else if ((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) || (ctx != null && ctx == 'after undelete')) { this.context = TriggerContext.AFTER_UNDELETE; } } // increment the loop count @TestVisible private void addToLoopCount() { String handlerName = getHandlerName(); if (TriggerHandler.loopCountMap.containsKey(handlerName)) { Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment(); if (exceeded) { Integer max = TriggerHandler.loopCountMap.get(handlerName).max; throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName); } } } // make sure this trigger should continue to run @TestVisible private Boolean validateRun() { if (!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } return !TriggerHandler.bypassedHandlers.contains(getHandlerName()); } @TestVisible private String getHandlerName() { return String.valueOf(this).substring(0, String.valueOf(this).indexOf(':')); } /*************************************** * context methods ***************************************/ // context-specific methods for override @TestVisible protected virtual void beforeInsert() { } @TestVisible protected virtual void beforeUpdate() { } @TestVisible protected virtual void beforeDelete() { } @TestVisible protected virtual void afterInsert() { } @TestVisible protected virtual void afterUpdate() { } @TestVisible protected virtual void afterDelete() { } @TestVisible protected virtual void afterUndelete() { } /*************************************** * inner classes ***************************************/ // inner class for managing the loop count per handler @TestVisible private class LoopCount { private Integer max; private Integer count; public LoopCount() { this.max = 5; this.count = 0; } public LoopCount(Integer max) { this.max = max; this.count = 0; } public Boolean increment() { this.count++; return this.exceeded(); } public Boolean exceeded() { return this.max >= 0 && this.count > this.max; } public Integer getMax() { return this.max; } public Integer getCount() { return this.count; } public void setMax(Integer max) { this.max = max; } } // possible trigger contexts @TestVisible private enum TriggerContext { BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE, AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE, AFTER_UNDELETE } // exception class public class TriggerHandlerException extends Exception { } }
Editor is loading...