From 42ef520eb48dd4f3606ad96d90c07a1597bb8d71 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 7 Aug 2015 14:22:41 -0500 Subject: [PATCH 01/31] Refactoring. --- src/classes/TDTM_Filter.cls | 65 +++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 98ec5176db..95447722b5 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -47,40 +47,26 @@ public with sharing class TDTM_Filter { public static FilteredLists filter(SObject classToRunRecord, List newList, List oldList, Schema.DescribeSObjectResult describeObj) { FilteredLists filtered = new FilteredLists(); + UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { String filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); UTIL_Debug.debug('****Filter field: ' + filterField); if(filterField != null) { //get field type Map fieldMap = describeObj.fields.getMap(); - UTIL_Debug.debug('****Field map: ' + fieldMap); - Schema.SObjectField field = fieldMap.get(filterField); - if(field != null) { //the field name is valid! - UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); - //let's find the field type - Schema.DisplayType fieldType = field.getDescribe().getType(); - UTIL_Debug.debug('****Filter field type: ' + fieldType); - String val = String.valueOf(classToRunRecord.get('Filter_Value__c')); - - if(fieldType == Schema.DisplayType.String || fieldType == Schema.DisplayType.Email - || fieldType == Schema.DisplayType.Phone || fieldType == Schema.DisplayType.Picklist) { - filterByCondition(newList, oldList, filterField, filtered, val); - } else if(fieldType == Schema.DisplayType.Boolean) { - if(val == 'true') { - filterByCondition(newList, oldList, filterField, filtered, true); - } else if(val == 'false') { - filterByCondition(newList, oldList, filterField, filtered, false); - } - } else if(fieldType == Schema.DisplayType.Date) { - Date dateVal = Date.parse(val); - filterByCondition(newList, oldList, filterField, filtered, dateVal); - } else if(fieldType == Schema.DisplayType.Reference) { - UTIL_Debug.debug('****Filter Id val string: ' + val); - ID idVal = ID.valueOf(val); - filterByCondition(newList, oldList, filterField, filtered, idVal); - } - - UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); + + //If the field to filter on is made of relationships, recursively + if(filterField.contains('.')) { + UTIL_Debug.debug('****Filter is a relationship!'); + } else { + Schema.SObjectField field = fieldMap.get(filterField); + if(field != null) { //the field name is valid! + Object filter = getFilter(field, classToRunRecord); + filterByCondition(newList, oldList, filterField, filtered, filter); + UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); + } else { + UTIL_Debug.debug('****The field name is invalid.'); + } } } } catch(Exception e) { @@ -91,6 +77,29 @@ public with sharing class TDTM_Filter { return filtered; } + private static Object getFilter(Schema.SObjectField field, SObject classToRunRecord) { + //let's find the field type + Schema.DisplayType fieldType = field.getDescribe().getType(); + UTIL_Debug.debug('****Filter field type: ' + fieldType); + String val = String.valueOf(classToRunRecord.get('Filter_Value__c')); + + if(fieldType == Schema.DisplayType.String || fieldType == Schema.DisplayType.Email + || fieldType == Schema.DisplayType.Phone || fieldType == Schema.DisplayType.Picklist) { + return val; + } else if(fieldType == Schema.DisplayType.Boolean) { + if(val == 'true') { + return true; + } else if(val == 'false') { + return false; + } + } else if(fieldType == Schema.DisplayType.Date) { + return Date.parse(val); + } else if(fieldType == Schema.DisplayType.Reference) { + return ID.valueOf(val); + } + return null; + } + private static void filterByCondition(List newList, List oldList, String filterField, FilteredLists filtered, Object val) { for(SObject o : newList) { if(o.get(filterField) != val) { From b20162a83a026754c0b39f0c92d9f7ef525d90f6 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 7 Aug 2015 16:48:55 -0500 Subject: [PATCH 02/31] Failing test (and some code for it) added. --- src/classes/TDTM_Filter.cls | 63 ++++++++++++++++++++++++-------- src/classes/TDTM_Filter_TEST.cls | 36 ++++++++++++++++++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 95447722b5..4cfe2de019 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -46,27 +46,16 @@ public with sharing class TDTM_Filter { */ public static FilteredLists filter(SObject classToRunRecord, List newList, List oldList, Schema.DescribeSObjectResult describeObj) { - FilteredLists filtered = new FilteredLists(); UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { String filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); UTIL_Debug.debug('****Filter field: ' + filterField); if(filterField != null) { - //get field type - Map fieldMap = describeObj.fields.getMap(); - //If the field to filter on is made of relationships, recursively if(filterField.contains('.')) { - UTIL_Debug.debug('****Filter is a relationship!'); + return filterByRelationship(filterField, classToRunRecord, newList, oldList, describeObj); } else { - Schema.SObjectField field = fieldMap.get(filterField); - if(field != null) { //the field name is valid! - Object filter = getFilter(field, classToRunRecord); - filterByCondition(newList, oldList, filterField, filtered, filter); - UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); - } else { - UTIL_Debug.debug('****The field name is invalid.'); - } + return filterByField(filterField, classToRunRecord, newList, oldList, describeObj); } } } catch(Exception e) { @@ -74,7 +63,50 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug(LoggingLevel.WARN, '****Exception: ' + e.getMessage()); UTIL_Debug.debug(LoggingLevel.WARN, '\n****Stack Trace:\n' + e.getStackTraceString() + '\n'); } - return filtered; + return new FilteredLists(); //To avoid returning null. + } + + private static FilteredLists filterByRelationship(String filterField, SObject classToRunRecord, List newList, + List oldList, Schema.DescribeSObjectResult describeObj) { + FilteredLists filtered = new FilteredLists(); + + //separate cross object references, i.e. account.name + list splitField = (filterField.split('\\.', 0)); + UTIL_Debug.debug('****Relationships chain: ' + JSON.serializePretty(splitField)); + + //remove the field name itself to only include parent object references + String fieldName = splitField[splitField.size() - 1]; + UTIL_Debug.debug('****Filter field name: ' + fieldName); + + String parentObjectName = splitField[splitField.size() - 2]; + UTIL_Debug.debug('****Filter parent object: ' + parentObjectName); + + splitField.remove(splitField.size() - 1); + UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(splitField)); + + Schema.DescribeSObjectResult describe = UTIL_Describe.getObjectDescribe(parentObjectName); + Map fieldMap = describe.fields.getMap(); + Schema.SObjectField field = fieldMap.get(fieldName); + UTIL_Debug.debug('****Field in object: ' + field); + + return filtered; + } + + private static FilteredLists filterByField(String filterField, SObject classToRunRecord, List newList, + List oldList, Schema.DescribeSObjectResult describeObj) { + FilteredLists filtered = new FilteredLists(); + + //get field type + Map fieldMap = describeObj.fields.getMap(); + Schema.SObjectField field = fieldMap.get(filterField); + if(field != null) { //the field name is valid! + Object filter = getFilter(field, classToRunRecord); + filterByCondition(newList, oldList, filterField, filtered, filter); + UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); + } else { + UTIL_Debug.debug('****The field name is invalid.'); + } + return filtered; } private static Object getFilter(Schema.SObjectField field, SObject classToRunRecord) { @@ -100,7 +132,8 @@ public with sharing class TDTM_Filter { return null; } - private static void filterByCondition(List newList, List oldList, String filterField, FilteredLists filtered, Object val) { + private static void filterByCondition(List newList, List oldList, String filterField, + FilteredLists filtered, Object val) { for(SObject o : newList) { if(o.get(filterField) != val) { filtered.newList.add(o); diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 52831a4e87..8a80d3fbea 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -280,4 +280,40 @@ public with sharing class TDTM_Filter_TEST { } System.assertEquals(2, rels.size()); } + + public static testmethod void relationshipField() { + if (strTestOnly != '*' && strTestOnly != 'relationshipField') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Account.Name', + Filter_Value__c = 'Acme'); + + Account accFiltered = new Account(Name = 'Acme'); + Account accNotFiltered = new Account(Name = 'ABC'); + insert new Account[] {accFiltered, accNotFiltered}; + + //Creating four contacts. two of them are not students, because they doesn't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Account = accNotFiltered); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Account = accFiltered); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', Account = accNotFiltered); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Account = accFiltered); + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); + } } \ No newline at end of file From 3aeb48d40b6f4deb90dfe6f76bdbe5ce7e056875 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 11 Aug 2015 12:07:30 -0500 Subject: [PATCH 03/31] The filtering on a related object is actually working, though we are now losing all the original values in the trigger records. (Also, a lot of cleanup needs to be done.) --- src/classes/TDTM_Filter.cls | 90 +++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 4cfe2de019..3b22f08eaf 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -70,25 +70,50 @@ public with sharing class TDTM_Filter { List oldList, Schema.DescribeSObjectResult describeObj) { FilteredLists filtered = new FilteredLists(); + Map newMap = new Map(newList); + Set newListIDs = newMap.keySet(); + //query filter values, in case they are not in the trigger + String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :newListIDs'; + UTIL_Debug.debug('****Dynamic query: ' + dynamicQuery); + List withRelatedFields = Database.query(dynamicQuery); + //List withRelatedFields = [select Account.Name from Contact where ID in :newListIDs]; + //UTIL_Debug.debug('****withRelatedFields: ' + JSON.serializePretty(withRelatedFields)); + //separate cross object references, i.e. account.name - list splitField = (filterField.split('\\.', 0)); + list splitField = (filterField.split('\\.', 0)); UTIL_Debug.debug('****Relationships chain: ' + JSON.serializePretty(splitField)); - //remove the field name itself to only include parent object references + //get the field name itself String fieldName = splitField[splitField.size() - 1]; UTIL_Debug.debug('****Filter field name: ' + fieldName); + //get the name of the field parent = last object in the chain String parentObjectName = splitField[splitField.size() - 2]; UTIL_Debug.debug('****Filter parent object: ' + parentObjectName); - splitField.remove(splitField.size() - 1); - UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(splitField)); + //remove the field, to have only the parent object chain + List filterObjectChain = new List(); + for(Integer i = 0; i < (splitField.size() - 1); i++) + filterObjectChain.add(splitField[i]); + UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); Schema.DescribeSObjectResult describe = UTIL_Describe.getObjectDescribe(parentObjectName); Map fieldMap = describe.fields.getMap(); Schema.SObjectField field = fieldMap.get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); + if(field != null) { //the field name is valid for the object at the top of the chain! + Object filterValue = getFilter(field, classToRunRecord); + UTIL_Debug.debug('****Relationship filter value: ' + filterValue); + UTIL_Debug.debug('****Relationship filter field: ' + fieldName); + //Now we have to somehow put back together the relationship chain and navigate it... + //filterByCondition(newList, oldList, filterObjectChain, fieldName, filtered, filterValue); + filterByCondition(withRelatedFields, oldList, filterObjectChain, fieldName, filtered, filterValue); + UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); + } else { + UTIL_Debug.debug('****The field name is invalid.'); + } + return filtered; } @@ -101,7 +126,7 @@ public with sharing class TDTM_Filter { Schema.SObjectField field = fieldMap.get(filterField); if(field != null) { //the field name is valid! Object filter = getFilter(field, classToRunRecord); - filterByCondition(newList, oldList, filterField, filtered, filter); + filterByCondition(newList, oldList, null, filterField, filtered, filter); UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -132,20 +157,47 @@ public with sharing class TDTM_Filter { return null; } - private static void filterByCondition(List newList, List oldList, String filterField, - FilteredLists filtered, Object val) { - for(SObject o : newList) { - if(o.get(filterField) != val) { - filtered.newList.add(o); - } - } - if(oldList != null && oldList.size() > 0) { - for(SObject o : oldList) { - if(o.get(filterField) != val) { - filtered.oldList.add(o); - } - } - } + private static void filterByCondition(List newList, List oldList, List filterObjectChain, + String filterField, FilteredLists filtered, Object val) { + if(filterObjectChain == null) { //The field in in the same object the trigger fires on + filterList(newList, filterField, filtered, val); + filterList(oldList, filterField, filtered, val); + + } else { //The field is in a related object + UTIL_Debug.debug('****The field is in a related object'); + UTIL_Debug.debug('****filterObjectChain: ' + filterObjectChain); + if(newList != null && newList.size() > 0) { + for(Integer i = 0; i < newList.size(); i++) { + SObject o = newList[i]; + SObject original = o; //I just want to keep a reference to the original object, to be able to filter on it. + UTIL_Debug.debug('****Object in trigger: ' + o); + //traverse parent relationships until the last one + if (o != null) { + for (String parentObj : filterObjectChain) { + UTIL_Debug.debug('****Object to traverse: ' + parentObj); + o = o.getsObject(parentObj); + UTIL_Debug.debug('****Parent object: ' + o); + } + } + //perform the filtering + UTIL_Debug.debug('****Filtering by field ' + filterField + ', with value ' + val + ' on object ' + o); + if(o != null && o.get(filterField) != val) { + UTIL_Debug.debug('****Adding object ' + original + ' to filtered list.'); + filtered.newList.add(original); + } + } + } + } + } + + private static void filterList(List listToFilter, String filterField, FilteredLists filtered, Object val) { + if(listToFilter != null && listToFilter.size() > 0) { + for(SObject o : listToFilter) { + if(o.get(filterField) != val) { + filtered.newList.add(o); + } + } + } } /******************************************************************************************************* From 79f5b0dd58661d57aa32b0aa0b22689429686e65 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 11 Aug 2015 14:13:24 -0500 Subject: [PATCH 04/31] Relationship field filter test now passes (and all the other fail). --- src/classes/TDTM_Filter.cls | 111 +++++++++++++++---------------- src/classes/TDTM_Filter_TEST.cls | 14 ++-- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 3b22f08eaf..5e13cb5a5b 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -49,10 +49,8 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { String filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); - UTIL_Debug.debug('****Filter field: ' + filterField); if(filterField != null) { - //If the field to filter on is made of relationships, recursively - if(filterField.contains('.')) { + if(filterField.contains('.')) { //If the field to filter on is made of relationships return filterByRelationship(filterField, classToRunRecord, newList, oldList, describeObj); } else { return filterByField(filterField, classToRunRecord, newList, oldList, describeObj); @@ -70,26 +68,12 @@ public with sharing class TDTM_Filter { List oldList, Schema.DescribeSObjectResult describeObj) { FilteredLists filtered = new FilteredLists(); - Map newMap = new Map(newList); - Set newListIDs = newMap.keySet(); - //query filter values, in case they are not in the trigger - String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :newListIDs'; - UTIL_Debug.debug('****Dynamic query: ' + dynamicQuery); - List withRelatedFields = Database.query(dynamicQuery); - //List withRelatedFields = [select Account.Name from Contact where ID in :newListIDs]; - //UTIL_Debug.debug('****withRelatedFields: ' + JSON.serializePretty(withRelatedFields)); - - //separate cross object references, i.e. account.name - list splitField = (filterField.split('\\.', 0)); - UTIL_Debug.debug('****Relationships chain: ' + JSON.serializePretty(splitField)); - - //get the field name itself - String fieldName = splitField[splitField.size() - 1]; - UTIL_Debug.debug('****Filter field name: ' + fieldName); + List newListRelatedFields = queryRelatedFields(filterField, newList, describeObj); + List oldListRelatedFields = queryRelatedFields(filterField, oldList, describeObj); - //get the name of the field parent = last object in the chain - String parentObjectName = splitField[splitField.size() - 2]; - UTIL_Debug.debug('****Filter parent object: ' + parentObjectName); + List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name + String fieldName = splitField[splitField.size() - 1]; //get the field name itself + String parentObjectName = splitField[splitField.size() - 2]; //get the name of the field parent = last object in the chain //remove the field, to have only the parent object chain List filterObjectChain = new List(); @@ -97,26 +81,36 @@ public with sharing class TDTM_Filter { filterObjectChain.add(splitField[i]); UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - Schema.DescribeSObjectResult describe = UTIL_Describe.getObjectDescribe(parentObjectName); - Map fieldMap = describe.fields.getMap(); - Schema.SObjectField field = fieldMap.get(fieldName); + Schema.SObjectField field = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap().get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid for the object at the top of the chain! Object filterValue = getFilter(field, classToRunRecord); - UTIL_Debug.debug('****Relationship filter value: ' + filterValue); - UTIL_Debug.debug('****Relationship filter field: ' + fieldName); - //Now we have to somehow put back together the relationship chain and navigate it... - //filterByCondition(newList, oldList, filterObjectChain, fieldName, filtered, filterValue); - filterByCondition(withRelatedFields, oldList, filterObjectChain, fieldName, filtered, filterValue); + filterByCondition(newList, oldList, newListRelatedFields, oldListRelatedFields, filterObjectChain, + fieldName, filtered, filterValue); UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); - } - + } return filtered; } + private static List queryRelatedFields(String filterField, List compList, + Schema.DescribeSObjectResult describeObj) { + Map compMap = new Map(compList); + Set compListIDs = compMap.keySet(); + //query filter values, in case they are not in the trigger + String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :compListIDs'; + UTIL_Debug.debug('****Dynamic query: ' + dynamicQuery); + Map withRelatedFieldsMap = new Map(Database.query(dynamicQuery)); + List withRelatedFields = new List(); + //Let's make sure we return them in the same order the list passed as param + for(SObject compRecord : compList) { + withRelatedFields.add(withRelatedFieldsMap.get(compRecord.ID)); + } + return withRelatedFields; + } + private static FilteredLists filterByField(String filterField, SObject classToRunRecord, List newList, List oldList, Schema.DescribeSObjectResult describeObj) { FilteredLists filtered = new FilteredLists(); @@ -126,7 +120,7 @@ public with sharing class TDTM_Filter { Schema.SObjectField field = fieldMap.get(filterField); if(field != null) { //the field name is valid! Object filter = getFilter(field, classToRunRecord); - filterByCondition(newList, oldList, null, filterField, filtered, filter); + filterByCondition(newList, oldList, null, null, null, filterField, filtered, filter); UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -157,35 +151,38 @@ public with sharing class TDTM_Filter { return null; } - private static void filterByCondition(List newList, List oldList, List filterObjectChain, - String filterField, FilteredLists filtered, Object val) { + private static void filterByCondition(List newList, List oldList, List newListRelatedFields, + ListoldListRelatedFields, List filterObjectChain, String filterField, FilteredLists filtered, Object val) { if(filterObjectChain == null) { //The field in in the same object the trigger fires on filterList(newList, filterField, filtered, val); - filterList(oldList, filterField, filtered, val); - + filterList(oldList, filterField, filtered, val); } else { //The field is in a related object - UTIL_Debug.debug('****The field is in a related object'); - UTIL_Debug.debug('****filterObjectChain: ' + filterObjectChain); - if(newList != null && newList.size() > 0) { - for(Integer i = 0; i < newList.size(); i++) { - SObject o = newList[i]; - SObject original = o; //I just want to keep a reference to the original object, to be able to filter on it. - UTIL_Debug.debug('****Object in trigger: ' + o); - //traverse parent relationships until the last one - if (o != null) { - for (String parentObj : filterObjectChain) { - UTIL_Debug.debug('****Object to traverse: ' + parentObj); - o = o.getsObject(parentObj); - UTIL_Debug.debug('****Parent object: ' + o); - } - } - //perform the filtering - UTIL_Debug.debug('****Filtering by field ' + filterField + ', with value ' + val + ' on object ' + o); - if(o != null && o.get(filterField) != val) { - UTIL_Debug.debug('****Adding object ' + original + ' to filtered list.'); - filtered.newList.add(original); + filterListByRelatedField(newListRelatedFields, newList, filtered.newList, filterObjectChain, filterField, val); + filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList, filterObjectChain, filterField, val); + } + } + + private static void filterListByRelatedField(List listRelatedFields, List originalList, + List filteredList, List filterObjectChain, String filterField, Object val) { + if(listRelatedFields != null && listRelatedFields.size() > 0) { + for(Integer i = 0; i < listRelatedFields.size(); i++) { + SObject o = listRelatedFields[i]; + //SObject original = o; //I just want to keep a reference to the original object, to be able to filter on it. + UTIL_Debug.debug('****Object in trigger: ' + o); + //traverse parent relationships until the last one + if (o != null) { + for (String parentObj : filterObjectChain) { + UTIL_Debug.debug('****Object to traverse: ' + parentObj); + o = o.getsObject(parentObj); + UTIL_Debug.debug('****Parent object: ' + o); } } + //perform the filtering + UTIL_Debug.debug('****Filtering by field ' + filterField + ', with value ' + val + ' on object ' + o); + if(o != null && o.get(filterField) != val) { + UTIL_Debug.debug('****Adding object ' + originalList[i] + ' to filtered list.'); + filteredList.add(originalList[i]); + } } } } diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 8a80d3fbea..46f69eaebf 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -53,7 +53,7 @@ public with sharing class TDTM_Filter_TEST { Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'UniversityEmail__c', Filter_Value__c = null); - //Creating four contacts. two of them are not students, because they doesn't have a university email. + //Creating four contacts. two of them are not students, because they don't have a university email. Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', UniversityEmail__c = 'tt1@fake.edu'); Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', UniversityEmail__c = null); Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', UniversityEmail__c = 'tt3@fake.edu'); @@ -270,7 +270,9 @@ public with sharing class TDTM_Filter_TEST { c1.ReportsToId = c2.Id; c2.ReportsToId = c3.Id; c3.ReportsToId = c4.Id; + Test.startTest(); update contacts; + Test.stopTest(); //Only those from c1 and c3 should have had a relationship automatically created. rels = [select Contact__c, RelatedContact__c from Relationship__c]; @@ -297,11 +299,11 @@ public with sharing class TDTM_Filter_TEST { Account accNotFiltered = new Account(Name = 'ABC'); insert new Account[] {accFiltered, accNotFiltered}; - //Creating four contacts. two of them are not students, because they doesn't have a university email. - Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Account = accNotFiltered); - Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Account = accFiltered); - Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', Account = accNotFiltered); - Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Account = accFiltered); + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AccountId = accNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered.Id); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', AccountId = accFiltered.Id); Contact[] contacts = new Contact[] {c1, c2, c3, c4}; insert contacts; From db03f47b5c7d311db2f680c029634df472a30cea Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 11 Aug 2015 14:33:33 -0500 Subject: [PATCH 05/31] Fixing bug. All tests pass. --- src/classes/TDTM_Filter.cls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 5e13cb5a5b..ac236b065a 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -154,8 +154,8 @@ public with sharing class TDTM_Filter { private static void filterByCondition(List newList, List oldList, List newListRelatedFields, ListoldListRelatedFields, List filterObjectChain, String filterField, FilteredLists filtered, Object val) { if(filterObjectChain == null) { //The field in in the same object the trigger fires on - filterList(newList, filterField, filtered, val); - filterList(oldList, filterField, filtered, val); + filterList(newList, filterField, filtered.newList, val); + filterList(oldList, filterField, filtered.oldList, val); } else { //The field is in a related object filterListByRelatedField(newListRelatedFields, newList, filtered.newList, filterObjectChain, filterField, val); filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList, filterObjectChain, filterField, val); @@ -187,11 +187,11 @@ public with sharing class TDTM_Filter { } } - private static void filterList(List listToFilter, String filterField, FilteredLists filtered, Object val) { + private static void filterList(List listToFilter, String filterField, List filteredList, Object val) { if(listToFilter != null && listToFilter.size() > 0) { for(SObject o : listToFilter) { if(o.get(filterField) != val) { - filtered.newList.add(o); + filteredList.add(o); } } } From c036059ea66a39795ef993d48bb34e94b11a5849 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 11 Aug 2015 16:30:22 -0500 Subject: [PATCH 06/31] Refactoring to avoid having to pass so many parameters around. --- src/classes/TDTM_Filter.cls | 102 ++++++++++++++++++++-------- src/classes/TDTM_TriggerHandler.cls | 3 +- 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index ac236b065a..08276d2976 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -36,24 +36,39 @@ */ public with sharing class TDTM_Filter { - /******************************************************************************************************* - * @description Filters records to process. + SObject classToRunRecord; + List newList; + List oldList; + Schema.DescribeSObjectResult describeObj; + + /******************************************************************************************************* + * @description Constructor. Sets properties. * @param classToRunRecord The class being run. * @param newList The records that were passed to the trigger as trigger.new. * @param oldList The records that were passed to the trigger as trigger.old. * @param describeObj The type of SObject the class runs for. - * @return Void */ - public static FilteredLists filter(SObject classToRunRecord, List newList, List oldList, + public TDTM_Filter(SObject classToRunRecord, List newList, List oldList, Schema.DescribeSObjectResult describeObj) { + this.classToRunRecord = classToRunRecord; + this.newList = newList; + this.oldList = oldList; + this.describeObj = describeObj; + } + + /******************************************************************************************************* + * @description Filters records to process. + * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. + */ + public FilteredLists filter() { UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { String filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); if(filterField != null) { if(filterField.contains('.')) { //If the field to filter on is made of relationships - return filterByRelationship(filterField, classToRunRecord, newList, oldList, describeObj); + return filterByRelationship(filterField); } else { - return filterByField(filterField, classToRunRecord, newList, oldList, describeObj); + return filterByField(filterField); } } } catch(Exception e) { @@ -64,12 +79,16 @@ public with sharing class TDTM_Filter { return new FilteredLists(); //To avoid returning null. } - private static FilteredLists filterByRelationship(String filterField, SObject classToRunRecord, List newList, - List oldList, Schema.DescribeSObjectResult describeObj) { + /******************************************************************************************************* + * @description Filters newList and oldList based on the value of a related field. + * @param filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name". + * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. + */ + private FilteredLists filterByRelationship(String filterField) { FilteredLists filtered = new FilteredLists(); - List newListRelatedFields = queryRelatedFields(filterField, newList, describeObj); - List oldListRelatedFields = queryRelatedFields(filterField, oldList, describeObj); + List newListRelatedFields = queryRelatedFields(filterField, newList); + List oldListRelatedFields = queryRelatedFields(filterField, oldList); List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name String fieldName = splitField[splitField.size() - 1]; //get the field name itself @@ -85,9 +104,8 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid for the object at the top of the chain! - Object filterValue = getFilter(field, classToRunRecord); - filterByCondition(newList, oldList, newListRelatedFields, oldListRelatedFields, filterObjectChain, - fieldName, filtered, filterValue); + Object filterValue = getFilter(field); + filterByCondition(newListRelatedFields, oldListRelatedFields, filterObjectChain, fieldName, filtered, filterValue); UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -95,15 +113,23 @@ public with sharing class TDTM_Filter { return filtered; } - private static List queryRelatedFields(String filterField, List compList, - Schema.DescribeSObjectResult describeObj) { + /******************************************************************************************************* + * @description Queries the fields that are part of the relationship filter, since these values are not initially + * present in the records the triggers acts on. + * @param filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name". + * @param compList The list of records to query. + * @return List A list of records pointing to the same records that are present in newList or oldList, + * but containing only the fields defined in the query condition. The returned list is also in the same order as + * newList or oldList. + */ + private List queryRelatedFields(String filterField, List compList) { Map compMap = new Map(compList); Set compListIDs = compMap.keySet(); //query filter values, in case they are not in the trigger String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :compListIDs'; - UTIL_Debug.debug('****Dynamic query: ' + dynamicQuery); + UTIL_Debug.debug('****Relationship filter dynamic query: ' + dynamicQuery); Map withRelatedFieldsMap = new Map(Database.query(dynamicQuery)); - List withRelatedFields = new List(); + List withRelatedFields = new List(); //We don't want to modify the original list, but use a new one instead. //Let's make sure we return them in the same order the list passed as param for(SObject compRecord : compList) { withRelatedFields.add(withRelatedFieldsMap.get(compRecord.ID)); @@ -111,16 +137,20 @@ public with sharing class TDTM_Filter { return withRelatedFields; } - private static FilteredLists filterByField(String filterField, SObject classToRunRecord, List newList, - List oldList, Schema.DescribeSObjectResult describeObj) { + /******************************************************************************************************* + * @description Filters newList and oldList based on the value of a field on the trigger records. + * @param filterField The field to filter on. + * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. + */ + private FilteredLists filterByField(String filterField) { FilteredLists filtered = new FilteredLists(); //get field type Map fieldMap = describeObj.fields.getMap(); Schema.SObjectField field = fieldMap.get(filterField); if(field != null) { //the field name is valid! - Object filter = getFilter(field, classToRunRecord); - filterByCondition(newList, oldList, null, null, null, filterField, filtered, filter); + Object filter = getFilter(field); + filterByCondition(null, null, null, filterField, filtered, filter); UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -128,7 +158,12 @@ public with sharing class TDTM_Filter { return filtered; } - private static Object getFilter(Schema.SObjectField field, SObject classToRunRecord) { + /******************************************************************************************************* + * @description Returns the value to compare against in the correct type. + * @param Field The SObjectField used in the filtering comparison. + * @return Object The value to compare against when filtering, already in the correct type. + */ + private Object getFilter(Schema.SObjectField field) { //let's find the field type Schema.DisplayType fieldType = field.getDescribe().getType(); UTIL_Debug.debug('****Filter field type: ' + fieldType); @@ -151,8 +186,19 @@ public with sharing class TDTM_Filter { return null; } - private static void filterByCondition(List newList, List oldList, List newListRelatedFields, - ListoldListRelatedFields, List filterObjectChain, String filterField, FilteredLists filtered, Object val) { + /******************************************************************************************************* + * @description Filters newList and oldList based on the defined filtering criteria. + * @param newListRelatedFields A list of records pointing to the same records that are present in newList, + * but containing only the fields defined in the query condition. In the same order as newList. + * @param oldListRelatedFields A list of records pointing to the same records that are present in oldList, + * but containing only the fields defined in the query condition. In the same order as newList. + * @param filterObjectChain + * @param filterField The field to filter on. + * @param val The value to compare against when filtering, already in the correct type. + * @return void + */ + private void filterByCondition(List newListRelatedFields, List oldListRelatedFields, + List filterObjectChain, String filterField, FilteredLists filtered, Object val) { if(filterObjectChain == null) { //The field in in the same object the trigger fires on filterList(newList, filterField, filtered.newList, val); filterList(oldList, filterField, filtered.oldList, val); @@ -162,12 +208,11 @@ public with sharing class TDTM_Filter { } } - private static void filterListByRelatedField(List listRelatedFields, List originalList, + private void filterListByRelatedField(List listRelatedFields, List originalList, List filteredList, List filterObjectChain, String filterField, Object val) { if(listRelatedFields != null && listRelatedFields.size() > 0) { for(Integer i = 0; i < listRelatedFields.size(); i++) { SObject o = listRelatedFields[i]; - //SObject original = o; //I just want to keep a reference to the original object, to be able to filter on it. UTIL_Debug.debug('****Object in trigger: ' + o); //traverse parent relationships until the last one if (o != null) { @@ -187,7 +232,7 @@ public with sharing class TDTM_Filter { } } - private static void filterList(List listToFilter, String filterField, List filteredList, Object val) { + private void filterList(List listToFilter, String filterField, List filteredList, Object val) { if(listToFilter != null && listToFilter.size() > 0) { for(SObject o : listToFilter) { if(o.get(filterField) != val) { @@ -198,7 +243,8 @@ public with sharing class TDTM_Filter { } /******************************************************************************************************* - * @description Contains the filtered new and old lists of records, so we can return both from the filter method. + * @description Wrapper containing the filtered new and old lists of records, so we can return both simultaneously + * from a method. */ public class FilteredLists { public List newList; diff --git a/src/classes/TDTM_TriggerHandler.cls b/src/classes/TDTM_TriggerHandler.cls index 6923470d4b..558192d45f 100644 --- a/src/classes/TDTM_TriggerHandler.cls +++ b/src/classes/TDTM_TriggerHandler.cls @@ -177,7 +177,8 @@ public with sharing class TDTM_TriggerHandler { } else { UTIL_Debug.debugWithInfo('****Calling synchronously: ' + classToRunName); - TDTM_Filter.FilteredLists filtered = TDTM_Filter.filter(classToRunRecord, newList, oldList, describeObj); + TDTM_Filter filter = new TDTM_Filter(classToRunRecord, newList, oldList, describeObj); + TDTM_Filter.FilteredLists filtered = filter.filter(); UTIL_Debug.debug('****newList after filtering: ' + JSON.serializePretty(filtered.newList)); if(filtered.newList.size() > 0) { From 8a1bb7232182853c2d71c2cd301b72d5f870248f Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 12 Aug 2015 10:50:33 -0500 Subject: [PATCH 07/31] More refactoring. --- src/classes/TDTM_Filter.cls | 105 +++++++++++++++------------- src/classes/TDTM_TriggerHandler.cls | 4 +- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 08276d2976..43dc962bc9 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -36,10 +36,22 @@ */ public with sharing class TDTM_Filter { + /* @description An instance of the class being run.*/ SObject classToRunRecord; + /* * @description The records that were passed to the trigger as trigger.new. */ List newList; - List oldList; + /* @description The records that were passed to the trigger as trigger.old. */ + List oldList; + /* @description The type of SObject the class runs for. */ Schema.DescribeSObjectResult describeObj; + /* @description filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name".*/ + String filterField; + /* The field to filter on in the parent object, i.e. "Name". */ + String fieldName; + /* @description The value to compare against when filtering, already in the correct type. */ + Object filterValue; + /* @description An instance of the wrapper object that contains the filtered newList and oldList. */ + FilteredLists filtered; /******************************************************************************************************* * @description Constructor. Sets properties. @@ -54,6 +66,7 @@ public with sharing class TDTM_Filter { this.newList = newList; this.oldList = oldList; this.describeObj = describeObj; + filtered = new FilteredLists(); } /******************************************************************************************************* @@ -63,35 +76,34 @@ public with sharing class TDTM_Filter { public FilteredLists filter() { UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { - String filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); + filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); if(filterField != null) { if(filterField.contains('.')) { //If the field to filter on is made of relationships - return filterByRelationship(filterField); + filterByRelationship(); } else { - return filterByField(filterField); + fieldName = filterField; //No need to break down the field condition + filterByField(); } + return filtered; } } catch(Exception e) { UTIL_Debug.debug(LoggingLevel.WARN, '****Invalid filtering condition'); UTIL_Debug.debug(LoggingLevel.WARN, '****Exception: ' + e.getMessage()); UTIL_Debug.debug(LoggingLevel.WARN, '\n****Stack Trace:\n' + e.getStackTraceString() + '\n'); } - return new FilteredLists(); //To avoid returning null. + return null; } /******************************************************************************************************* * @description Filters newList and oldList based on the value of a related field. - * @param filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name". - * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. + * @return void */ - private FilteredLists filterByRelationship(String filterField) { - FilteredLists filtered = new FilteredLists(); - - List newListRelatedFields = queryRelatedFields(filterField, newList); - List oldListRelatedFields = queryRelatedFields(filterField, oldList); + private void filterByRelationship() { + List newListRelatedFields = queryRelatedFields(newList); + List oldListRelatedFields = queryRelatedFields(oldList); List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name - String fieldName = splitField[splitField.size() - 1]; //get the field name itself + fieldName = splitField[splitField.size() - 1]; //get the field name itself String parentObjectName = splitField[splitField.size() - 2]; //get the name of the field parent = last object in the chain //remove the field, to have only the parent object chain @@ -104,58 +116,57 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid for the object at the top of the chain! - Object filterValue = getFilter(field); - filterByCondition(newListRelatedFields, oldListRelatedFields, filterObjectChain, fieldName, filtered, filterValue); + filterValue = getFilter(field); + UTIL_Debug.debug('****Filter value: ' + filterValue); + filterByCondition(newListRelatedFields, oldListRelatedFields, filterObjectChain); UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); } - return filtered; } /******************************************************************************************************* * @description Queries the fields that are part of the relationship filter, since these values are not initially * present in the records the triggers acts on. - * @param filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name". * @param compList The list of records to query. * @return List A list of records pointing to the same records that are present in newList or oldList, * but containing only the fields defined in the query condition. The returned list is also in the same order as * newList or oldList. */ - private List queryRelatedFields(String filterField, List compList) { - Map compMap = new Map(compList); - Set compListIDs = compMap.keySet(); - //query filter values, in case they are not in the trigger - String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :compListIDs'; - UTIL_Debug.debug('****Relationship filter dynamic query: ' + dynamicQuery); - Map withRelatedFieldsMap = new Map(Database.query(dynamicQuery)); + private List queryRelatedFields(List compList) { List withRelatedFields = new List(); //We don't want to modify the original list, but use a new one instead. - //Let's make sure we return them in the same order the list passed as param - for(SObject compRecord : compList) { - withRelatedFields.add(withRelatedFieldsMap.get(compRecord.ID)); + if(compList != null) { + Map compMap = new Map(compList); + Set compListIDs = compMap.keySet(); + //query filter values, in case they are not in the trigger + String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :compListIDs'; + UTIL_Debug.debug('****Relationship filter dynamic query: ' + dynamicQuery); + Map withRelatedFieldsMap = new Map(Database.query(dynamicQuery)); + + //Let's make sure we return them in the same order the list passed as param + for(SObject compRecord : compList) { + withRelatedFields.add(withRelatedFieldsMap.get(compRecord.ID)); + } } return withRelatedFields; } /******************************************************************************************************* * @description Filters newList and oldList based on the value of a field on the trigger records. - * @param filterField The field to filter on. * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. */ - private FilteredLists filterByField(String filterField) { - FilteredLists filtered = new FilteredLists(); - + private void filterByField() { //get field type - Map fieldMap = describeObj.fields.getMap(); - Schema.SObjectField field = fieldMap.get(filterField); + Schema.SObjectField field = describeObj.fields.getMap().get(fieldName); + UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid! - Object filter = getFilter(field); - filterByCondition(null, null, null, filterField, filtered, filter); + filterValue = getFilter(field); + UTIL_Debug.debug('****Filter value: ' + filterValue); + filterByCondition(null, null, null); UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); } - return filtered; } /******************************************************************************************************* @@ -193,23 +204,21 @@ public with sharing class TDTM_Filter { * @param oldListRelatedFields A list of records pointing to the same records that are present in oldList, * but containing only the fields defined in the query condition. In the same order as newList. * @param filterObjectChain - * @param filterField The field to filter on. - * @param val The value to compare against when filtering, already in the correct type. * @return void */ private void filterByCondition(List newListRelatedFields, List oldListRelatedFields, - List filterObjectChain, String filterField, FilteredLists filtered, Object val) { + List filterObjectChain) { if(filterObjectChain == null) { //The field in in the same object the trigger fires on - filterList(newList, filterField, filtered.newList, val); - filterList(oldList, filterField, filtered.oldList, val); + filterList(newList, filtered.newList); + filterList(oldList, filtered.oldList); } else { //The field is in a related object - filterListByRelatedField(newListRelatedFields, newList, filtered.newList, filterObjectChain, filterField, val); - filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList, filterObjectChain, filterField, val); + filterListByRelatedField(newListRelatedFields, newList, filtered.newList, filterObjectChain); + filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList, filterObjectChain); } } private void filterListByRelatedField(List listRelatedFields, List originalList, - List filteredList, List filterObjectChain, String filterField, Object val) { + List filteredList, List filterObjectChain) { if(listRelatedFields != null && listRelatedFields.size() > 0) { for(Integer i = 0; i < listRelatedFields.size(); i++) { SObject o = listRelatedFields[i]; @@ -223,8 +232,8 @@ public with sharing class TDTM_Filter { } } //perform the filtering - UTIL_Debug.debug('****Filtering by field ' + filterField + ', with value ' + val + ' on object ' + o); - if(o != null && o.get(filterField) != val) { + UTIL_Debug.debug('****Filtering by field ' + fieldName + ', with value ' + filterValue + ' on object ' + o); + if(o != null && o.get(fieldName) != filterValue) { UTIL_Debug.debug('****Adding object ' + originalList[i] + ' to filtered list.'); filteredList.add(originalList[i]); } @@ -232,10 +241,10 @@ public with sharing class TDTM_Filter { } } - private void filterList(List listToFilter, String filterField, List filteredList, Object val) { + private void filterList(List listToFilter, List filteredList) { if(listToFilter != null && listToFilter.size() > 0) { for(SObject o : listToFilter) { - if(o.get(filterField) != val) { + if(o.get(fieldName) != filterValue) { filteredList.add(o); } } diff --git a/src/classes/TDTM_TriggerHandler.cls b/src/classes/TDTM_TriggerHandler.cls index 558192d45f..9abf81b25d 100644 --- a/src/classes/TDTM_TriggerHandler.cls +++ b/src/classes/TDTM_TriggerHandler.cls @@ -179,9 +179,9 @@ public with sharing class TDTM_TriggerHandler { TDTM_Filter filter = new TDTM_Filter(classToRunRecord, newList, oldList, describeObj); TDTM_Filter.FilteredLists filtered = filter.filter(); - UTIL_Debug.debug('****newList after filtering: ' + JSON.serializePretty(filtered.newList)); - if(filtered.newList.size() > 0) { + if(filtered != null && filtered.newList.size() > 0) { + UTIL_Debug.debug('****newList after filtering: ' + JSON.serializePretty(filtered.newList)); return classToRun.run(filtered.newList, filtered.oldList, thisAction, describeObj); } else { return classToRun.run(newList, oldList, thisAction, describeObj); From 36eeb4145db08cdba657e3f9228269d5bc900a33 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 12 Aug 2015 11:28:36 -0500 Subject: [PATCH 08/31] ApexDocs finished. --- src/classes/TDTM_Filter.cls | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 43dc962bc9..bb26f61f43 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -52,6 +52,9 @@ public with sharing class TDTM_Filter { Object filterValue; /* @description An instance of the wrapper object that contains the filtered newList and oldList. */ FilteredLists filtered; + /* @description The chain of parent objects used in the filter, not including the object the class is running on. + i.e., if the class runs on Contact this could be "Account", and if it runs on Opportunity "Contact.Account"*/ + List filterObjectChain; /******************************************************************************************************* * @description Constructor. Sets properties. @@ -67,6 +70,7 @@ public with sharing class TDTM_Filter { this.oldList = oldList; this.describeObj = describeObj; filtered = new FilteredLists(); + filterObjectChain = new List(); } /******************************************************************************************************* @@ -107,7 +111,6 @@ public with sharing class TDTM_Filter { String parentObjectName = splitField[splitField.size() - 2]; //get the name of the field parent = last object in the chain //remove the field, to have only the parent object chain - List filterObjectChain = new List(); for(Integer i = 0; i < (splitField.size() - 1); i++) filterObjectChain.add(splitField[i]); UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); @@ -118,7 +121,7 @@ public with sharing class TDTM_Filter { if(field != null) { //the field name is valid for the object at the top of the chain! filterValue = getFilter(field); UTIL_Debug.debug('****Filter value: ' + filterValue); - filterByCondition(newListRelatedFields, oldListRelatedFields, filterObjectChain); + filterByCondition(newListRelatedFields, oldListRelatedFields); UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -162,7 +165,7 @@ public with sharing class TDTM_Filter { if(field != null) { //the field name is valid! filterValue = getFilter(field); UTIL_Debug.debug('****Filter value: ' + filterValue); - filterByCondition(null, null, null); + filterByCondition(null, null); UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); @@ -203,22 +206,20 @@ public with sharing class TDTM_Filter { * but containing only the fields defined in the query condition. In the same order as newList. * @param oldListRelatedFields A list of records pointing to the same records that are present in oldList, * but containing only the fields defined in the query condition. In the same order as newList. - * @param filterObjectChain * @return void */ - private void filterByCondition(List newListRelatedFields, List oldListRelatedFields, - List filterObjectChain) { - if(filterObjectChain == null) { //The field in in the same object the trigger fires on + private void filterByCondition(List newListRelatedFields, List oldListRelatedFields) { + if(filterObjectChain.size() == 0) { //The field in in the same object the trigger fires on filterList(newList, filtered.newList); filterList(oldList, filtered.oldList); } else { //The field is in a related object - filterListByRelatedField(newListRelatedFields, newList, filtered.newList, filterObjectChain); - filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList, filterObjectChain); + filterListByRelatedField(newListRelatedFields, newList, filtered.newList); + filterListByRelatedField(oldListRelatedFields, oldList, filtered.oldList); } } private void filterListByRelatedField(List listRelatedFields, List originalList, - List filteredList, List filterObjectChain) { + List filteredList) { if(listRelatedFields != null && listRelatedFields.size() > 0) { for(Integer i = 0; i < listRelatedFields.size(); i++) { SObject o = listRelatedFields[i]; From 113ce61532b6290e010b5b3e7c6e3bc24d52045c Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 12 Aug 2015 11:29:28 -0500 Subject: [PATCH 09/31] ApexDocs finished (the previous commit was actually some additional refactoring). --- src/classes/TDTM_Filter.cls | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index bb26f61f43..a1db40b797 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -218,6 +218,16 @@ public with sharing class TDTM_Filter { } } + /******************************************************************************************************* + * @description Populates filteredList with the records from originalList that don't match the filtering + * criteria included in listRelatedFields. Used for filtering criteria based on a relationship. + * @param listRelatedFields A list of records pointing to the same records that are present in originalList, + * but containing only the fields defined in the query condition. In the same order as originalList. + * @param originalList The original list whose records not matching the filter criteria will be added to + * the resulting filtered list. + * @param filteredList The resulting filtered list. + * @return void + */ private void filterListByRelatedField(List listRelatedFields, List originalList, List filteredList) { if(listRelatedFields != null && listRelatedFields.size() > 0) { @@ -242,6 +252,13 @@ public with sharing class TDTM_Filter { } } + /******************************************************************************************************* + * @description Populates filteredList with the records from listToFilter that don't match the filtering + * criteria. + * @param listToFilter The list of records to filter. + * @param filteredList The resulting filtered list. + * @return void + */ private void filterList(List listToFilter, List filteredList) { if(listToFilter != null && listToFilter.size() > 0) { for(SObject o : listToFilter) { From a8212d95dff1eb62ee02ef44925bbf65cd79ec24 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 12 Aug 2015 17:06:53 -0500 Subject: [PATCH 10/31] Creating a passing and a failing test, and a workaround for the latter that doesn't actually work. --- src/classes/TDTM_Filter.cls | 16 ++++++- src/classes/TDTM_Filter_TEST.cls | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index a1db40b797..a9c5a9f678 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -109,13 +109,27 @@ public with sharing class TDTM_Filter { List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name fieldName = splitField[splitField.size() - 1]; //get the field name itself String parentObjectName = splitField[splitField.size() - 2]; //get the name of the field parent = last object in the chain + UTIL_Debug.debug('****splitField: ' + splitField); + + //For the special Parent field on Account case + /*if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { + UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); + parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe in line 127. + //splitField[splitField.size() - 2] = 'Account'; //Exception: Invalid relationship Account for Account, from getsObject in line 254. + //splitField[splitField.size() - 2] = 'Parent'; //This is what is done by default, w no chnage - acc.getsObject('Parent') returns null in line 254. + //splitField[splitField.size() - 2] = 'parentId'; //Exception: Invalid relationship parentId for Account, from getsObject in line 254. But this is the actual field name in the map. + }*/ //remove the field, to have only the parent object chain for(Integer i = 0; i < (splitField.size() - 1); i++) filterObjectChain.add(splitField[i]); UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - Schema.SObjectField field = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap().get(fieldName); + Map fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); + /*for(String fn : fieldsMap.keySet()) { + UTIL_Debug.debug('****Field name in map: ' + fn); + }*/ + Schema.SObjectField field = fieldsMap.get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid for the object at the top of the chain! diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 46f69eaebf..47292af73c 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -299,6 +299,88 @@ public with sharing class TDTM_Filter_TEST { Account accNotFiltered = new Account(Name = 'ABC'); insert new Account[] {accFiltered, accNotFiltered}; + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AccountId = accNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered.Id); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', AccountId = accFiltered.Id); + /*Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Account = accNotFiltered); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Account = accFiltered); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', Account = accNotFiltered); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Account = accFiltered);*/ + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); + } + + public static testmethod void doubleRelationshipField() { + if (strTestOnly != '*' && strTestOnly != 'doubleRelationshipField') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Account.Parent.Name', + Filter_Value__c = 'top'); + + Account topAccount = new Account(Name = 'top'); + insert topAccount; + + Account accFiltered = new Account(Name = 'Acme', Parent = topAccount); + Account accNotFiltered = new Account(Name = 'ABC'); + insert new Account[] {accFiltered, accNotFiltered}; + + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AccountId = accNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered.Id); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', AccountId = accFiltered.Id); + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); + } + + public static testmethod void recordTypeRelationship() { + if (strTestOnly != '*' && strTestOnly != 'recordTypeRelationship') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Account.RecordType.Name', + Filter_Value__c = 'Business Organization'); + + ID orgRecTypeID = Schema.Sobjecttype.Account.getRecordTypeInfosByName().get('Business Organization').getRecordTypeId(); + System.assertNotEquals(null, orgRecTypeID); //Let's make sure the record type exists in the system + + Account accFiltered = new Account(Name = 'Acme', RecordTypeId = orgRecTypeID); + Account accNotFiltered = new Account(Name = 'ABC'); + insert new Account[] {accFiltered, accNotFiltered}; + //Creating four contacts. two of them are not students, because they don't have a university email. Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AccountId = accNotFiltered.Id); Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); From 81f02fbdf1d2f6b4b969ef1a2ba0821ef73e0b07 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Thu, 13 Aug 2015 15:42:52 -0500 Subject: [PATCH 11/31] Changing test name, and using ParentId instead of Parent for the parent account (which doesn't fix it). --- src/classes/TDTM_Filter_TEST.cls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 47292af73c..2e282e0cea 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -323,8 +323,8 @@ public with sharing class TDTM_Filter_TEST { System.assertEquals(2, rels.size()); } - public static testmethod void doubleRelationshipField() { - if (strTestOnly != '*' && strTestOnly != 'doubleRelationshipField') return; + public static testmethod void accountParentField() { + if (strTestOnly != '*' && strTestOnly != 'accountParentField') return; insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', Field__c='ReportsToId', Relationship_Type__c = 'TestType'); @@ -338,7 +338,7 @@ public with sharing class TDTM_Filter_TEST { Account topAccount = new Account(Name = 'top'); insert topAccount; - Account accFiltered = new Account(Name = 'Acme', Parent = topAccount); + Account accFiltered = new Account(Name = 'Acme', ParentId = topAccount.Id); Account accNotFiltered = new Account(Name = 'ABC'); insert new Account[] {accFiltered, accNotFiltered}; From 927f05d9963dd1038fa11f064da2f303e12a1931 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Thu, 13 Aug 2015 17:58:17 -0500 Subject: [PATCH 12/31] Tests finally pass. It turns out we were just not handling the null parent account case properly. --- src/classes/TDTM_Filter.cls | 32 ++++++++++++++------------------ src/classes/TDTM_Filter_TEST.cls | 11 ++++++----- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index a9c5a9f678..ebf8812feb 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -112,13 +112,10 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****splitField: ' + splitField); //For the special Parent field on Account case - /*if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { + if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); - parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe in line 127. - //splitField[splitField.size() - 2] = 'Account'; //Exception: Invalid relationship Account for Account, from getsObject in line 254. - //splitField[splitField.size() - 2] = 'Parent'; //This is what is done by default, w no chnage - acc.getsObject('Parent') returns null in line 254. - //splitField[splitField.size() - 2] = 'parentId'; //Exception: Invalid relationship parentId for Account, from getsObject in line 254. But this is the actual field name in the map. - }*/ + parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. + } //remove the field, to have only the parent object chain for(Integer i = 0; i < (splitField.size() - 1); i++) @@ -126,9 +123,6 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); Map fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); - /*for(String fn : fieldsMap.keySet()) { - UTIL_Debug.debug('****Field name in map: ' + fn); - }*/ Schema.SObjectField field = fieldsMap.get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); @@ -159,8 +153,7 @@ public with sharing class TDTM_Filter { String dynamicQuery = 'select ' + filterField + ' from ' + describeObj.getName() + ' where ID in :compListIDs'; UTIL_Debug.debug('****Relationship filter dynamic query: ' + dynamicQuery); Map withRelatedFieldsMap = new Map(Database.query(dynamicQuery)); - - //Let's make sure we return them in the same order the list passed as param + //Let's make sure we return them in the same order as the list passed as param for(SObject compRecord : compList) { withRelatedFields.add(withRelatedFieldsMap.get(compRecord.ID)); } @@ -242,19 +235,22 @@ public with sharing class TDTM_Filter { * @param filteredList The resulting filtered list. * @return void */ - private void filterListByRelatedField(List listRelatedFields, List originalList, + private void filterListByRelatedField(List listWithRelatedFields, List originalList, List filteredList) { - if(listRelatedFields != null && listRelatedFields.size() > 0) { - for(Integer i = 0; i < listRelatedFields.size(); i++) { - SObject o = listRelatedFields[i]; + if(listWithRelatedFields != null && listWithRelatedFields.size() > 0) { + for(Integer i = 0; i < listWithRelatedFields.size(); i++) { + SObject o = listWithRelatedFields[i]; UTIL_Debug.debug('****Object in trigger: ' + o); //traverse parent relationships until the last one if (o != null) { for (String parentObj : filterObjectChain) { - UTIL_Debug.debug('****Object to traverse: ' + parentObj); - o = o.getsObject(parentObj); - UTIL_Debug.debug('****Parent object: ' + o); + if(o != null) { + UTIL_Debug.debug('****Object to traverse: ' + parentObj); + o = o.getsObject(parentObj); + UTIL_Debug.debug('****Parent object: ' + o); + } } + } //perform the filtering UTIL_Debug.debug('****Filtering by field ' + fieldName + ', with value ' + filterValue + ' on object ' + o); diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 2e282e0cea..4f7dad5827 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -333,13 +333,14 @@ public with sharing class TDTM_Filter_TEST { insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Account.Parent.Name', - Filter_Value__c = 'top'); + Filter_Value__c = 'top1'); - Account topAccount = new Account(Name = 'top'); - insert topAccount; + Account topAccountFiltered = new Account(Name = 'top1'); + Account topAccountNotFiltered = new Account(Name = 'top2'); + insert new Account[] {topAccountFiltered, topAccountNotFiltered}; - Account accFiltered = new Account(Name = 'Acme', ParentId = topAccount.Id); - Account accNotFiltered = new Account(Name = 'ABC'); + Account accFiltered = new Account(Name = 'Acme', ParentId = topAccountFiltered.Id); + Account accNotFiltered = new Account(Name = 'ABC', ParentId = topAccountNotFiltered.Id); insert new Account[] {accFiltered, accNotFiltered}; //Creating four contacts. two of them are not students, because they don't have a university email. From 5192a254d0cb0830e2f27f2f01eaa7a648e16b2a Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 14 Aug 2015 10:40:08 -0500 Subject: [PATCH 13/31] Handling the case of a null parent in the relationship chain. --- src/classes/TDTM_Filter.cls | 16 +++++++++++++--- src/classes/TDTM_Filter_TEST.cls | 9 +++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index ebf8812feb..2726cc7850 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -241,20 +241,30 @@ public with sharing class TDTM_Filter { for(Integer i = 0; i < listWithRelatedFields.size(); i++) { SObject o = listWithRelatedFields[i]; UTIL_Debug.debug('****Object in trigger: ' + o); + Boolean addDirectly = false; //traverse parent relationships until the last one - if (o != null) { + if (o != null) { //if the object at the bottom of the chain (the one in the trigger) isn't null for (String parentObj : filterObjectChain) { - if(o != null) { + if(o != null) { //if each following object in the chain isn't null UTIL_Debug.debug('****Object to traverse: ' + parentObj); o = o.getsObject(parentObj); UTIL_Debug.debug('****Parent object: ' + o); + } else { + UTIL_Debug.debug('****Object in the chain is null, we should add element directly to filteredList'); + addDirectly = true; + break; } } } + //in case the topmost object in the chain is null + if(o == null) { + UTIL_Debug.debug('****Top object in the chain is null, we should add element directly to filteredList'); + addDirectly = true; + } //perform the filtering UTIL_Debug.debug('****Filtering by field ' + fieldName + ', with value ' + filterValue + ' on object ' + o); - if(o != null && o.get(fieldName) != filterValue) { + if(addDirectly || (o != null && o.get(fieldName) != filterValue)) { UTIL_Debug.debug('****Adding object ' + originalList[i] + ' to filtered list.'); filteredList.add(originalList[i]); } diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 4f7dad5827..47f015c40b 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -304,10 +304,6 @@ public with sharing class TDTM_Filter_TEST { Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered.Id); Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', AccountId = accFiltered.Id); - /*Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Account = accNotFiltered); - Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Account = accFiltered); - Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', Account = accNotFiltered); - Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Account = accFiltered);*/ Contact[] contacts = new Contact[] {c1, c2, c3, c4}; insert contacts; @@ -341,12 +337,13 @@ public with sharing class TDTM_Filter_TEST { Account accFiltered = new Account(Name = 'Acme', ParentId = topAccountFiltered.Id); Account accNotFiltered = new Account(Name = 'ABC', ParentId = topAccountNotFiltered.Id); - insert new Account[] {accFiltered, accNotFiltered}; + Account accNotFiltered2 = new Account(Name = 'XYC'); + insert new Account[] {accFiltered, accNotFiltered, accNotFiltered2}; //Creating four contacts. two of them are not students, because they don't have a university email. Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AccountId = accNotFiltered.Id); Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AccountId = accFiltered.Id); - Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3', AccountId = accNotFiltered2.Id); Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', AccountId = accFiltered.Id); Contact[] contacts = new Contact[] {c1, c2, c3, c4}; insert contacts; From bd54bedbccc2a198b93f88a3bf5d9e629f644622 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 14 Aug 2015 10:57:23 -0500 Subject: [PATCH 14/31] Treating any other case as a String when filtering. --- src/classes/TDTM_Filter.cls | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 2726cc7850..de5e57f0a8 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -188,12 +188,8 @@ public with sharing class TDTM_Filter { //let's find the field type Schema.DisplayType fieldType = field.getDescribe().getType(); UTIL_Debug.debug('****Filter field type: ' + fieldType); - String val = String.valueOf(classToRunRecord.get('Filter_Value__c')); - - if(fieldType == Schema.DisplayType.String || fieldType == Schema.DisplayType.Email - || fieldType == Schema.DisplayType.Phone || fieldType == Schema.DisplayType.Picklist) { - return val; - } else if(fieldType == Schema.DisplayType.Boolean) { + String val = String.valueOf(classToRunRecord.get('Filter_Value__c')); + if(fieldType == Schema.DisplayType.Boolean) { if(val == 'true') { return true; } else if(val == 'false') { @@ -203,6 +199,8 @@ public with sharing class TDTM_Filter { return Date.parse(val); } else if(fieldType == Schema.DisplayType.Reference) { return ID.valueOf(val); + } else { //We'll treat everything else as a string, including String, Email, Phone, and Picklist + return val; } return null; } From 9108c5d1d4a0f3439c986b7bcb576d6fa6369302 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 14 Aug 2015 13:09:58 -0500 Subject: [PATCH 15/31] Changing field label in Address. --- src/objects/Address__c.object | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objects/Address__c.object b/src/objects/Address__c.object index cf74fea2fe..b7eaeb2f63 100644 --- a/src/objects/Address__c.object +++ b/src/objects/Address__c.object @@ -215,8 +215,8 @@ MailingCity__c & IF(LEN(MailingCity__c) > 0, ", ", "" Parent_Account__c false - The Household Account this Address is for. - + The Account this Address is for. + Account Addresses Addresses From e95cb0db1104860aa6d9617105a81b79d4ec2ac2 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 14 Aug 2015 15:27:29 -0500 Subject: [PATCH 16/31] Making it work for custom objects too. --- src/classes/TDTM_Filter.cls | 8 +++++- src/classes/TDTM_Filter_TEST.cls | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index de5e57f0a8..59302ad227 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -117,6 +117,12 @@ public with sharing class TDTM_Filter { parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. } + //For the Custom Object case + if(splitField.size() > 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { + UTIL_Debug.debug('****The parent object is custom'); + parentObjectName = parentObjectName.replace('__r', '__c'); + } + //remove the field, to have only the parent object chain for(Integer i = 0; i < (splitField.size() - 1); i++) filterObjectChain.add(splitField[i]); @@ -263,7 +269,7 @@ public with sharing class TDTM_Filter { //perform the filtering UTIL_Debug.debug('****Filtering by field ' + fieldName + ', with value ' + filterValue + ' on object ' + o); if(addDirectly || (o != null && o.get(fieldName) != filterValue)) { - UTIL_Debug.debug('****Adding object ' + originalList[i] + ' to filtered list.'); + UTIL_Debug.debug('****Adding object to filtered list: ' + originalList[i]); filteredList.add(originalList[i]); } } diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 47f015c40b..1f2e9998b4 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -398,4 +398,48 @@ public with sharing class TDTM_Filter_TEST { Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; System.assertEquals(2, rels.size()); } + + public static testmethod void customObject() { + if (strTestOnly != '*' && strTestOnly != 'customObject') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Current_Address__r.Parent_Account__r.RecordType.Name', + Filter_Value__c = 'Business Organization'); + + ID orgRecTypeID = Schema.Sobjecttype.Account.getRecordTypeInfosByName().get('Business Organization').getRecordTypeId(); + System.assertNotEquals(null, orgRecTypeID); //Let's make sure the record type exists in the system + + Account accFiltered = new Account(Name = 'Acme', RecordTypeId = orgRecTypeID); + Account accNotFiltered = new Account(Name = 'ABC'); + insert new Account[] {accFiltered, accNotFiltered}; + + Address__c addressFiltered = new Address__c(Parent_Account__c = accFiltered.Id); + Address__c addressNotFiltered = new Address__c(Parent_Account__c = accNotFiltered.Id); + insert new Address__c[] {addressFiltered, addressNotFiltered}; + + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Current_Address__c = addressNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Current_Address__c = addressFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3'); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Current_Address__c = addressFiltered.Id); + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); + + } } \ No newline at end of file From 2f1fa808ac6f29daf421222e8e4fefa188396899 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Fri, 14 Aug 2015 15:42:28 -0500 Subject: [PATCH 17/31] Removing couple of debug statements. --- src/classes/TDTM_Filter.cls | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 59302ad227..e0eda96a55 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -78,7 +78,6 @@ public with sharing class TDTM_Filter { * @return FilteredLists An instance of the wrapper object that contains the filtered newList and oldList. */ public FilteredLists filter() { - UTIL_Debug.debug('****New list before filtering: ' + JSON.serializePretty(newList)); try { filterField = String.valueOf(classToRunRecord.get('Filter_Field__c')); if(filterField != null) { @@ -117,7 +116,7 @@ public with sharing class TDTM_Filter { parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. } - //For the Custom Object case + //For the Custom Object at the top of the chain case if(splitField.size() > 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { UTIL_Debug.debug('****The parent object is custom'); parentObjectName = parentObjectName.replace('__r', '__c'); @@ -136,7 +135,6 @@ public with sharing class TDTM_Filter { filterValue = getFilter(field); UTIL_Debug.debug('****Filter value: ' + filterValue); filterByCondition(newListRelatedFields, oldListRelatedFields); - UTIL_Debug.debug('****Filtered new list: \n ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); } @@ -179,7 +177,6 @@ public with sharing class TDTM_Filter { filterValue = getFilter(field); UTIL_Debug.debug('****Filter value: ' + filterValue); filterByCondition(null, null); - UTIL_Debug.debug('****Filtered new list: ' + JSON.serializePretty(filtered.newList)); } else { UTIL_Debug.debug('****The field name is invalid.'); } From 5cb01c1e8bd260f5aed138eac6fb5c6ec48488c4 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 10:24:07 -0500 Subject: [PATCH 18/31] Adding test for custom object at the top of the filter chain. --- src/classes/TDTM_Filter_TEST.cls | 45 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 1f2e9998b4..0f2b3ed095 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -42,7 +42,7 @@ public with sharing class TDTM_Filter_TEST { private static string strTestOnly = '*'; public static testmethod void emailField() { - if (strTestOnly != '*' && strTestOnly != 'emailField') return; + if (strTestOnly != '*' && strTestOnly != '*') return; insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', Field__c='ReportsToId', Relationship_Type__c = 'TestType'); @@ -439,7 +439,46 @@ public with sharing class TDTM_Filter_TEST { //Only those from c1 and c3 should have had a relationship automatically created. Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; - System.assertEquals(2, rels.size()); - + System.assertEquals(2, rels.size()); + } + + public static testmethod void customObjectTopOfChain() { + if (strTestOnly != '*' && strTestOnly != 'customObjectTopOfChain') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Current_Address__r.Parent_Account__r.Name', + Filter_Value__c = 'topAcc'); + + Account accFiltered = new Account(Name = 'topAcc'); + Account accNotFiltered = new Account(Name = 'ABC'); + insert new Account[] {accFiltered, accNotFiltered}; + + Address__c addressFiltered = new Address__c(Parent_Account__c = accFiltered.Id); + Address__c addressNotFiltered = new Address__c(Parent_Account__c = accNotFiltered.Id); + insert new Address__c[] {addressFiltered, addressNotFiltered}; + + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Current_Address__c = addressNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Current_Address__c = addressFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3'); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Current_Address__c = addressFiltered.Id); + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); } } \ No newline at end of file From 538f64202cd47615864e845aad8bef48f012d083 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 10:39:14 -0500 Subject: [PATCH 19/31] Adding test for custom object as the only one if the filter chain. --- src/classes/TDTM_Filter_TEST.cls | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 0f2b3ed095..c04d90c561 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -481,4 +481,43 @@ public with sharing class TDTM_Filter_TEST { Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; System.assertEquals(2, rels.size()); } + + public static testmethod void customObjectOnlyOneInChain() { + if (strTestOnly != '*' && strTestOnly != 'customObjectOnlyOneInChain') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'Current_Address__r.Default_Address__c', + Filter_Value__c = 'true'); + + Account topAcc = new Account(Name = 'topAcc'); + insert new Account[] {topAcc}; + + Address__c addressFiltered = new Address__c(Parent_Account__c = topAcc.Id, Default_Address__c = true); + Address__c addressNotFiltered = new Address__c(Parent_Account__c = topAcc.Id, Default_Address__c = false); + insert new Address__c[] {addressFiltered, addressNotFiltered}; + + //Creating four contacts. two of them are not students, because they don't have a university email. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', Current_Address__c = addressNotFiltered.Id); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', Current_Address__c = addressFiltered.Id); + Contact c3 = new Contact(FirstName = 'Test', LastName = 'Testerson3'); + Contact c4 = new Contact(FirstName = 'Test', LastName = 'Testerson4', Current_Address__c = addressFiltered.Id); + Contact[] contacts = new Contact[] {c1, c2, c3, c4}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + c2.ReportsToId = c3.Id; + c3.ReportsToId = c4.Id; + update contacts; + + //Only those from c1 and c3 should have had a relationship automatically created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(2, rels.size()); + } } \ No newline at end of file From bcd8dd52f00f3b48b829b99c38777b300ede7284 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 10:42:49 -0500 Subject: [PATCH 20/31] Oops. --- src/classes/TDTM_Filter_TEST.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index c04d90c561..57a19e3d91 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -42,7 +42,7 @@ public with sharing class TDTM_Filter_TEST { private static string strTestOnly = '*'; public static testmethod void emailField() { - if (strTestOnly != '*' && strTestOnly != '*') return; + if (strTestOnly != '*' && strTestOnly != 'emailField') return; insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', Field__c='ReportsToId', Relationship_Type__c = 'TestType'); From 272ad4f9be79fb74c7994583c7189acab73174c2 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 12:10:16 -0500 Subject: [PATCH 21/31] Getting the type of the object that is the parent of the filter field, even if it's custom. --- src/classes/TDTM_Filter.cls | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index e0eda96a55..95507ffcbe 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -104,6 +104,7 @@ public with sharing class TDTM_Filter { private void filterByRelationship() { List newListRelatedFields = queryRelatedFields(newList); List oldListRelatedFields = queryRelatedFields(oldList); + Map fieldsMap; List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name fieldName = splitField[splitField.size() - 1]; //get the field name itself @@ -111,15 +112,36 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****splitField: ' + splitField); //For the special Parent field on Account case - if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { + /*if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. - } + }*/ //For the Custom Object at the top of the chain case - if(splitField.size() > 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { + if(splitField.size() >= 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { UTIL_Debug.debug('****The parent object is custom'); parentObjectName = parentObjectName.replace('__r', '__c'); + + String parentOfParent; + //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent + //is the object in the trigger. + if(splitField.size() > 2) { + parentOfParent = splitField[-3]; + } else { + parentOfParent = describeObj.getName(); + } + UTIL_Debug.debug('****Parent of parent: ' + parentOfParent); + + //Get the object type of the field. For example, the field might be called Current_Address__c, but the object is Address__c + fieldsMap = UTIL_Describe.getObjectDescribe(parentOfParent).fields.getMap(); + Schema.DescribeFieldResult customObjParentDescribe = fieldsMap.get(parentObjectName).getDescribe(); + List refs = customObjParentDescribe.getReferenceTo(); + if(refs != null && refs.size() == 1) { + String objPointingTo = refs[0].getDescribe().getName(); + UTIL_Debug.debug('****Object with filter field is of type ' + objPointingTo); + } else if(refs.size() > 1) { + UTIL_Debug.debug('****Field could be pointing to more than one type of object. We will have to iterate through each and see which one contains that field.'); + } } //remove the field, to have only the parent object chain @@ -127,7 +149,8 @@ public with sharing class TDTM_Filter { filterObjectChain.add(splitField[i]); UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - Map fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); + if(fieldsMap == null) + fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); Schema.SObjectField field = fieldsMap.get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); From 846fba666cb46be06b4ea46264d3744791055766 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 13:11:22 -0500 Subject: [PATCH 22/31] All filter tests, except customObjectTopOfChain, pass. --- src/classes/TDTM_Filter.cls | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 95507ffcbe..8b4cb31d8f 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -112,16 +112,17 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****splitField: ' + splitField); //For the special Parent field on Account case - /*if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { + if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. - }*/ + } //For the Custom Object at the top of the chain case if(splitField.size() >= 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { UTIL_Debug.debug('****The parent object is custom'); parentObjectName = parentObjectName.replace('__r', '__c'); + //Getting the type of the object that is the parent of the filter field, even if it's custom. String parentOfParent; //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent //is the object in the trigger. @@ -132,16 +133,18 @@ public with sharing class TDTM_Filter { } UTIL_Debug.debug('****Parent of parent: ' + parentOfParent); + String objPointingTo; //Get the object type of the field. For example, the field might be called Current_Address__c, but the object is Address__c fieldsMap = UTIL_Describe.getObjectDescribe(parentOfParent).fields.getMap(); Schema.DescribeFieldResult customObjParentDescribe = fieldsMap.get(parentObjectName).getDescribe(); List refs = customObjParentDescribe.getReferenceTo(); if(refs != null && refs.size() == 1) { - String objPointingTo = refs[0].getDescribe().getName(); + objPointingTo = refs[0].getDescribe().getName(); UTIL_Debug.debug('****Object with filter field is of type ' + objPointingTo); } else if(refs.size() > 1) { UTIL_Debug.debug('****Field could be pointing to more than one type of object. We will have to iterate through each and see which one contains that field.'); } + parentObjectName = objPointingTo; } //remove the field, to have only the parent object chain @@ -149,8 +152,7 @@ public with sharing class TDTM_Filter { filterObjectChain.add(splitField[i]); UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - if(fieldsMap == null) - fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); + fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); Schema.SObjectField field = fieldsMap.get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); From d79a455244f586ea5d1e1acefd34c270b06483fe Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Mon, 17 Aug 2015 14:40:16 -0500 Subject: [PATCH 23/31] Minor fix. Doesn't actually fix the test. --- src/classes/TDTM_Filter.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 8b4cb31d8f..85ee4ef19a 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -127,7 +127,7 @@ public with sharing class TDTM_Filter { //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent //is the object in the trigger. if(splitField.size() > 2) { - parentOfParent = splitField[-3]; + parentOfParent = splitField[splitField.size() - 3]; } else { parentOfParent = describeObj.getName(); } From 3a1d0e29b4ad09db51507d6d5e816647598f3890 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 18 Aug 2015 12:23:25 -0500 Subject: [PATCH 24/31] Getting there. We went all the way down the object chain to find the object type of the first field that is not named as its matching object. Now we have to go back up the chain. --- src/classes/TDTM_Filter.cls | 179 ++++++++++++++++++++++++++---------- 1 file changed, 128 insertions(+), 51 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 85ee4ef19a..46fcdec56c 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -43,7 +43,7 @@ public with sharing class TDTM_Filter { /* @description The records that were passed to the trigger as trigger.old. */ List oldList; /* @description The type of SObject the class runs for. */ - Schema.DescribeSObjectResult describeObj; + DescribeSObjectResult describeObj; /* @description filterField The field to filter on, including the whole relationship chain, i.e. "Account.Name".*/ String filterField; /* The field to filter on in the parent object, i.e. "Name". */ @@ -64,7 +64,7 @@ public with sharing class TDTM_Filter { * @param describeObj The type of SObject the class runs for. */ public TDTM_Filter(SObject classToRunRecord, List newList, List oldList, - Schema.DescribeSObjectResult describeObj) { + DescribeSObjectResult describeObj) { this.classToRunRecord = classToRunRecord; this.newList = newList; this.oldList = oldList; @@ -104,58 +104,34 @@ public with sharing class TDTM_Filter { private void filterByRelationship() { List newListRelatedFields = queryRelatedFields(newList); List oldListRelatedFields = queryRelatedFields(oldList); - Map fieldsMap; - List splitField = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name - fieldName = splitField[splitField.size() - 1]; //get the field name itself - String parentObjectName = splitField[splitField.size() - 2]; //get the name of the field parent = last object in the chain - UTIL_Debug.debug('****splitField: ' + splitField); + List filterFullChain = (filterField.split('\\.', 0)); //separate cross object references, i.e. account.name + fieldName = filterFullChain[filterFullChain.size() - 1]; //get the field name itself + String parentObjectName = filterFullChain[filterFullChain.size() - 2]; //get the name of the field parent = last object in the chain + UTIL_Debug.debug('****filterFullChain: ' + filterFullChain); + //remove the field, to have only the parent object chain + for(Integer i = 0; i < (filterFullChain.size() - 1); i++) + filterObjectChain.add(filterFullChain[i]); + UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); //For the special Parent field on Account case - if(splitField.size() > 2 && splitField[splitField.size() - 3] == 'Account' && splitField[splitField.size() - 2] == 'Parent') { + if(filterFullChain.size() > 2 && filterFullChain[filterFullChain.size() - 3] == 'Account' && filterFullChain[filterFullChain.size() - 2] == 'Parent') { UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. } - //For the Custom Object at the top of the chain case - if(splitField.size() >= 2 && (splitField[splitField.size() - 2]).endsWith('__r')) { - UTIL_Debug.debug('****The parent object is custom'); - parentObjectName = parentObjectName.replace('__r', '__c'); - - //Getting the type of the object that is the parent of the filter field, even if it's custom. - String parentOfParent; - //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent - //is the object in the trigger. - if(splitField.size() > 2) { - parentOfParent = splitField[splitField.size() - 3]; - } else { - parentOfParent = describeObj.getName(); - } - UTIL_Debug.debug('****Parent of parent: ' + parentOfParent); - - String objPointingTo; - //Get the object type of the field. For example, the field might be called Current_Address__c, but the object is Address__c - fieldsMap = UTIL_Describe.getObjectDescribe(parentOfParent).fields.getMap(); - Schema.DescribeFieldResult customObjParentDescribe = fieldsMap.get(parentObjectName).getDescribe(); - List refs = customObjParentDescribe.getReferenceTo(); - if(refs != null && refs.size() == 1) { - objPointingTo = refs[0].getDescribe().getName(); - UTIL_Debug.debug('****Object with filter field is of type ' + objPointingTo); - } else if(refs.size() > 1) { - UTIL_Debug.debug('****Field could be pointing to more than one type of object. We will have to iterate through each and see which one contains that field.'); - } - parentObjectName = objPointingTo; - } - - //remove the field, to have only the parent object chain - for(Integer i = 0; i < (splitField.size() - 1); i++) - filterObjectChain.add(splitField[i]); - UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - - fieldsMap = UTIL_Describe.getObjectDescribe(parentObjectName).fields.getMap(); - Schema.SObjectField field = fieldsMap.get(fieldName); + UTIL_Debug.debug('****Bottom child: ' + fieldName); + UTIL_Debug.debug('****Bottom parent: ' + parentObjectName); + SObjectField field = getSOField(parentObjectName, fieldName); UTIL_Debug.debug('****Field in object: ' + field); + if(field == null) { + //If the field is not valid, go down the object chain until we find one that is valid. Then go back up to find the object type + //at each level, until we get to the type of the parent of the filter field. + SObjectField topFirstValidField = findValidObjectInChain(filterObjectChain.size() - 1); + UTIL_Debug.debug('****topFirstValidField: ' + topFirstValidField); + //Now go back up the chain... + } if(field != null) { //the field name is valid for the object at the top of the chain! filterValue = getFilter(field); UTIL_Debug.debug('****Filter value: ' + filterValue); @@ -165,6 +141,107 @@ public with sharing class TDTM_Filter { } } + private SObjectField findValidObjectInChain(Integer objectChainIndex) { + UTIL_Debug.debug('****objectChainIndex: ' + objectChainIndex); + if(objectChainIndex == 0) { + UTIL_Debug.debug('****Top child: ' + filterObjectChain[objectChainIndex]); + UTIL_Debug.debug('****Top parent: ' + describeObj.getName()); + return getSOField(describeObj.getName(), filterObjectChain[objectChainIndex]); + } else { + String child = filterObjectChain[objectChainIndex]; + UTIL_Debug.debug('****Child: ' + child); + String parent = filterObjectChain[objectChainIndex -1]; + UTIL_Debug.debug('****Parent: ' + parent); + String parentObjectName = getObjectTypeReferenced(parent, child); + UTIL_Debug.debug('****parentObjectName: ' + parentObjectName); + if(parentObjectName != null) { + SObjectField field = getSOField(parentObjectName, child); + UTIL_Debug.debug('****Field in findValidObjectInChain: ' + field); + if(field == null) { + return findValidObjectInChain(--objectChainIndex); + } else { + return field; + } + } else { + return findValidObjectInChain(--objectChainIndex); + } + } + } + + /******************************************************************************************************* + * @description Getting the type of the object that is the parent of the filter field, even if it's custom. + */ + /*private String getTypeOfParentOfFilterField(String parentObjectName, List filterObjectChain) { + //For the Custom Object at the top of the chain case + if(filterObjectChain.size() >= 1 && (filterObjectChain[filterObjectChain.size() - 1]).endsWith('__r')) { + UTIL_Debug.debug('****The parent object is custom'); + parentObjectName = parentObjectName.replace('__r', '__c'); + } + String parentOfParent; + //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent + //is the object in the trigger. + if(filterObjectChain.size() > 1) { + parentOfParent = filterObjectChain[filterObjectChain.size() - 2]; + } else { + parentOfParent = describeObj.getName(); + } + UTIL_Debug.debug('****Parent of parent: ' + parentOfParent); + return getObjectTypeReferenced(parentOfParent, parentObjectName); + }*/ + + /******************************************************************************************************* + * @description Get the object type of the filter field. For example, the field might be called Current_Address__c, + but the object is Address__c. + */ + private String getObjectTypeReferenced(String parent, String child) { + String objPointingTo; + DescribeSObjectResult objectDescribe; + try { + if(parent.endsWith('__r')) + parent = parent.replace('__r', '__c'); + if(child.endsWith('__r')) + child = child.replace('__r', '__c'); + objectDescribe = UTIL_Describe.getObjectDescribe(parent); + if(objectDescribe != null) { + Map fieldsMap = objectDescribe.fields.getMap(); + DescribeFieldResult customObjParentDescribe = fieldsMap.get(child).getDescribe(); + List refs = customObjParentDescribe.getReferenceTo(); + if(refs != null && refs.size() == 1) { + objPointingTo = refs[0].getDescribe().getName(); + UTIL_Debug.debug('****Child object is of type ' + objPointingTo); + } else if(refs.size() > 1) { + UTIL_Debug.debug('****Field could be pointing to more than one type of object. ' + + ' We should iterate through each and see which one contains that field.'); + } + } + } catch(UTIL_Describe.SchemaDescribeException e) { + UTIL_Debug.debug('****Invalid object name: ' + parent); + return null; + } + return objPointingTo; + } + + private SObjectField getSOField(String parentObjectName, String fieldName) { + if(parentObjectName.endsWith('__r')) + parentObjectName = parentObjectName.replace('__r', '__c'); + if(fieldName.endsWith('__r')) + fieldName = fieldName.replace('__r', '__c'); + UTIL_Debug.debug('****Parent in getSOField: ' + parentObjectName); + UTIL_Debug.debug('****Child in getSOField: ' + fieldName); + try { + DescribeSObjectResult objectDescribe = UTIL_Describe.getObjectDescribe(parentObjectName); + if(objectDescribe != null) { + Map fieldsMap = objectDescribe.fields.getMap(); + return fieldsMap.get(fieldName); + } else { + return null; + } + } catch(UTIL_Describe.SchemaDescribeException e) { + UTIL_Debug.debug('****Invalid object name: ' + parentObjectName); + return null; + } + } + /******************************************************************************************************* * @description Queries the fields that are part of the relationship filter, since these values are not initially * present in the records the triggers acts on. @@ -196,7 +273,7 @@ public with sharing class TDTM_Filter { */ private void filterByField() { //get field type - Schema.SObjectField field = describeObj.fields.getMap().get(fieldName); + SObjectField field = describeObj.fields.getMap().get(fieldName); UTIL_Debug.debug('****Field in object: ' + field); if(field != null) { //the field name is valid! filterValue = getFilter(field); @@ -212,20 +289,20 @@ public with sharing class TDTM_Filter { * @param Field The SObjectField used in the filtering comparison. * @return Object The value to compare against when filtering, already in the correct type. */ - private Object getFilter(Schema.SObjectField field) { + private Object getFilter(SObjectField field) { //let's find the field type - Schema.DisplayType fieldType = field.getDescribe().getType(); + DisplayType fieldType = field.getDescribe().getType(); UTIL_Debug.debug('****Filter field type: ' + fieldType); String val = String.valueOf(classToRunRecord.get('Filter_Value__c')); - if(fieldType == Schema.DisplayType.Boolean) { + if(fieldType == DisplayType.Boolean) { if(val == 'true') { return true; } else if(val == 'false') { return false; } - } else if(fieldType == Schema.DisplayType.Date) { + } else if(fieldType == DisplayType.Date) { return Date.parse(val); - } else if(fieldType == Schema.DisplayType.Reference) { + } else if(fieldType == DisplayType.Reference) { return ID.valueOf(val); } else { //We'll treat everything else as a string, including String, Email, Phone, and Picklist return val; From 753c887688e65ed917ba61a615c7bc37e4373cb1 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 18 Aug 2015 14:28:06 -0500 Subject: [PATCH 25/31] Wrapper added to see if this helps traversing the chain. Also some refactoring. --- src/classes/TDTM_Filter.cls | 79 +++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 46fcdec56c..dbd01c5a25 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -128,9 +128,9 @@ public with sharing class TDTM_Filter { if(field == null) { //If the field is not valid, go down the object chain until we find one that is valid. Then go back up to find the object type //at each level, until we get to the type of the parent of the filter field. - SObjectField topFirstValidField = findValidObjectInChain(filterObjectChain.size() - 1); + ChainLink topFirstValidField = findValidObjectInChain(filterObjectChain.size() - 1); UTIL_Debug.debug('****topFirstValidField: ' + topFirstValidField); - //Now go back up the chain... + //Now go back up the chain. What we really want is the object type of the parent of the filter field. } if(field != null) { //the field name is valid for the object at the top of the chain! filterValue = getFilter(field); @@ -141,53 +141,37 @@ public with sharing class TDTM_Filter { } } - private SObjectField findValidObjectInChain(Integer objectChainIndex) { + private ChainLink findValidObjectInChain(Integer objectChainIndex) { UTIL_Debug.debug('****objectChainIndex: ' + objectChainIndex); + SObjectField field; if(objectChainIndex == 0) { UTIL_Debug.debug('****Top child: ' + filterObjectChain[objectChainIndex]); UTIL_Debug.debug('****Top parent: ' + describeObj.getName()); - return getSOField(describeObj.getName(), filterObjectChain[objectChainIndex]); + field = getSOField(describeObj.getName(), filterObjectChain[objectChainIndex]); + String objectReferenced = getObjectTypeReferenced(describeObj.getName(), filterObjectChain[objectChainIndex]); + ChainLink link = new ChainLink(field, objectReferenced, describeObj.getName(), objectChainIndex); + return link; } else { String child = filterObjectChain[objectChainIndex]; UTIL_Debug.debug('****Child: ' + child); String parent = filterObjectChain[objectChainIndex -1]; UTIL_Debug.debug('****Parent: ' + parent); - String parentObjectName = getObjectTypeReferenced(parent, child); - UTIL_Debug.debug('****parentObjectName: ' + parentObjectName); - if(parentObjectName != null) { - SObjectField field = getSOField(parentObjectName, child); + String objectReferenced = getObjectTypeReferenced(parent, child); + UTIL_Debug.debug('****objectReferenced: ' + objectReferenced); + if(parent != null) { + field = getSOField(parent, child); UTIL_Debug.debug('****Field in findValidObjectInChain: ' + field); if(field == null) { return findValidObjectInChain(--objectChainIndex); } else { - return field; + ChainLink link = new ChainLink(field, objectReferenced, parent, objectChainIndex); + return link; } } else { return findValidObjectInChain(--objectChainIndex); } } } - - /******************************************************************************************************* - * @description Getting the type of the object that is the parent of the filter field, even if it's custom. - */ - /*private String getTypeOfParentOfFilterField(String parentObjectName, List filterObjectChain) { - //For the Custom Object at the top of the chain case - if(filterObjectChain.size() >= 1 && (filterObjectChain[filterObjectChain.size() - 1]).endsWith('__r')) { - UTIL_Debug.debug('****The parent object is custom'); - parentObjectName = parentObjectName.replace('__r', '__c'); - } - String parentOfParent; - //Get the parent object of the object at the top of the chain. If there is only one object in the chain, the parent - //is the object in the trigger. - if(filterObjectChain.size() > 1) { - parentOfParent = filterObjectChain[filterObjectChain.size() - 2]; - } else { - parentOfParent = describeObj.getName(); - } - UTIL_Debug.debug('****Parent of parent: ' + parentOfParent); - return getObjectTypeReferenced(parentOfParent, parentObjectName); - }*/ /******************************************************************************************************* * @description Get the object type of the filter field. For example, the field might be called Current_Address__c, @@ -197,10 +181,8 @@ public with sharing class TDTM_Filter { String objPointingTo; DescribeSObjectResult objectDescribe; try { - if(parent.endsWith('__r')) - parent = parent.replace('__r', '__c'); - if(child.endsWith('__r')) - child = child.replace('__r', '__c'); + parent = fromRtoC(parent); + child = fromRtoC(child); objectDescribe = UTIL_Describe.getObjectDescribe(parent); if(objectDescribe != null) { Map fieldsMap = objectDescribe.fields.getMap(); @@ -222,10 +204,8 @@ public with sharing class TDTM_Filter { } private SObjectField getSOField(String parentObjectName, String fieldName) { - if(parentObjectName.endsWith('__r')) - parentObjectName = parentObjectName.replace('__r', '__c'); - if(fieldName.endsWith('__r')) - fieldName = fieldName.replace('__r', '__c'); + parentObjectName = fromRtoC(parentObjectName); + fieldName = fromRtoC(fieldName); UTIL_Debug.debug('****Parent in getSOField: ' + parentObjectName); UTIL_Debug.debug('****Child in getSOField: ' + fieldName); try { @@ -242,6 +222,13 @@ public with sharing class TDTM_Filter { } } + private String fromRtoC(String fieldName) { + if(fieldName.endsWith('__r')) + return fieldName.replace('__r', '__c'); + else + return fieldName; + } + /******************************************************************************************************* * @description Queries the fields that are part of the relationship filter, since these values are not initially * present in the records the triggers acts on. @@ -405,4 +392,20 @@ public with sharing class TDTM_Filter { oldList = new List(); } } + + public class ChainLink { + public SObjectField field; + public String fieldName; + public String objectReferenced; + public String parentName; + public Integer positionFromBackOfChain; + + public ChainLink(SObjectField field, String objectReferenced, String parentName, Integer positionFromBackOfChain) { + this.field = field; + this.fieldName = this.field.getDescribe().getName(); //TODO: optimize this to save describe calls. + this.objectReferenced = objectReferenced; + this.parentName = parentName; + this.positionFromBackOfChain = positionFromBackOfChain; + } + } } \ No newline at end of file From a103ffd7351f75c70af4e29f17d261cb7b195495 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Tue, 18 Aug 2015 17:02:13 -0500 Subject: [PATCH 26/31] It all works! Amazing... --- src/classes/TDTM_Filter.cls | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index dbd01c5a25..589f54f13d 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -128,9 +128,21 @@ public with sharing class TDTM_Filter { if(field == null) { //If the field is not valid, go down the object chain until we find one that is valid. Then go back up to find the object type //at each level, until we get to the type of the parent of the filter field. - ChainLink topFirstValidField = findValidObjectInChain(filterObjectChain.size() - 1); - UTIL_Debug.debug('****topFirstValidField: ' + topFirstValidField); + ChainLink firstValidLink = findValidObjectInChain(filterObjectChain.size() - 1); + UTIL_Debug.debug('****firstValidLink: ' + firstValidLink); //Now go back up the chain. What we really want is the object type of the parent of the filter field. + ChainLink link = firstValidLink; + for(Integer i = firstValidLink.objectChainIndex + 1; i < filterObjectChain.size(); i++) { + UTIL_Debug.debug('****Object chain index: ' + i); + UTIL_Debug.debug('****Parent object name: ' + link.objectReferenced); + UTIL_Debug.debug('****filterObjectChain element: ' + filterObjectChain[i]); + field = getSOField(link.objectReferenced, filterObjectChain[i]); + UTIL_Debug.debug('****field back up the chain: ' + field); + String objectReferenced = getObjectTypeReferenced(link.objectReferenced, filterObjectChain[i]); + link = new ChainLink(field, objectReferenced, link.objectReferenced, i); + UTIL_Debug.debug('****Link: ' + Link); + } + field = getSOField(link.objectReferenced, fieldName); } if(field != null) { //the field name is valid for the object at the top of the chain! filterValue = getFilter(field); @@ -182,7 +194,9 @@ public with sharing class TDTM_Filter { DescribeSObjectResult objectDescribe; try { parent = fromRtoC(parent); + UTIL_Debug.debug('****Parent in getObjectTypeReferenced: ' + parent); child = fromRtoC(child); + UTIL_Debug.debug('****Child in getObjectTypeReferenced: ' + child); objectDescribe = UTIL_Describe.getObjectDescribe(parent); if(objectDescribe != null) { Map fieldsMap = objectDescribe.fields.getMap(); @@ -398,14 +412,14 @@ public with sharing class TDTM_Filter { public String fieldName; public String objectReferenced; public String parentName; - public Integer positionFromBackOfChain; + public Integer objectChainIndex; - public ChainLink(SObjectField field, String objectReferenced, String parentName, Integer positionFromBackOfChain) { + public ChainLink(SObjectField field, String objectReferenced, String parentName, Integer objectChainIndex) { this.field = field; - this.fieldName = this.field.getDescribe().getName(); //TODO: optimize this to save describe calls. + this.fieldName = this.field.getDescribe().getName(); //@TODO: optimize this to save describe calls. this.objectReferenced = objectReferenced; this.parentName = parentName; - this.positionFromBackOfChain = positionFromBackOfChain; + this.objectChainIndex = objectChainIndex; } } } \ No newline at end of file From 26d1da5669ab9c2a90d1b35ea67b7ac5010324fd Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 19 Aug 2015 11:00:44 -0500 Subject: [PATCH 27/31] Removing some debug statements. --- src/classes/TDTM_Filter.cls | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 589f54f13d..95b04a0429 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -109,15 +109,15 @@ public with sharing class TDTM_Filter { fieldName = filterFullChain[filterFullChain.size() - 1]; //get the field name itself String parentObjectName = filterFullChain[filterFullChain.size() - 2]; //get the name of the field parent = last object in the chain UTIL_Debug.debug('****filterFullChain: ' + filterFullChain); + //remove the field, to have only the parent object chain for(Integer i = 0; i < (filterFullChain.size() - 1); i++) filterObjectChain.add(filterFullChain[i]); - UTIL_Debug.debug('****Parent objects chain: ' + JSON.serializePretty(filterObjectChain)); - //For the special Parent field on Account case + //For the special Parent field on Account case. The relationship is called "Parent", but the object is Account. if(filterFullChain.size() > 2 && filterFullChain[filterFullChain.size() - 3] == 'Account' && filterFullChain[filterFullChain.size() - 2] == 'Parent') { - UTIL_Debug.debug('****The relationship is called "Parent", but the object is Account'); - parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe below. + parentObjectName = 'Account'; //If we don't do this, we get "Exception: Invalid object name 'Parent'" when calling getObjectDescribe, because the + //element in the map retrieved from the parent is ParentId. } UTIL_Debug.debug('****Bottom child: ' + fieldName); @@ -126,21 +126,17 @@ public with sharing class TDTM_Filter { UTIL_Debug.debug('****Field in object: ' + field); if(field == null) { - //If the field is not valid, go down the object chain until we find one that is valid. Then go back up to find the object type + //If the field is not valid, go up the object chain until we find one that is valid. Then go back down to find the object type //at each level, until we get to the type of the parent of the filter field. ChainLink firstValidLink = findValidObjectInChain(filterObjectChain.size() - 1); - UTIL_Debug.debug('****firstValidLink: ' + firstValidLink); - //Now go back up the chain. What we really want is the object type of the parent of the filter field. + //Now go back down the chain. What we really want is the object type of the parent of the filter field. ChainLink link = firstValidLink; for(Integer i = firstValidLink.objectChainIndex + 1; i < filterObjectChain.size(); i++) { - UTIL_Debug.debug('****Object chain index: ' + i); UTIL_Debug.debug('****Parent object name: ' + link.objectReferenced); - UTIL_Debug.debug('****filterObjectChain element: ' + filterObjectChain[i]); field = getSOField(link.objectReferenced, filterObjectChain[i]); UTIL_Debug.debug('****field back up the chain: ' + field); String objectReferenced = getObjectTypeReferenced(link.objectReferenced, filterObjectChain[i]); link = new ChainLink(field, objectReferenced, link.objectReferenced, i); - UTIL_Debug.debug('****Link: ' + Link); } field = getSOField(link.objectReferenced, fieldName); } @@ -154,12 +150,12 @@ public with sharing class TDTM_Filter { } private ChainLink findValidObjectInChain(Integer objectChainIndex) { - UTIL_Debug.debug('****objectChainIndex: ' + objectChainIndex); SObjectField field; if(objectChainIndex == 0) { UTIL_Debug.debug('****Top child: ' + filterObjectChain[objectChainIndex]); - UTIL_Debug.debug('****Top parent: ' + describeObj.getName()); - field = getSOField(describeObj.getName(), filterObjectChain[objectChainIndex]); + String topParent = describeObj.getName(); + UTIL_Debug.debug('****Top parent: ' + topParent); + field = getSOField(topParent, filterObjectChain[objectChainIndex]); String objectReferenced = getObjectTypeReferenced(describeObj.getName(), filterObjectChain[objectChainIndex]); ChainLink link = new ChainLink(field, objectReferenced, describeObj.getName(), objectChainIndex); return link; @@ -369,7 +365,6 @@ public with sharing class TDTM_Filter { //perform the filtering UTIL_Debug.debug('****Filtering by field ' + fieldName + ', with value ' + filterValue + ' on object ' + o); if(addDirectly || (o != null && o.get(fieldName) != filterValue)) { - UTIL_Debug.debug('****Adding object to filtered list: ' + originalList[i]); filteredList.add(originalList[i]); } } From 271466148985ec552a08e0dbdf1903e1b1011f7b Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 19 Aug 2015 12:07:24 -0500 Subject: [PATCH 28/31] Refactoring. --- src/classes/TDTM_Filter.cls | 51 ++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 95b04a0429..262669ff4c 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -120,35 +120,50 @@ public with sharing class TDTM_Filter { //element in the map retrieved from the parent is ParentId. } + SObjectField field = getSObjectFilterField(parentObjectName); + + if(field != null) { //the field name is valid for the object at the top of the chain! + filterValue = getFilter(field); + UTIL_Debug.debug('****Filter value: ' + filterValue); + filterByCondition(newListRelatedFields, oldListRelatedFields); + } else { + UTIL_Debug.debug('****The field name is invalid.'); + } + } + + /******************************************************************************************************* + * @description We need the SObjectField to know the type of the filter field and determine if any manipulation is necessary. + * All the filter conditions are stored as strings, but some many need to be transformed to compare against the values in the + * trigger records. For example, is a filtering condition is stored as 'true' we'll need to transform it into the Boolean value + * true. + */ + private SObjectField getSObjectFilterField(String parentObjectName) { UTIL_Debug.debug('****Bottom child: ' + fieldName); UTIL_Debug.debug('****Bottom parent: ' + parentObjectName); SObjectField field = getSOField(parentObjectName, fieldName); UTIL_Debug.debug('****Field in object: ' + field); if(field == null) { - //If the field is not valid, go up the object chain until we find one that is valid. Then go back down to find the object type - //at each level, until we get to the type of the parent of the filter field. - ChainLink firstValidLink = findValidObjectInChain(filterObjectChain.size() - 1); - //Now go back down the chain. What we really want is the object type of the parent of the filter field. - ChainLink link = firstValidLink; - for(Integer i = firstValidLink.objectChainIndex + 1; i < filterObjectChain.size(); i++) { - UTIL_Debug.debug('****Parent object name: ' + link.objectReferenced); - field = getSOField(link.objectReferenced, filterObjectChain[i]); - UTIL_Debug.debug('****field back up the chain: ' + field); + //If the field is not valid, go up the object chain until we find one that is valid. Then go back down to find the object type + //at each level, until we get to the type of the parent of the filter field. + ChainLink firstValidLink = findValidObjectInChain(filterObjectChain.size() - 1); + //Now go back down the chain. What we really want is the object type of the parent of the filter field. + ChainLink link = firstValidLink; + for(Integer i = firstValidLink.objectChainIndex + 1; i < filterObjectChain.size(); i++) { + UTIL_Debug.debug('****Parent object name: ' + link.objectReferenced); + field = getSOField(link.objectReferenced, filterObjectChain[i]); + UTIL_Debug.debug('****field back up the chain: ' + field); String objectReferenced = getObjectTypeReferenced(link.objectReferenced, filterObjectChain[i]); link = new ChainLink(field, objectReferenced, link.objectReferenced, i); - } - field = getSOField(link.objectReferenced, fieldName); + } + field = getSOField(link.objectReferenced, fieldName); } - if(field != null) { //the field name is valid for the object at the top of the chain! - filterValue = getFilter(field); - UTIL_Debug.debug('****Filter value: ' + filterValue); - filterByCondition(newListRelatedFields, oldListRelatedFields); - } else { - UTIL_Debug.debug('****The field name is invalid.'); - } + return field; } + /******************************************************************************************************* + * @description Finds the first valid object in the chain + */ private ChainLink findValidObjectInChain(Integer objectChainIndex) { SObjectField field; if(objectChainIndex == 0) { From 7e344858f2e59d54fbad809630770104c7de7482 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 19 Aug 2015 15:03:18 -0500 Subject: [PATCH 29/31] Adding ApexDocs. --- src/classes/TDTM_Filter.cls | 38 ++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/classes/TDTM_Filter.cls b/src/classes/TDTM_Filter.cls index 262669ff4c..d1ebbcdc04 100644 --- a/src/classes/TDTM_Filter.cls +++ b/src/classes/TDTM_Filter.cls @@ -132,10 +132,13 @@ public with sharing class TDTM_Filter { } /******************************************************************************************************* - * @description We need the SObjectField to know the type of the filter field and determine if any manipulation is necessary. - * All the filter conditions are stored as strings, but some many need to be transformed to compare against the values in the - * trigger records. For example, is a filtering condition is stored as 'true' we'll need to transform it into the Boolean value - * true. + * @description We need the SObjectField to know the type of the filter field and determine if any manipulation + * is necessary. All the filter conditions are stored as strings, but some many need to be transformed to + * compare against the values in the trigger records. For example, is a filtering condition is stored as 'true' + * we'll need to transform it into the Boolean value true. + * @param parentObjectName The name of the object that is the parent of the filter field, as initally defined in the filtering + * condition. + * @return SObjectField The field to filter on as SObjectField. */ private SObjectField getSObjectFilterField(String parentObjectName) { UTIL_Debug.debug('****Bottom child: ' + fieldName); @@ -162,7 +165,10 @@ public with sharing class TDTM_Filter { } /******************************************************************************************************* - * @description Finds the first valid object in the chain + * @description Finds the first valid object in the chain, that is the first item in the chain built from the filtering + * condition where the name is an actual object name. If none of the items in the chain are an actual object, it will + * go all the way back up to the object the class is running on. + * @objectChainIndex The index of the element in the chain to inspect. */ private ChainLink findValidObjectInChain(Integer objectChainIndex) { SObjectField field; @@ -198,7 +204,10 @@ public with sharing class TDTM_Filter { /******************************************************************************************************* * @description Get the object type of the filter field. For example, the field might be called Current_Address__c, - but the object is Address__c. + * but the object is Address__c. + * @param parent The parent object of the field. + * @param child The field for which we are trying to find the object type referenced. + * @return String The name of the object referenced by child, if any. */ private String getObjectTypeReferenced(String parent, String child) { String objPointingTo; @@ -228,6 +237,12 @@ public with sharing class TDTM_Filter { return objPointingTo; } + /******************************************************************************************************* + * @description Gets the SObjectField for the fieldName field name in parentObjectName. + * @param parentObjectName The name of the object containing the field, as a string. + * @param fieldName The name of the field we are looking for, as a string. + * @return SObjectField The field we are looking for. + */ private SObjectField getSOField(String parentObjectName, String fieldName) { parentObjectName = fromRtoC(parentObjectName); fieldName = fromRtoC(fieldName); @@ -247,6 +262,12 @@ public with sharing class TDTM_Filter { } } + /******************************************************************************************************* + * @description Replaces __r with __c in the string passed, in case that's the only difference between the name + * used to query and the actual object named referenced. + * @param The relationship name. + * @return String The object name, if the name of a custom relationship was passed. + */ private String fromRtoC(String fieldName) { if(fieldName.endsWith('__r')) return fieldName.replace('__r', '__c'); @@ -417,6 +438,9 @@ public with sharing class TDTM_Filter { } } + /******************************************************************************************************* + * @description Wrapper representing a link in the chain obtained from the filtering condition. + */ public class ChainLink { public SObjectField field; public String fieldName; @@ -426,7 +450,7 @@ public with sharing class TDTM_Filter { public ChainLink(SObjectField field, String objectReferenced, String parentName, Integer objectChainIndex) { this.field = field; - this.fieldName = this.field.getDescribe().getName(); //@TODO: optimize this to save describe calls. + this.fieldName = this.field.getDescribe().getName(); //@TODO: optimize this to save describe calls? this.objectReferenced = objectReferenced; this.parentName = parentName; this.objectChainIndex = objectChainIndex; From cbe67c8e75e65d1eb8bee597cb40f03b7407cbc2 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 19 Aug 2015 15:10:29 -0500 Subject: [PATCH 30/31] Not necessary any more now that we can traverse relationships! --- src/classes/TDTM_Filter_TEST.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 57a19e3d91..32acab8cf0 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -250,7 +250,7 @@ public with sharing class TDTM_Filter_TEST { //Creating filter condition. Trigger_Handler__c handler = [select Filter_Field__c, Filter_Value__c from Trigger_Handler__c where Class__c = 'REL_Relationships_Con_TDTM']; - handler.Filter_Field__c = 'AccountId'; //We have to append Id because that's what's in the map of fields! + handler.Filter_Field__c = 'Account.Id'; handler.Filter_Value__c = acc2.Id; update handler; From 5258976dde5fdaf81c71c460fcd6707b324fecf9 Mon Sep 17 00:00:00 2001 From: Carlos Eiroa Date: Wed, 19 Aug 2015 17:41:01 -0500 Subject: [PATCH 31/31] Filtering also for only one record. (If the record is filtered out, the list will be empty.) --- src/classes/TDTM_Filter_TEST.cls | 28 ++++++++++++++++++++++++++++ src/classes/TDTM_TriggerHandler.cls | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/classes/TDTM_Filter_TEST.cls b/src/classes/TDTM_Filter_TEST.cls index 32acab8cf0..68dddb9773 100644 --- a/src/classes/TDTM_Filter_TEST.cls +++ b/src/classes/TDTM_Filter_TEST.cls @@ -105,6 +105,34 @@ public with sharing class TDTM_Filter_TEST { System.assertEquals(2, rels.size()); } + public static testmethod void oneRecordOnly() { + if (strTestOnly != '*' && strTestOnly != 'oneRecordOnly') return; + + insert new Relationship_Auto_Create__c(Name='AutoCreate2',Object__c='Contact', + Field__c='ReportsToId', Relationship_Type__c = 'TestType'); + + //Creating filter condition. + insert new Trigger_Handler__c(Active__c = true, Asynchronous__c = false, + Class__c = 'REL_Relationships_Con_TDTM', Load_Order__c = 1, Object__c = 'Contact', + Trigger_Action__c = 'AfterInsert;AfterUpdate;AfterDelete', Filter_Field__c = 'AssistantName', + Filter_Value__c = 'Anne'); + + //Creating two contacts. The second one meets the filtering criteria. + Contact c1 = new Contact(FirstName = 'Test', LastName = 'Testerson1', AssistantName = 'Anne'); + Contact c2 = new Contact(FirstName = 'Test', LastName = 'Testerson2', AssistantName = 'Nancy'); + Contact[] contacts = new Contact[] {c1, c2}; + insert contacts; + + //Adding lookups among the contacts. Relationships should be automatically created from them. + //Using the 'ReportsTo' field because it's a standard lookup field from Contact to Contact. + c1.ReportsToId = c2.Id; + update c1; + + //Since c1 should be filtered out, not relationship should be created. + Relationship__c[] rels = [select Contact__c, RelatedContact__c from Relationship__c]; + System.assertEquals(0, rels.size()); + } + public static testmethod void checkboxField() { if (strTestOnly != '*' && strTestOnly != 'checkboxField') return; diff --git a/src/classes/TDTM_TriggerHandler.cls b/src/classes/TDTM_TriggerHandler.cls index 9abf81b25d..15e65fb57a 100644 --- a/src/classes/TDTM_TriggerHandler.cls +++ b/src/classes/TDTM_TriggerHandler.cls @@ -180,7 +180,7 @@ public with sharing class TDTM_TriggerHandler { TDTM_Filter filter = new TDTM_Filter(classToRunRecord, newList, oldList, describeObj); TDTM_Filter.FilteredLists filtered = filter.filter(); - if(filtered != null && filtered.newList.size() > 0) { + if(filtered != null) { UTIL_Debug.debug('****newList after filtering: ' + JSON.serializePretty(filtered.newList)); return classToRun.run(filtered.newList, filtered.oldList, thisAction, describeObj); } else {