├── TriggerX
├── salesforce.schema
├── src
│ ├── classes
│ │ ├── TriggerX.cls-meta.xml
│ │ ├── TriggerXTest.cls-meta.xml
│ │ ├── AccountSampleHandler.cls-meta.xml
│ │ ├── AccountSampleHandler.cls
│ │ ├── TriggerXTest.cls
│ │ └── TriggerX.cls
│ ├── triggers
│ │ ├── AccountSample.trigger-meta.xml
│ │ └── AccountSample.trigger
│ ├── package.xml
│ └── objects
│ │ └── TRIGGER_CONTROL__c.object
└── .project
├── .gitignore
└── README.md
/TriggerX/salesforce.schema:
--------------------------------------------------------------------------------
1 | place holder
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .settings/
3 |
4 | .DS_Store*
5 | ehthumbs.db
6 | Icon?
7 | Thumbs.db
--------------------------------------------------------------------------------
/TriggerX/src/classes/TriggerX.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 27.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/TriggerX/src/classes/TriggerXTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 27.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/TriggerX/src/classes/AccountSampleHandler.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 27.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/TriggerX/src/triggers/AccountSample.trigger-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 27.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/TriggerX/src/triggers/AccountSample.trigger:
--------------------------------------------------------------------------------
1 | /**
2 | * TriggerX by Sebastian Wagner 2013
3 | *
4 | * Redistribution and use in source and binary forms, with or without modification, are permitted.
5 | *
6 | * Sample Trigger for TriggerX implementation
7 | * http://github.com/sebwagner/TriggerX
8 | */
9 | trigger AccountSample on Account (after update, before update) {
10 |
11 | TriggerX.handleTrigger(AccountSampleHandler.class);
12 |
13 | }
--------------------------------------------------------------------------------
/TriggerX/src/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AccountSampleHandler
5 | TriggerX
6 | TriggerXTest
7 | ApexClass
8 |
9 |
10 | AccountSample
11 | ApexTrigger
12 |
13 |
14 | *
15 | CustomObject
16 |
17 | 27.0
18 |
19 |
--------------------------------------------------------------------------------
/TriggerX/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | TriggerX
4 |
5 |
6 |
7 |
8 |
9 | com.salesforce.ide.builder.default
10 |
11 |
12 |
13 |
14 | com.salesforce.ide.builder.online
15 |
16 |
17 |
18 |
19 |
20 | com.salesforce.ide.nature.default
21 | com.salesforce.ide.nature.online
22 |
23 |
24 |
--------------------------------------------------------------------------------
/TriggerX/src/objects/TRIGGER_CONTROL__c.object:
--------------------------------------------------------------------------------
1 |
2 |
3 | List
4 | Public
5 | Control trigger event execution. Save settings by the name of the class you want to control. If now record exists, all trigger events are considered as enabled
6 | false
7 | false
8 |
9 | AFTER_DELETE__c
10 | true
11 | false
12 | Uncheck to turn off AFTER DELETE
13 |
14 | Checkbox
15 |
16 |
17 | AFTER_INSERT__c
18 | true
19 | false
20 | Uncheck to disable AFTER INSERT
21 |
22 | Checkbox
23 |
24 |
25 | AFTER_UNDELETE__c
26 | true
27 | false
28 | Uncheck to disable AFTER UNDELETE
29 |
30 | Checkbox
31 |
32 |
33 | AFTER_UPDATE__c
34 | true
35 | false
36 | Uncheck to disable AFTER UPDATE
37 |
38 | Checkbox
39 |
40 |
41 | BEFORE_DELETE__c
42 | true
43 | false
44 | Uncheck to disable BEFORE DELETE
45 |
46 | Checkbox
47 |
48 |
49 | BEFORE_INSERT__c
50 | true
51 | false
52 | Uncheck to disable BEFORE_INSERT
53 |
54 | Checkbox
55 |
56 |
57 | BEFORE_UPDATE__c
58 | true
59 | false
60 | Uncheck to disable BEFORE UPDATE
61 |
62 | Checkbox
63 |
64 |
65 | DESCRIPTION__c
66 | false
67 |
68 | false
69 | TextArea
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/TriggerX/src/classes/AccountSampleHandler.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * TriggerX by Sebastian Wagner 2013
3 | *
4 | * Redistribution and use in source and binary forms, with or without modification, are permitted.
5 | *
6 | * Sample Implementation of a TriggerHandler for TriggerX
7 | * http://github.com/sebwagner/TriggerX
8 | */
9 | public class AccountSampleHandler extends TriggerX {
10 |
11 |
12 | public override void onBeforeUpdate(Map triggerOldMap){
13 |
14 | Set updatedIds = TriggerX.getUpdatedIds();
15 |
16 | for (Account record:(Account[])records)
17 | {
18 | // skip records which have been updated within the current context
19 | if (updatedIds.contains(record.Id)) continue;
20 |
21 | }
22 | // more logic goes here
23 | }
24 |
25 |
26 | // Method executed to perform AFTER_UPDATE operations, overwrite if applicable
27 | public override void onAfterUpdate(Map triggerOldMap){
28 |
29 | Map withAddressUpdate = new Map();
30 |
31 | sObjectField[] addressFields = new sObjectField[]{
32 | Account.BillingStreet
33 | , Account.BillingPostalCode
34 | , Account.BillingCity
35 | , Account.BillingCountry};
36 |
37 | // use prefiltered list
38 | for (Account record:(Account[])getNonRecursiveUpdates())
39 | {
40 | // check if the address has change and keep record for further processing if so
41 | if (TriggerX.hasChangedFields(addressFields, record, triggerOldMap.get(record.Id)))
42 | {
43 | withAddressUpdate.put(record.Id,record);
44 | }
45 | }
46 |
47 | // track updated ids to prevent recursive updates
48 | TriggerX.addUpdatedIds(triggerOldMap.keySet());
49 |
50 | if (withAddressUpdate.size() > 0)
51 | {
52 | updateContactAddressFromAccount(withAddressUpdate);
53 | }
54 |
55 | // more logic goes here
56 | }
57 |
58 |
59 | private void updateContactAddressFromAccount(Map accountMap){
60 |
61 | Contact[] contacts = [
62 | select Id
63 | , AccountId
64 | from Contact
65 | where AccountId IN: accountMap.keySet()];
66 |
67 | for (Contact record:contacts)
68 | {
69 |
70 | Account acc = accountMap.get(record.AccountId);
71 |
72 | record.MailingCity = acc.BillingCity;
73 | record.MailingCountry = acc.BillingCountry;
74 | record.MailingPostalCode = acc.BillingPostalCode;
75 | record.MailingStreet = acc.BillingStreet;
76 | }
77 |
78 | // disable Account updates before saving the contacts, so we dont have to run it twice
79 | TriggerX.disable(AccountSampleHandler.class, TriggerX.getUpdateEvents());
80 |
81 | update contacts;
82 |
83 | // reenable updates for futher processing
84 | TriggerX.enable(AccountSampleHandler.class, TriggerX.getUpdateEvents());
85 | }
86 |
87 |
88 | @isTest
89 | private static void stupidTest(){
90 |
91 | // just cover the trigger
92 | Account record = new Account(Name = 'TEST_ACCOUNT');
93 | try
94 | {
95 | insert record;
96 |
97 | // create contact
98 | Contact contact = new Contact(
99 | AccountId = record.Id
100 | , LastName = 'LAST_NAME');
101 | insert contact;
102 |
103 | record.BillingStreet = 'Street';
104 | update record;
105 |
106 | Map accMap = new Map();
107 |
108 | TriggerX handler = new AccountSampleHandler().doConstruct(new sObject[]{record});
109 | handler.onBeforeUpdate(accMap);
110 | handler.onAfterUpdate(accMap);
111 | }
112 | catch(Exception ex)
113 | {
114 |
115 | }
116 | }
117 |
118 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | **Table of Contents**
7 | [Features](#features)
8 | _[One-Line Trigger Code](#one-line-trigger-code)
9 | _[Evaluate Field Value Changes](#evaluate-field-value-changes)
10 | _[Dynamic Event Control](#dynamic-event-control)
11 | _[Recursion Control](#recursion-control)
12 |
13 | [How To Use](#how-to-use)
14 | [Test Coverage](#test-coverage)
15 | [License](#license)
16 |
17 | # Features#
18 |
19 |
20 | ## One-Line Trigger Code ##
21 |
22 | Execute Triggers with a single line of code
23 |
24 | ```java
25 | TriggerX.handleTrigger(AccountSampleHandler.class)
26 | ```
27 |
28 | ## Evaluate Field Value Changes ##
29 | Often triggers contain conditional logic that checks for changed field values
30 |
31 | ```java
32 | if (record.CloseDate != recordOld.CloseDate
33 | || record.OwnerId != recordOld.CloseDate
34 | || record.StageName != recordOld.StageName
35 | || record.Type != recordOld.Type){
36 |
37 | // logic executed when condition is true
38 | }
39 | ```
40 | . Using the hasChangedFields and getChangedFields methods you just pass in a list of fields (String or sObjectField) for which changes should be evaluated
41 |
42 | ```java
43 | // use string field names
44 | String[] stringFieldZ = new String[]{'StageName','CloseDate','OwnerId','Type'};
45 |
46 | if (TriggerX.hasChangedFields(stringFieldZ,record,recordOld)){
47 | // logic executed when condition is true
48 | }
49 |
50 | // or sObjectFields
51 | sObjectField[] fieldZ = new sObjectField[]{Opportunity.StageName, Opportunity.CloseDate, Opportunity.OwnerId, Opportunity.Type};
52 |
53 | for (sObjectField field:TriggerX.getChangedFields(fieldZ,record,recordOld)){
54 | // process field
55 | }
56 | ```
57 |
58 | ## Dynamic Event Control ##
59 |
60 | Turn execution of events within the runtime context on and off. Use for instance when you perform operations that cause updates on multiple hierachy levels of the same ObjectType see also *Recursion Control*
61 |
62 | **control single events**
63 |
64 | ```java
65 | TriggerX.disable(AccountSampleHandler.class,TriggerX.EventType.AFTER_UPDATE)
66 | TriggerX.enable(AccountSampleHandler.class,TriggerX.EventType.AFTER_UPDATE)
67 | ```
68 |
69 | **control multiple events**
70 |
71 | ```java
72 | TriggerX.disable(AccountSampleHandler.class
73 | ,new TriggerX.EventType[]{
74 | TriggerX.EventType.AFTER_INSERT
75 | , TriggerX.EventType.BEFORE_UPDATE
76 | , TriggerX.EventType.AFTER_UPDATE})
77 |
78 | TriggerX.enable(AccountSampleHandler.class
79 | ,new TriggerX.EventType[]{
80 | TriggerX.EventType.BEFORE_UPDATE
81 | , TriggerX.EventType.AFTER_UPDATE})
82 |
83 | ```
84 | **control entire trigger via code**
85 |
86 | ```java
87 | TriggerX.disable(AccountSampleHandler.class)
88 | TriggerX.enable(AccountSampleHandler.class)
89 | ```
90 | **control trigger via custom setting**
91 |
92 | With the custom setting TRIGGER_CONTROL you can control the execution of your trigger via configuration, which is especially usefull when performing data migration and massive batchjobs. The following custom setting for the AccountSampleHandler.class
93 |
94 | ```java
95 | TRIGGER_CONTROL__c {
96 | Name = 'AccountSampleHandler'
97 | , AFTER_INSERT__c = true
98 | , AFTER_UPDATE__c = false
99 | , AFTER_DELETE__c = false
100 | , AFTER_UNDELETE__c = true
101 | , BEFORE_INSERT__c = false
102 | , BEFORE_UPDATE__c = false
103 | , BEFORE_DELETE__c = false}
104 | ```
105 |
106 | will prevent the execution of all BEFORE events as well as AFTER UPDATE and AFTER DELETE events for AccountSampleHandler.class. If no TRIGGER_CONTROL__c record exists, all events are considered as enabled!
107 |
108 |
109 | ## Recursion Control ##
110 | The built-in recursion control allows you to keep track of updated records within the current runtime context and filter on those records which have already been processed. Use for instance for updates on multiple hierachy levels of the same ObjectType or for recursive updates.
111 |
112 | ```java
113 | // add all records in the current update context to the updatedIds and
114 | TriggerX.addUpdatedIds(triggerOldMap.keySet());
115 |
116 | // and use this to return only records which havent been processed before
117 | #getNonRecursiveUpdates()
118 | ```
119 |
120 | # How To Use #
121 |
122 | **Handler class**
123 | Create a Handler class that extends TriggerX, per Custom Object. Overwrite those methods you actually want to handle. Keep in mind, that you have to cast the record variables to the concrete sObjectType
124 |
125 | ```java
126 | public class AccountSampleHandler extends TriggerX {
127 |
128 | // handle after update
129 | public override void onBeforeInsert(){
130 | for (Account record:(Account[])records){
131 | // BEFORE INSERT LOGIC
132 | }
133 | }
134 | // handle after update
135 | public override void onAfterUpdate(Map triggerOldMap){
136 | // just process Account records that haven't been updated in the same context
137 | for (Account record:(Account[])getNonRecursiveUpdates()){
138 | // AFTER UPDATE LOGIC
139 | }
140 | // prevent recursion
141 | TriggerX.addUpdatedIds(triggerOldMap.keySet());
142 | }
143 | }
144 | ```
145 |
146 | Create then a Trigger for your Custom Object and call TriggerX.handleTrigger with the Type of the handler class you just created
147 |
148 | trigger AccountSample on Account (before insert, after update){
149 | TriggerX.handleTrigger(AccountSampleHandler.class)
150 | }
151 |
152 |
153 | # Test Coverage #
154 | TriggerX.cls has a 100% test coverage.
155 | AccountSampleHandler.cls and AccountSample.trigger might have a lower coverage, depending on required fields and validation rules on Account and Contact.
156 |
157 | # License #
158 | Redistribution and use in source and binary forms, with or without modification, are permitted.
159 |
--------------------------------------------------------------------------------
/TriggerX/src/classes/TriggerXTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * TriggerX by Sebastian Wagner 2013
3 | *
4 | * Redistribution and use in source and binary forms, with or without modification, are permitted.
5 | *
6 | * Unit Tests for TriggerX
7 | * http://github.com/sebwagner/TriggerX
8 | */
9 | @isTest
10 | public class TriggerXTest {
11 |
12 | //--------------------------------------------------------------
13 | // DataTypes
14 | //--------------------------------------------------------------
15 | public class ConcreteHandlerWithoutOverride extends TriggerX {}
16 |
17 | // Test hanlder that sets the TriggerX.EventType of the executed method
18 | public class ConcreteHandler extends TriggerX {
19 |
20 | public TriggerX.EventType eventX;
21 |
22 | // set eventX to null
23 | public void reset(){
24 | eventX = null;
25 | }
26 |
27 | public override void onBeforeInsert(){
28 | eventX = TriggerX.EventType.BEFORE_INSERT;
29 | }
30 |
31 | public override void onAfterInsert(){
32 | eventX = TriggerX.EventType.AFTER_INSERT;
33 | }
34 |
35 | public override void onBeforeUpdate(Map triggerOldMap){
36 | eventX = TriggerX.EventType.BEFORE_UPDATE;
37 | }
38 |
39 | public override void onAfterUpdate(Map triggerOldMap){
40 | eventX = TriggerX.EventType.AFTER_UPDATE;
41 | }
42 |
43 | public override void onBeforeDelete(){
44 | eventX = TriggerX.EventType.BEFORE_DELETE;
45 | }
46 |
47 | public override void onAfterDelete(){
48 | eventX = TriggerX.EventType.AFTER_DELETE;
49 | }
50 |
51 | public override void onAfterUndelete(){
52 | eventX = TriggerX.EventType.AFTER_UNDELETE;
53 | }
54 |
55 |
56 | public sObject[] getNonRecursiveRecords(){
57 |
58 | return super.getNonRecursiveUpdates();
59 | }
60 |
61 |
62 | public Boolean hasChangedNameOrPhoneField(sObject record, sObject recordOld){
63 | return TriggerX.hasChangedFields(new sObjectField[]{Account.Name,Account.Phone},record,recordOld);
64 | }
65 |
66 |
67 | public Boolean hasChangedNameOrPhoneString(sObject record, sObject recordOld){
68 | return TriggerX.hasChangedFields(new String[]{'Name','Phone'},record,recordOld);
69 | }
70 |
71 |
72 | public sObjectField[] getNameAndPhoneChangesField(sObject record, sObject recordOld){
73 | return TriggerX.getChangedFields(new sObjectField[]{Account.Name,Account.Phone},record,recordOld);
74 | }
75 |
76 | public String[] getNameAndPhoneChangesString(sObject record, sObject recordOld){
77 | return TriggerX.getChangedFields(new String[]{'Name','Phone'},record,recordOld);
78 | }
79 |
80 | }
81 |
82 |
83 | // Holds context variables for #handleTriggerTest
84 | private class TestContext {
85 |
86 | public TriggerX.EventType eventX;
87 | public Boolean isBefore;
88 | public Boolean isInsert;
89 | public Boolean isUpdate;
90 | public Boolean isDelete;
91 | public Boolean isUndelete;
92 |
93 | public sObject[] records = new sObject[]{};
94 | public Map triggerOldMap = new Map();
95 |
96 | public TestContext(){}
97 |
98 | // sets context variables based on the EventType
99 | public void setEvent(TriggerX.EventType eventX){
100 |
101 | this.eventX = eventX;
102 | String name = eventX.name();
103 | this.isBefore = eventX.name().contains('BEFORE');
104 | this.isInsert = name.contains('INSERT');
105 | this.isUpdate = name.contains('UPDATE');
106 | this.isDelete = name.contains('_DELETE');
107 | this.isUndelete = name.contains('UNDELETE');
108 | }
109 | }
110 |
111 |
112 | //--------------------------------------------------------------
113 | // Handler Features
114 | //--------------------------------------------------------------
115 | // test #hasChangedFields and #getChangedFields for TriggerX
116 | private static testMethod void fieldChangesTest(){
117 |
118 | String name = 'SAME_NAME';
119 | Account record = new Account(Name = name, Phone = '123456');
120 | Account recordOld = new Account(Name = name, Phone = record.Phone);
121 |
122 | Test.startTest();
123 |
124 | ConcreteHandler hndl = new ConcreteHandler();
125 |
126 | system.assertEquals(false,hndl.hasChangedNameOrPhoneField(record, recordOld),'should be not true because nothing has changed');
127 | system.assertEquals(false,hndl.hasChangedNameOrPhoneString(record, recordOld),'should be not true because nothing has changed');
128 | recordOld.Phone += '1';
129 | system.assertEquals(true,hndl.hasChangedNameOrPhoneField(record, recordOld),'should be true because Phone has changed');
130 | system.assertEquals(true,hndl.hasChangedNameOrPhoneString(record, recordOld),'should be true because Phone has changed');
131 |
132 | system.assertEquals(Account.Phone,hndl.getNameAndPhoneChangesField(record, recordOld).get(0),'should return changed sObjectField Account.Phone');
133 | system.assertEquals('Phone',hndl.getNameAndPhoneChangesString(record, recordOld).get(0),'should return changed Fieldname Phone');
134 |
135 | Test.stopTest();
136 | }
137 |
138 |
139 | // test for recusion control while updating
140 | private static testMethod void recursionControlTest(){
141 |
142 | Test.startTest();
143 |
144 | ConcreteHandler hndl = new ConcreteHandler();
145 | sObject[] records = new sObject[]{
146 | new User(Id = UserInfo.getUserId())
147 | , new User(Id = null)};
148 |
149 | hndl.doConstruct(records);
150 | TriggerX.addUpdatedIds(new Set{UserInfo.getUserId()});
151 |
152 | system.assertEquals(true,TriggerX.getUpdatedIds().contains(UserInfo.getUserId()));
153 | system.assertEquals(null,hndl.getNonRecursiveRecords().get(0).Id,'should not return any record which id is returned by TriggerX.getUpdatedIds()');
154 |
155 | Test.stopTest();
156 | }
157 |
158 | //--------------------------------------------------------------
159 | // Event Control
160 | //--------------------------------------------------------------
161 |
162 | // test support with trigger control
163 | private static testMethod void fromControlTest(){
164 |
165 | Type typ = ConcreteHandler.class;
166 |
167 | // list of Event strings to disable
168 | TriggerX.EventType[] dsbld = new TriggerX.EventType[]{
169 | TriggerX.EventType.AFTER_INSERT
170 | , TriggerX.EventType.AFTER_DELETE
171 | , TriggerX.EventType.AFTER_UNDELETE
172 | , TriggerX.EventType.BEFORE_UPDATE
173 | , TriggerX.EventType.BEFORE_DELETE};
174 |
175 | // create the CONTROL record
176 | TRIGGER_CONTROL__c record = new TRIGGER_CONTROL__c(
177 | Name = typ.toString());
178 |
179 | // set flags on corresponding fields
180 | for (TriggerX.EventType eventX:dsbld)
181 | {
182 | record.put(eventX.name() + '__c',false);
183 | }
184 |
185 | upsert record;
186 |
187 |
188 | Test.startTest();
189 |
190 | // load Controls
191 | TriggerX.initControls(typ,true);
192 |
193 | // make sure all events are disabled
194 | for (TriggerX.EventType eventX:dsbld)
195 | {
196 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' should be disabled via TRIGGER_CONTROL__c');
197 | }
198 |
199 | Test.stopTest();
200 | }
201 |
202 |
203 | // Test for TriggerX.handleTrigger methods, with event control
204 | private static testMethod void handleTriggerTest(){
205 |
206 | Test.startTest();
207 |
208 | ConcreteHandler hndl = new ConcreteHandler();
209 | Type typ = ConcreteHandler.class;
210 | TestContext tx = new TestContext();
211 |
212 | // run test for each EventType
213 | for (TriggerX.EventType eventX:TriggerX.EventType.values())
214 | {
215 |
216 | // update the context
217 | tx.setEvent(eventX);
218 | hndl.reset();
219 |
220 | // disabled event
221 | TriggerX.disable(typ,tx.eventX);
222 | TriggerX.handleTrigger(hndl, typ, tx.isBefore, tx.isInsert, tx.isUpdate, tx.isDelete, tx.isUndelete, tx.records, tx.triggerOldMap);
223 | system.assertEquals(null,hndl.eventX,tx.eventX.name() + ' should be disabled for ' + typ.toString());
224 |
225 | // enabled event
226 | TriggerX.enable(typ,tx.eventX);
227 | TriggerX.handleTrigger(hndl, typ, tx.isBefore, tx.isInsert, tx.isUpdate, tx.isDelete, tx.isUndelete, tx.records, tx.triggerOldMap);
228 | system.assertEquals(tx.eventX,hndl.eventX,tx.eventX.name() + ' should be enabled for ' + typ.toString());
229 |
230 | }
231 |
232 | Test.stopTest();
233 | }
234 |
235 |
236 | // controls all events via TriggerX.disable(Type) and TriggerX.enable(Type)
237 | public static testMethod void dynamicControlAllTest(){
238 |
239 | Test.startTest();
240 | Type typ = ConcreteHandler.class;
241 |
242 | // disable all events
243 | TriggerX.disable(typ);
244 |
245 | // make sure all events are disabled
246 | for (TriggerX.EventType eventX:TriggerX.EventType.values())
247 | {
248 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be disabled for ' + typ.toString());
249 | }
250 |
251 | // enable em all
252 | TriggerX.enable(typ);
253 |
254 | for (TriggerX.EventType eventX:TriggerX.EventType.values())
255 | {
256 | system.assert(!TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be enabled for ' + typ.toString());
257 | }
258 |
259 | Test.stopTest();
260 | }
261 |
262 |
263 | // enables all events via TriggerX.enable(Type, EventType[])
264 | public static testMethod void dynamicControlTest(){
265 |
266 | Test.startTest();
267 | Type typ = ConcreteHandler.class;
268 |
269 | // disable all events
270 | TriggerX.disable(typ);
271 |
272 | // make sure all events are disabled
273 | for (TriggerX.EventType eventX:TriggerX.EventType.values())
274 | {
275 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be disabled for ' + typ.toString());
276 | }
277 |
278 |
279 | // an enable all
280 | TriggerX.enable(typ, TriggerX.EventType.values());
281 |
282 | for (TriggerX.EventType eventX:TriggerX.EventType.values())
283 | {
284 | system.assert(!TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be enabled for ' + typ.toString());
285 | }
286 |
287 | Test.stopTest();
288 | }
289 |
290 |
291 | // tests TriggerX.handleTrigger(Type) without Trigger context, coverage only
292 | private static testMethod void nonTriggerContextTest(){
293 |
294 | Boolean success = true;
295 | try
296 | {
297 | TriggerX.handleTrigger(ConcreteHandler.class);
298 | }
299 | catch(Exception ex)
300 | {
301 | success = false;
302 | }
303 | system.assert(!success, 'TriggerX.handleTrigger(Type) should fail in Non-Trigger mode');
304 | }
305 |
306 |
307 | // calls virtual TriggerX methods, just for TestCoverage
308 | private static testMethod void nonOverrideTest(){
309 |
310 | Test.startTest();
311 |
312 | ConcreteHandlerWithoutOverride hndl = new ConcreteHandlerWithoutOverride();
313 | hndl.onBeforeInsert();
314 | hndl.onAfterInsert();
315 | hndl.onBeforeUpdate(null);
316 | hndl.onAfterUpdate(null);
317 | hndl.onBeforeDelete();
318 | hndl.onAfterDelete();
319 | hndl.onAfterUndelete();
320 |
321 | Test.stopTest();
322 | }
323 |
324 | // calls getInsertEvents, getUpdateEvents and getDeleteEvents()
325 | private static testMethod void getTypesTest(){
326 |
327 | Test.startTest();
328 |
329 | for (TriggerX.EventType eventX:TriggerX.getDeleteEvents())
330 | {
331 | system.assert(eventX.name().contains('DELETE'),'DELETE events should contain DELETE');
332 | }
333 | for (TriggerX.EventType eventX:TriggerX.getInsertEvents())
334 | {
335 | system.assert(eventX.name().contains('INSERT'),'INSERT events should contain INSERT');
336 | }
337 | for (TriggerX.EventType eventX:TriggerX.getUpdateEvents())
338 | {
339 | system.assert(eventX.name().contains('UPDATE'),'UPDATE events should contain UPDATE');
340 | }
341 | Test.stopTest();
342 | }
343 | }
--------------------------------------------------------------------------------
/TriggerX/src/classes/TriggerX.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * TriggerX by Sebastian Wagner 2013
3 | *
4 | * Redistribution and use in source and binary forms, with or without modification, are permitted.
5 | *
6 | * Force.com Trigger API for advanced trigger execution
7 | * http://github.com/sebwagner/TriggerX
8 | */
9 | public virtual class TriggerX {
10 |
11 | // for INSERT && UPDATE Trigger.new otherwise Trigger.old
12 | public sObject[] records;
13 |
14 | public TriggerX(){}
15 |
16 |
17 | // used instead of constructor since handlers are instanciated with an emptry contructor
18 | public virtual TriggerX doConstruct(sObject[] records){
19 |
20 | this.records = records;
21 | return this;
22 | }
23 |
24 |
25 | // Method executed to perform BEFORE_INSERT operations, overwrite if applicable
26 | public virtual void onBeforeInsert(){}
27 |
28 |
29 | // Method executed to perform AFTER_INSERT operations, overwrite if applicable
30 | public virtual void onAfterInsert(){}
31 |
32 |
33 | // Method executed to perform BEFORE_UPDATE operations, overwrite if applicable
34 | public virtual void onBeforeUpdate(Map triggerOldMap){}
35 |
36 |
37 | // Method executed to perform AFTER_UPDATE operations, overwrite if applicable
38 | public virtual void onAfterUpdate(Map triggerOldMap){}
39 |
40 |
41 | // Method executed to perform BEFORE_DELETE operations, overwrite if applicable
42 | public virtual void onBeforeDelete(){}
43 |
44 |
45 | // Method executed to perform AFTER_DELETE operations, overwrite if applicable
46 | public virtual void onAfterDelete(){}
47 |
48 |
49 | // Method executed to perform AFTER_UNDELETE operations, overwrite if applicable
50 | public virtual void onAfterUndelete(){}
51 |
52 |
53 | // returns a list that contains only records which ids are not returned by TriggerX.getUpdatedIds()
54 | // for improved performance use contains() directly on the set of updated ids
55 | //
56 | // Set updatedIds = TriggerX.getUpdatedIds();
57 | // for (Account record:(Account)records){
58 | // if (!updatedIds.contains(record.Id))
59 | // }
60 | protected sObject[] getNonRecursiveUpdates(){
61 |
62 | Set updatedIds = TriggerX.getUpdatedIds();
63 |
64 | // make a copy of records
65 | sObject[] tmp = records.clone();
66 | tmp.clear();
67 | // loop through records and make check if is tracked as updated
68 | for (sObject record:records)
69 | {
70 | if (!updatedIds.contains((Id)record.get('Id')))
71 | {
72 | tmp.add(record);
73 | }
74 | }
75 |
76 | return tmp;
77 | }
78 |
79 |
80 | //--------------------------------------------------------------
81 | // Handling
82 | //--------------------------------------------------------------
83 | // instanciates the applicable Trigger Handler object and passes it with Trigger context to handleTrigger
84 | public static void handleTrigger(Type handlerType){
85 |
86 | handleTrigger(
87 | (TriggerX)handlerType.newInstance()
88 | , handlerType
89 | , Trigger.isBefore
90 | , Trigger.isInsert
91 | , Trigger.IsUpdate
92 | , Trigger.isDelete
93 | , Trigger.isUndelete
94 | , Trigger.new
95 | , Trigger.oldMap);
96 | }
97 |
98 |
99 | // executes the required methods based on the triggers context
100 | public static void handleTrigger(TriggerX handler, Type handlerType, Boolean isBefore, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete, sObject[] triggerNew, Map triggerOldMap){
101 |
102 | initControls(handlerType,false);
103 |
104 | //TriggerX handler = handlerClass.newInstance();
105 | Set dsbld = getDisabledEvents(handlerType);
106 |
107 | // BEFORE events
108 | if (isBefore)
109 | {
110 | if (isInsert && !dsbld.contains(EventType.BEFORE_INSERT.name())) handler.doConstruct(triggerNew).onBeforeInsert();
111 | else if (isUpdate && !dsbld.contains(EventType.BEFORE_UPDATE.name())) handler.doConstruct(triggerNew).onBeforeUpdate(triggerOldMap);
112 | else if (isDelete && !dsbld.contains(EventType.BEFORE_DELETE.name())) handler.doConstruct(triggerOldMap.values()).onBeforeDelete();
113 | }
114 | // AFTER events
115 | else
116 | {
117 | if (isInsert && !dsbld.contains(EventType.AFTER_INSERT.name())) handler.doConstruct(triggerNew).onAfterInsert();
118 | else if (isUpdate && !dsbld.contains(EventType.AFTER_UPDATE.name())) handler.doConstruct(triggerNew).onAfterUpdate(triggerOldMap);
119 | else if (isDelete && !dsbld.contains(EventType.AFTER_DELETE.name())) handler.doConstruct(triggerOldMap.values()).onAfterDelete();
120 | else if (isUndelete && !dsbld.contains(EventType.AFTER_UNDELETE.name())) handler.doConstruct(triggerNew).onAfterUndelete();
121 | }
122 | }
123 |
124 |
125 | //--------------------------------------------------------------
126 | // Change Tracking
127 | //--------------------------------------------------------------
128 |
129 | // returns true if a value of one of the specified fields has changed
130 | public static Boolean hasChangedFields(String[] fieldZ, sObject record, sObject recordOld){
131 |
132 | for (String field:fieldZ)
133 | {
134 | if (record.get(field) != recordOld.get(field)) return true;
135 | }
136 |
137 | return false;
138 | }
139 |
140 |
141 | // returns true if a value of one of the specified fields has changed
142 | public static Boolean hasChangedFields(sObjectField[] fieldZ, sObject record, sObject recordOld){
143 |
144 | for (sObjectField field:fieldZ)
145 | {
146 | if (record.get(field) != recordOld.get(field)) return true;
147 | }
148 |
149 | return false;
150 | }
151 |
152 |
153 | // returns a list of changed fields based on provided fieldZ list
154 | public static String[] getChangedFields(String[] fieldZ, sObject record, sObject recordOld){
155 |
156 | String[] changes = new String[]{};
157 |
158 | for (String field:fieldZ)
159 | {
160 | if (record.get(field) != recordOld.get(field)) changes.add(field);
161 | }
162 |
163 | return changes;
164 | }
165 |
166 | // returns a list of changed fields based on provided fieldZ list
167 | public static sObjectField[] getChangedFields(sObjectField[] fieldZ, sObject record, sObject recordOld){
168 |
169 | sObjectField[] changes = new sObjectField[]{};
170 |
171 | for (sObjectField field:fieldZ)
172 | {
173 | if (record.get(field) != recordOld.get(field)) changes.add(field);
174 | }
175 |
176 | return changes;
177 | }
178 |
179 |
180 | //--------------------------------------------------------------
181 | // EVENT Control
182 | //--------------------------------------------------------------
183 | // enum that represents all Trigger events
184 | public enum EventType {
185 | BEFORE_INSERT
186 | , AFTER_INSERT
187 | , BEFORE_UPDATE
188 | , AFTER_UPDATE
189 | , BEFORE_DELETE
190 | , AFTER_DELETE
191 | , AFTER_UNDELETE
192 | }
193 |
194 | // all disabled events by tyep
195 | static Map> disabledMap = new Map>();
196 |
197 | // keep track for which TriggerX instances initControls has been executed
198 | static Set ctrlInits = new Set();
199 |
200 | // keep track of the updated ids
201 | static Set ctrlUpdatedIds = new Set();
202 |
203 | // add set of ids to updatedIds
204 | public static void addUpdatedIds(Set idSet){
205 | ctrlUpdatedIds.addAll(idSet);
206 | }
207 |
208 | // return all updated ids
209 | public static Set getUpdatedIds(){
210 | return ctrlUpdatedIds;
211 | }
212 |
213 |
214 | // init disabled events from TRIGGER_CONTROL__c settings
215 | public static void initControls(Type className, Boolean forceInit){
216 |
217 | // only init the list for the given set
218 | if (!ctrlInits.contains(className) || forceInit)
219 | {
220 |
221 | TRIGGER_CONTROL__c record = TRIGGER_CONTROL__c.getInstance(className.toString());
222 | Set events = getDisabledEvents(className);
223 |
224 | if (record != null)
225 | {
226 | // reduce statements
227 | events.addAll(
228 | new Set{
229 | !record.AFTER_INSERT__c ? 'AFTER_INSERT' : null
230 | , !record.AFTER_UPDATE__c ? 'AFTER_UPDATE' : null
231 | , !record.AFTER_DELETE__c ? 'AFTER_DELETE' : null
232 | , !record.AFTER_UNDELETE__c ? 'AFTER_UNDELETE' : null
233 | , !record.BEFORE_INSERT__c ? 'BEFORE_INSERT' : null
234 | , !record.BEFORE_UPDATE__c ? 'BEFORE_UPDATE' : null
235 | , !record.BEFORE_DELETE__c ? 'BEFORE_DELETE' : null});
236 |
237 | events.remove(null);
238 | }
239 |
240 | // remember that the given key has been index already
241 | ctrlInits.add(className);
242 | }
243 |
244 | }
245 |
246 |
247 | // returns set of disabled events
248 | public static Set getDisabledEvents(Type className){
249 |
250 | if (!disabledMap.containsKey(className))
251 | {
252 | disabledMap.put(className,new Set());
253 | }
254 | return disabledMap.get(className);
255 | }
256 |
257 |
258 | // returns true if the specified event is disabled
259 | public static Boolean isDisabled(Type className, EventType event){
260 | return getDisabledEvents(className).contains(event.name());
261 | }
262 |
263 |
264 | // all insert events
265 | public static EventType[] getInsertEvents(){
266 | return new EventType[]{
267 | EventType.BEFORE_INSERT
268 | , EventType.AFTER_INSERT};
269 | }
270 |
271 |
272 | // all update events
273 | public static EventType[] getUpdateEvents(){
274 | return new EventType[]{
275 | EventType.BEFORE_UPDATE
276 | , EventType.AFTER_UPDATE};
277 | }
278 |
279 |
280 | // all update events
281 | public static EventType[] getDeleteEvents(){
282 | return new EventType[]{
283 | EventType.BEFORE_DELETE
284 | , EventType.AFTER_DELETE};
285 | }
286 |
287 | // list of all BEFORE EventType enums
288 | public static EventType[] getBeforeEvents(){
289 | return new EventType[]{
290 | EventType.BEFORE_INSERT
291 | , EventType.BEFORE_UPDATE
292 | , EventType.BEFORE_DELETE};
293 | }
294 |
295 |
296 | // list of all AFTER EventType enums
297 | public static EventType[] getAfterEvents(){
298 | return new EventType[]{
299 | EventType.AFTER_INSERT
300 | , EventType.AFTER_UPDATE
301 | , EventType.AFTER_DELETE
302 | , EventType.AFTER_UNDELETE};
303 | }
304 |
305 |
306 | // disables all events for the type
307 | public static void disable(Type className){
308 |
309 | EventType[] allEvents = new EventType[]{};
310 | allEvents.addAll(getBeforeEvents());
311 | allEvents.addAll(getAfterEvents());
312 |
313 | disable(className,allEvents);//.addAll(toStringEvents(allEvents));
314 | }
315 |
316 |
317 | // removes all disabled events for the class
318 | public static void enable(Type className){
319 |
320 | getDisabledEvents(className).clear();
321 | }
322 |
323 |
324 | // disable all specificed events for the type
325 | public static void disable(Type className, EventType[] events){
326 | getDisabledEvents(className).addAll(toStringEvents(events));
327 | }
328 |
329 |
330 | // enable all specificed events for the type
331 | public static void enable(Type className, EventType[] events){
332 | getDisabledEvents(className).removeAll(toStringEvents(events));
333 | }
334 |
335 |
336 | // disable a single event
337 | public static void disable(Type className, EventType event){
338 | getDisabledEvents(className).add(event.name());
339 | }
340 |
341 |
342 | // enable a single event
343 | public static void enable(Type className, EventType event){
344 | getDisabledEvents(className).remove(event.name());
345 | }
346 |
347 |
348 | // converts a Set of Event enums into Strings
349 | public static Set toStringEvents(EventType[] events){
350 |
351 | Set output = new Set();
352 | for (EventType e:events)
353 | {
354 | output.add(e.name());
355 | }
356 | return output;
357 | }
358 |
359 | }
360 |
--------------------------------------------------------------------------------