Saturday, October 12, 2013

Deeper Clone: Test Class Example

For those of you experimenting with the UtilsDeeperClone tool, the next question is always "ok, now how do I write a proper test class?"  All code needs to be covered to at least 75%, but that is the bare minimum.  We will strive for 100% coverage here.  I also like to separate the sObject creation test methods into their own class for brevity and re-use, so you'll need to create a class named UtilsTest.

My primary use case involves a tree as follows, and is what the following code is based on testing (Schedule Lines are created via trigger, so no need to manually address those in my ChildRelationships Custom Setting).  You'll need to adapt to your own use cases:



1. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

@isTest(SeeAllData=true)
public without sharing class UtilsTest {
    
    public static List<Account> createFieldSalesAccounts(Integer count, Id parentId) {
        List<Account> listOf = new List<Account>();
        for (Integer i = 0; i < count; i++) {
            Account toInsert = new Account();
                toInsert.RecordTypeId = UtilsVariables.RECORDTYPEID_ACCOUNT_FIELD_SALES;
                toInsert.Name = 'Test Field Sales Account ' + i;
                if (parentId != null) toInsert.ParentId = parentId;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<Opportunity> createBoards(Integer count, Id accountId) {
        List<Opportunity> listOf = new List<Opportunity>();
        List<End_Equipment__c> endequipments = createEndEquipments(1);
        if (accountId == null) accountId = createFieldSalesAccounts(1, null)[0].Id;
        for (Integer i = 0; i < count; i++) {
            Opportunity toInsert = new Opportunity();
                toInsert.RecordTypeId = UtilsVariables.RECORDTYPEID_OPPORTUNITY_BOARD;
                toInsert.Name = 'Test Board ' + i;
                toInsert.StageName = 'Architecture';
                toInsert.CloseDate = Date.Today();
                toInsert.Board_Design_Years__c = 1;
                toInsert.Board_Design_Months__c = 3;
                toInsert.Board_End_Equipment__c = endequipments[0].Id;
                toInsert.AccountId = accountId;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<Opportunity> createSockets(Integer count, Id boardId) {
        List<Opportunity> listOf = new List<Opportunity>();
        List<Socket_Function__c> socketfunctions = createSocketFunctions(1);
        if (boardId == null) boardId = createBoards(1, null)[0].Id;
        Id accountId = [SELECT AccountId FROM Opportunity WHERE Id = :boardId].AccountId;
        for (Integer i = 0; i < count; i++) {
            Opportunity toInsert = new Opportunity();
                toInsert.RecordTypeId = UtilsVariables.RECORDTYPEID_OPPORTUNITY_SOCKET;
                toInsert.Name = 'Test Socket ' + i;
                toInsert.StageName = 'Potential';
                toInsert.CloseDate = Date.Today();
                toInsert.AccountId = accountId;
                toInsert.Board_Name__c = boardId;
                toInsert.Socket_Function__c = socketfunctions[0].Id;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<Product2> createProducts(Integer count) {
        List<Product2> listOf = new List<Product2>();
        for (Integer i = 0; i < count; i++) {
            Product2 toInsert = new Product2();
                toInsert.Name = 'Test Product ' + i;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<PricebookEntry> createPriceBookEntries(Integer count) {
        List<PricebookEntry> listOf = new List<PricebookEntry>();
        List<Product2> products = createProducts(count);
        List<Pricebook2> pricebooks = [SELECT Id FROM Pricebook2 WHERE IsStandard = true];
        for (Product2 product : products) {
            PricebookEntry toInsert = new PricebookEntry();
                toInsert.Product2Id = product.Id;
                toInsert.UnitPrice = 0;
                toInsert.Pricebook2Id = pricebooks[0].Id;
                toInsert.isActive = true;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<OpportunityLineItem> createOpportunityLineItems(List<Opportunity> opps,
            List<PricebookEntry> pricebookentries) {
        List<OpportunityLineItem> listOf = new List<OpportunityLineItem>();
        for (Opportunity opp : opps) {
            Boolean isFirst = true;
            for (PricebookEntry pricebookentry : pricebookentries) {
                OpportunityLineItem toInsert = new OpportunityLineItem();
                    toInsert.PricebookEntryId = pricebookentry.Id;
                    toInsert.OpportunityId = opp.Id;
                    toInsert.Quantity = 1;
                    if (opp.RecordTypeId == UtilsVariables.RECORDTYPEID_OPPORTUNITY_SOCKET)
                        toInsert.UnitPrice = 1.5;
                    if (opp.RecordTypeId == UtilsVariables.RECORDTYPEID_OPPORTUNITY_SALES_PLAN_PRODUCT)
                        toInsert.UnitPrice = 0;
                    if (opp.RecordTypeId == UtilsVariables.RECORDTYPEID_OPPORTUNITY_SALES_PLAN_PRODUCT)
                        toInsert.Annual_Value__c = 12;
                    if (opp.RecordTypeId == UtilsVariables.RECORDTYPEID_OPPORTUNITY_SOCKET && isFirst)
                        toInsert.Primary__c = true;
                listOf.add(toInsert);
                isFirst = false;
            }
        }
        insert listOf;
        return listOf;
    }
    
    public static List<Socket_Function__c> createSocketFunctions(Integer count) {
        List<Socket_Function__c> listOf = new List<Socket_Function__c>();
        for (Integer i = 0; i < count; i++) {
            Socket_Function__c toInsert = new Socket_Function__c();
                toInsert.Name = 'Test Function ' + i;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<End_Equipment__c> createEndEquipments(Integer count) {
        List<End_Equipment__c> listOf = new List<End_Equipment__c>();
        for (Integer i = 0; i < count; i++) {
            End_Equipment__c toInsert = new End_Equipment__c();
                toInsert.Name = 'Test End Equipment ' + i;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
    public static List<Calendar_Period__c> getPeriods(Integer count, String type) {
        List<Calendar_Period__c> listOf = [SELECT Id, Name, Start_Date__c, End_Date__c
            FROM Calendar_Period__c
            WHERE RecordTypeId = :type
                AND Start_Date__c >= :Date.today()
                AND Start_Date__c <= :Date.today().addMonths(count * 12)];
        return listOf;
    }
    
    public static List<Schedule__c> createSchedules(Integer count, Id boardId) {
        List<Schedule__c> listOf = new List<Schedule__c>();
        List<Calendar_Period__c> periods = getPeriods(count, UtilsVariables.RECORDTYPEID_CALENDAR_PERIOD_FISCAL_YEAR);
        for (Calendar_Period__c period : periods) {
            Schedule__c toInsert = new Schedule__c();
                toInsert.Fiscal_Year__c = period.Id;
                toInsert.Volume__c = 10;
                if (boardId != null) toInsert.Opportunity_Name__c = boardId;
            listOf.add(toInsert);
        }
        insert listOf;
        return listOf;
    }
    
}

12. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

@isTest(SeeAllData=true)
private class UtilsDeeperClone_test {

    private static Integer expectedProducts = 3;
    private static Integer expectedBoards = 1;
    private static Integer expectedSockets = 1;
    private static Integer expectedOpportunityLineItems = expectedSockets * expectedProducts;
    private static Integer expectedSchedules = 4;
    private static Integer expectedScheduleLines = expectedSockets * expectedSchedules;
    
    static testMethod void testCloneWithoutChatterBoard() {
        Boolean withChatter = false;
        List<PricebookEntry> pricebookentries = UtilsTest.createPricebookEntries(expectedProducts);
        List<Opportunity> boards = UtilsTest.createBoards(expectedBoards, null);
        List<Opportunity> sockets = UtilsTest.createSockets(expectedSockets, boards[0].Id);
        List<OpportunityLineItem> opportunitylineitems = UtilsTest.createOpportunityLineItems(sockets, pricebookentries);
        List<Schedule__c> schedules = UtilsTest.createSchedules(expectedSchedules, boards[0].Id);
        System.assertEquals(expectedBoards, boards.size(),
            'expected ' + expectedBoards + ' boards');
        System.assertEquals(expectedSockets, sockets.size(),
            'expected ' + expectedSockets + ' sockets');
        System.assertEquals(expectedSchedules, schedules.size(),
            'expected ' + expectedSchedules + ' schedules');
        List<Schedule_Line__c> schedulelines = [SELECT Id FROM Schedule_Line__c WHERE Schedule__c IN :schedules];
        System.assertEquals(expectedScheduleLines, schedulelines.size(),
            'expected ' + expectedScheduleLines + ' schedule lines');
        Test.startTest();
        Id clonedId = UtilsDeeperClone.clone(boards[0].Id, withChatter);
        Test.stopTest();
        List<FeedItem> chatterFeeds = [SELECT Id FROM FeedItem WHERE ParentId = :clonedId];
        List<Opportunity> clonedBoards = [SELECT Id, RecordType.Name FROM Opportunity WHERE Id = :clonedId];
        List<Opportunity> clonedSockets = [SELECT Id FROM Opportunity WHERE Board_Name__c = :clonedId];
        List<OpportunityLineItem> clonedParts = [SELECT Id FROM OpportunityLineItem WHERE OpportunityId IN :clonedSockets];
        List<Schedule__c> clonedSchedules = [SELECT Id FROM Schedule__c WHERE Opportunity_Name__c = :clonedId];
        List<Schedule_Line__c> clonedScheduleLines = [SELECT Id FROM Schedule_Line__c WHERE Schedule__c IN :schedules];
        System.assertEquals(withChatter, !chatterFeeds.isEmpty(),
            'expected chatter? ' + withChatter + ' ' + chatterFeeds.size());
        System.assertEquals(expectedBoards, clonedBoards.size(),
            'expected ' + expectedBoards + ' cloned boards');
        System.assertEquals(expectedSockets, clonedSockets.size(),
            'expected ' + expectedSockets + ' cloned sockets');
        System.assertEquals(expectedOpportunityLineItems, clonedParts.size(),
            'expected ' + expectedOpportunityLineItems + ' cloned parts');
        System.assertEquals(expectedSchedules, clonedSchedules.size(),
            'expected ' + expectedSchedules + ' cloned schedules');
        System.assertEquals(expectedScheduleLines, clonedScheduleLines.size(),
            'expected ' + expectedScheduleLines + ' cloned schedule lines');
    }
    
    static testMethod void testCloneWithChatterBoard() {
        Boolean withChatter = true;
        List<PricebookEntry> pricebookentries = UtilsTest.createPricebookEntries(expectedProducts);
        List<Opportunity> boards = UtilsTest.createBoards(expectedBoards, null);
        List<Opportunity> sockets = UtilsTest.createSockets(expectedSockets, boards[0].Id);
        List<OpportunityLineItem> opportunitylineitems = UtilsTest.createOpportunityLineItems(sockets, pricebookentries);
        List<Schedule__c> schedules = UtilsTest.createSchedules(expectedSchedules, boards[0].Id);
        System.assertEquals(expectedBoards, boards.size(),
            'expected ' + expectedBoards + ' boards');
        System.assertEquals(expectedSockets, sockets.size(),
            'expected ' + expectedSockets + ' sockets');
        System.assertEquals(expectedSchedules, schedules.size(),
            'expected ' + expectedSchedules + ' schedules');
        List<Schedule_Line__c> schedulelines = [SELECT Id FROM Schedule_Line__c WHERE Schedule__c IN :schedules];
        System.assertEquals(expectedScheduleLines, schedulelines.size(),
            'expected ' + expectedScheduleLines + ' schedule lines');
        Test.startTest();
        Id clonedId = UtilsDeeperClone.clone(boards[0].Id, withChatter);
        Test.stopTest();
        List<FeedItem> chatterFeeds = [SELECT Id FROM FeedItem WHERE ParentId = :clonedId];
        List<Opportunity> clonedBoards = [SELECT Id, RecordType.Name FROM Opportunity WHERE Id = :clonedId];
        List<Opportunity> clonedSockets = [SELECT Id FROM Opportunity WHERE Board_Name__c = :clonedId];
        List<OpportunityLineItem> clonedParts = [SELECT Id FROM OpportunityLineItem WHERE OpportunityId IN :clonedSockets];
        List<Schedule__c> clonedSchedules = [SELECT Id FROM Schedule__c WHERE Opportunity_Name__c = :clonedId];
        List<Schedule_Line__c> clonedScheduleLines = [SELECT Id FROM Schedule_Line__c WHERE Schedule__c IN :schedules];
        System.assertEquals(withChatter, !chatterFeeds.isEmpty(),
            'expected chatter? ' + withChatter + ' ' + chatterFeeds.size());
        System.assertEquals(expectedBoards, clonedBoards.size(),
            'expected ' + expectedBoards + ' cloned boards');
        System.assertEquals(expectedSockets, clonedSockets.size(),
            'expected ' + expectedSockets + ' cloned sockets');
        System.assertEquals(expectedOpportunityLineItems, clonedParts.size(),
            'expected ' + expectedOpportunityLineItems + ' cloned parts');
        System.assertEquals(expectedSchedules, clonedSchedules.size(),
            'expected ' + expectedSchedules + ' cloned schedules');
        System.assertEquals(expectedScheduleLines, clonedScheduleLines.size(),
            'expected ' + expectedScheduleLines + ' cloned schedule lines');
    }
    
    static testMethod void testCloneWithoutChatterSocket() {
        Boolean withChatter = false;
        List<PricebookEntry> pricebookentries = UtilsTest.createPricebookEntries(expectedProducts);
        List<Opportunity> sockets = UtilsTest.createSockets(expectedSockets, null);
        List<OpportunityLineItem> opportunitylineitems = UtilsTest.createOpportunityLineItems(sockets, pricebookentries);
        System.assertEquals(expectedSockets, sockets.size(),
            'expected ' + expectedSockets + ' sockets');
        Test.startTest();
        Id clonedId = UtilsDeeperClone.clone(sockets[0].Id, withChatter);
        Test.stopTest();
        List<FeedItem> chatterFeeds = [SELECT Id FROM FeedItem WHERE ParentId = :clonedId];
        List<Opportunity> clonedSockets = [SELECT Id FROM Opportunity WHERE Id = :clonedId];
        List<OpportunityLineItem> clonedParts = [SELECT Id FROM OpportunityLineItem WHERE OpportunityId = :clonedId];
        System.assertEquals(withChatter, !chatterFeeds.isEmpty(),
            'expected chatter? ' + withChatter + ' ' + chatterFeeds.size());
        System.assertEquals(expectedSockets, clonedSockets.size(),
            'expected ' + expectedSockets + ' cloned sockets');
        System.assertEquals(expectedOpportunityLineItems, clonedParts.size(),
            'expected ' + expectedOpportunityLineItems + ' cloned parts');
    }
    
    static testMethod void testCloneWithChatterSocket() {
        Boolean withChatter = true;
        List<PricebookEntry> pricebookentries = UtilsTest.createPricebookEntries(expectedProducts);
        List<Opportunity> sockets = UtilsTest.createSockets(expectedSockets, null);
        List<OpportunityLineItem> opportunitylineitems = UtilsTest.createOpportunityLineItems(sockets, pricebookentries);
        System.assertEquals(expectedSockets, sockets.size(),
            'expected ' + expectedSockets + ' sockets');
        Test.startTest();
        Id clonedId = UtilsDeeperClone.clone(sockets[0].Id, withChatter);
        Test.stopTest();
        List<FeedItem> chatterFeeds = [SELECT Id FROM FeedItem WHERE ParentId = :clonedId];
        List<Opportunity> clonedSockets = [SELECT Id FROM Opportunity WHERE Id = :clonedId];
        List<OpportunityLineItem> clonedParts = [SELECT Id FROM OpportunityLineItem WHERE OpportunityId = :clonedId];
        System.assertEquals(withChatter, !chatterFeeds.isEmpty(),
            'expected chatter? ' + withChatter + ' ' + chatterFeeds.size());
        System.assertEquals(expectedSockets, clonedSockets.size(),
            'expected ' + expectedSockets + ' cloned sockets');
        System.assertEquals(expectedOpportunityLineItems, clonedParts.size(),
            'expected ' + expectedOpportunityLineItems + ' cloned parts');
    }
    
}

A few notes:
  • You will need to modify the creatObject() methods in the UtilsTest class to abide by your org's requirements (required fields, values, etc)
  • This test code covers 98% in my org
Happy Coding!

Thursday, September 19, 2013

Deeper Clone: Clone Salesforce objects with all levels of children

During development, I ran into a requirement to be able to take any record and clone not only it, but all its children, grand-children, great-grand-children, etc.  As you may already know, Salesforce has already provided a generic "clone" method which literally copies the specified sObjects and casts that new record to a new sObject awaiting insertion.  This method also provides the capability to maintain relationships opt_isDeepClone.  However, there are two major limitations of this method.  First, the generic clone method only pays attention to the fields stored in-memory for the original sObject.  Second, the generic clone method does not pay any attention to child relationships and records, which limits the real-world applicability.

I will address both challenges here.


In order to circumvent the first "in-memory" issue and also prevent code modification with each field addition, we'll generate a dynamic list of fields for the sObjects in question, then run and store a SoQL query with the resulting fields.  Another trick I've included is the ability to exclude specific fields for a given sObject.  This may be important if there is a specific field that should be reset upon cloning (for instance, maybe you want to reset the "Stage" field when you clone an existing, completed Opportunity).

To address the children, grand-children, great-grand-children, etc I've employed two inner classes, which will serve as a storage medium while we iterate through and identify all levels of parents and children we wish to clone.  These classes will also help house the relationship information for later use, such as the name of the child object and the name of the field which is used to tie it to the parent (an example here would be the OpportunityId field for the OpportunityLineItem table).

Without further ado, let's get to it!

1. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ApexMessage with one field (ApexMessage):


2. Once created, click "Manage" for the ApexMessage Custom Setting and then click "New" to enter a new message:

**Feel free to re-word the message to your liking, but please note the Name must be exact.

3. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ChildRelationship with two fields (ChildRelationship, sObjectType):


4. Once created, click "Manage" for the ChildRelationship Custom Setting and then click "New" to enter a new relationship:

**The ChildRelationship field contains the actual child relationship name from the lookup

5. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldExclusion with two fields (FieldExclusion, sObjectType):


6. Once created, click "Manage" for the FieldExclusion Custom Setting and then click "New" to enter a new field exclusion:

**The FieldExclusion field contains the actual API name for the fields you wish to exclude

7. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldDefault with three fields (FieldDefault, FieldValue, sObjectType):


8. Once created, click "Manage" for the FieldDefault Custom Setting and then click "New" to enter a new field default:

**The FieldDefault field contains the actual API name for the fields you wish to exclude

9. Create a new Button (Setup > Customize > Opportunities > Buttons, Links, and Actions > New Button or Link) and create a button:

**You may choose to create this button on any other table, that's the beauty of the tool!

10. Add the Button from step nine to your page layout and enjoy!

11. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global class UtilsCustomSetting {
    
    private static final Map<String, String> mapApexMessages = mapApexMessages();
    private static final Map<String, Set<String>> mapChildRelationships = mapChildRelationships();
    private static final Map<String, Set<String>> mapFieldExclusions = mapFieldExclusions();
    private static final Map<String, Map<String, String>> mapFieldDefaults = mapFieldDefaults();
    
    global static Map<String, String> mapApexMessages() {
        List<ApexMessage__c> msgs = ApexMessage__c.getAll().values();
        Map<String, String> options = new Map<String, String>();
        for (ApexMessage__c msg : msgs) {
            options.put(msg.Name, msg.ApexMessage__c);
        }
        return options;
    }
    
    global static Map<String, Set<String>> mapChildRelationships() {
        List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (ChildRelationship__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c);
        }
        return mapOf;
    }
    
    global static Map<String, Set<String>> mapFieldExclusions() {
        List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (FieldExclusion__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase());
        }
        return mapOf;
    }
    
    global static Map<String, Map<String, String>> mapFieldDefaults() {
        List<FieldDefault__c> defs = FieldDefault__c.getAll().values();
        Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>();
        for (FieldDefault__c def : defs) {
            if (!mapOf.containsKey(def.sObjectType__c)) {
                mapOf.put(def.sObjectType__c, new Map<String, String>());
            }
            mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c);
        }
        return mapOf;
    }
    
    //** Returns a message for use in APEX error handling, chatter posting, etc
    global static String getApexMessage(String msgName) {
        return mapApexMessages.get(msgName);
    }
    
    //** Returns a set of child relationships to include in the cloning query
    global static Set<String> getChildRelationships(String relName) {
        Set<String> setOf = new Set<String>();
        if (mapChildRelationships.containsKey(relName)) {
            setOf.addAll(mapChildRelationships.get(relName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldExclusions(String esclName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldExclusions.containsKey(esclName)) {
            setOf.addAll(mapFieldExclusions.get(esclName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldDefaults(String defName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldDefaults.containsKey(defName)) {
            setOf.addAll(mapFieldDefaults.get(defName).keySet());
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static String getFieldDefault(String defName, String defValue) {
        if (mapFieldDefaults.containsKey(defName)) {
            if (mapFieldDefaults.get(defName).containsKey(defValue)) {
                return mapFieldDefaults.get(defName).get(defValue);
            }
        }
        return null;
    }
    
}

12. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global without sharing class UtilsVariables {
    
    global static Map<String, String> mapApexMessages() {
        List<ApexMessage__c> msgs = ApexMessage__c.getAll().values();
        Map<String, String> options = new Map<String, String>();
        for (ApexMessage__c msg : msgs) {
            options.put(msg.Name, msg.ApexMessage__c);
        }
        return options;
    }
    
    global static Map<String, Set<String>> mapChildRelationships() {
        List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (ChildRelationship__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c);
        }
        return mapOf;
    }
    
    global static Map<String, Set<String>> mapFieldExclusions() {
        List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values();
        Map<String, Set<String>> mapOf = new Map<String, Set<String>>();
        for (FieldExclusion__c excl : excls) {
            if (!mapOf.containsKey(excl.sObjectType__c)) {
                mapOf.put(excl.sObjectType__c, new Set<String>());
            }
            mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase());
        }
        return mapOf;
    }
    
    global static Map<String, Map<String, String>> mapFieldDefaults() {
        List<FieldDefault__c> defs = FieldDefault__c.getAll().values();
        Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>();
        for (FieldDefault__c def : defs) {
            if (!mapOf.containsKey(def.sObjectType__c)) {
                mapOf.put(def.sObjectType__c, new Map<String, String>());
            }
            mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c);
        }
        return mapOf;
    }
    
    //** Maps, held privately so as to only pull once per call
    private static final Map<String, String> mapApexMessages = UtilsCustomSetting.mapApexMessages();
    private static final Map<String, Set<String>> mapChildRelationships = UtilsCustomSetting.mapChildRelationships();
    private static final Map<String, Set<String>> mapFieldExclusions = UtilsCustomSetting.mapFieldExclusions();
    private static final Map<String, Map<String, String>> mapFieldDefaults = UtilsCustomSetting.mapFieldDefaults();
    
    //** Returns a message for use in APEX error handling, chatter posting, etc
    global static String getApexMessage(String msgName) {
        return mapApexMessages.get(msgName);
    }
    
    //** Returns a set of child relationships to include in the cloning query
    global static Set<String> getChildRelationships(String relName) {
        Set<String> setOf = new Set<String>();
        if (mapChildRelationships.containsKey(relName)) {
            setOf.addAll(mapChildRelationships.get(relName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldExclusions(String esclName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldExclusions.containsKey(esclName)) {
            setOf.addAll(mapFieldExclusions.get(esclName));
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static Set<String> getFieldDefaults(String defName) {
        Set<String> setOf = new Set<String>();
        if (mapFieldDefaults.containsKey(defName)) {
            setOf.addAll(mapFieldDefaults.get(defName).keySet());
        }
        return setOf;
    }
    
    //** Returns a set of fields to exclude from the cloning query
    global static String getFieldDefault(String defName, String defValue) {
        if (mapFieldDefaults.containsKey(defName)) {
            if (mapFieldDefaults.get(defName).containsKey(defValue)) {
                return mapFieldDefaults.get(defName).get(defValue);
            }
        }
        return null;
    }
    
}

13. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global class UtilsGeneral {
    
    global static sObject newBlankSObject(sObject so) {
        return so.getSObjectType().newSObject();
    }
    
}

14. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global with sharing class UtilsDeeperClone {
    
    //** Deeply clones a list of original records, and all related/requested children
    //** Original list must include the Id field
    global static List<Id> clone(List<sObject> originalObjects, Boolean chatterPost) {
        //** Method variables
        Map<Id, Id> mapIdOldNew = new Map<Id, Id>();
        Map<String, List<Id>> mapObjectIds = new Map<String, List<Id>>();
        Map<String, String> mapRelationshipFields = new Map<String, String>();
        Map<String, Map<Id, sObject>> mapObjectQueries = new Map<String, Map<Id, sObject>>();
        Map<Integer, Map<String, List<sObject>>> mapObject = new Map<Integer, Map<String, List<sObject>>>();
        List<sObjectChildRelationship> myChildRelationships = new List<sObjectChildRelationship>();
        //** Add Level 0 (originalObjects)
        Map<String, List<sObject>> mapObjectCurrent = new Map<String, List<sObject>>();
        List<sObjectChildRelationshipRow> myChildRelationshipRows = new List<sObjectChildRelationshipRow>();
        //** Process input sObjects by sObjectType
        Set<String> origObjectTypeNames = new Set<String>();
        for (sObject originalObject : originalObjects) {
            if (!origObjectTypeNames.contains(originalObject.getSObjectType().getDescribe().getName())) {
                origObjectTypeNames.add(originalObject.getSObjectType().getDescribe().getName());
            }
        }
        for (String origObjectTypeName : origObjectTypeNames) {
            for (sObject originalObject : originalObjects) {
                if (origObjectTypeName == originalObject.getSObjectType().getDescribe().getName()) {
                    //** Add sObject to myChildRelationshipRows
                    myChildRelationshipRows.add(new sObjectChildRelationshipRow(originalObject));
                    //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent
                    if (mapObjectCurrent.containsKey(origObjectTypeName)) {
                        mapObjectCurrent.get(origObjectTypeName).add(originalObject);
                    } else {
                        mapObjectCurrent.put(origObjectTypeName, new List<sObject>{originalObject});
                    }
                }
            }
            //** Add myChildRelationshipRows to a myChildRelationships record with relationship information
            if (!myChildRelationshipRows.isEmpty()) {
                myChildRelationships.add(new sObjectChildRelationship(0, origObjectTypeName, null, myChildRelationshipRows));
            }
        }
        //** Populate Map<Integer, Map<String, List<Id>>> mapObject
        mapObject.put(0, mapObjectCurrent);
        //** Loop through levels 1-n (children)
        for (Integer currentLevel = 1 ; currentLevel < 20 ; currentLevel++) {
            mapObjectCurrent = new Map<String, List<sObject>>();
            if (mapObject.size() == currentLevel) {
                //** Loop through all tables
                for (String objType : mapObject.get(currentLevel-1).keySet()) {
                    List<sObject> sObjectOriginals = mapObject.get(currentLevel-1).get(objType);
                    //** Get complete list of all child relationships for the given table 'objType'
                    List<Schema.ChildRelationship> childRelationships =
                        sObjectOriginals.get(0).getSObjectType().getDescribe().getChildRelationships();
                    //** Exit loop once there are no more childRelationships
                    if (!childRelationships.isEmpty()) {
                        //** Loop through all child relationships
                        for (Schema.ChildRelationship childRelationship : childRelationships) {
                            //** Only run script when the child relationships is acceptable (Custom Setting)
                            if (UtilsVariables.getChildRelationships(objType).contains(
                                    childRelationship.getRelationshipName())) {
                                myChildRelationshipRows = new List<sObjectChildRelationshipRow>();
                                //** Name of the Child Table
                                String childObjectTypeName = childRelationship.getChildSObject().getDescribe().getName();
                                //** Name of the Child Field which stores the Parent sObject Id
                                String childObjectFieldName = childRelationship.getField().getDescribe().getName();
                                //** Build query to return all children in this relationship
                                String childRelationshipQuery = 'SELECT ID';
                                childRelationshipQuery += ' FROM ' + childObjectTypeName;
                                childRelationshipQuery += ' WHERE ' + childObjectFieldName;
                                childRelationshipQuery += ' IN (\'' + sObjectOriginals.get(0).Id + '\'';
                                for (Integer i = 1 ; i < sObjectOriginals.size() ; i++) {
                                    childRelationshipQuery += ', \'' + sObjectOriginals.get(i).Id + '\'';
                                }
                                childRelationshipQuery += ')';
                                //** Executive query to return all children in this relationship
                                List<sObject> childRelationshipQueryResults = Database.query(childRelationshipQuery);
                                //** Loop through all queried children
                                for (sObject childObject : childRelationshipQueryResults) {
                                    //** Add sObject to myChildRelationshipRows
                                    myChildRelationshipRows.add(new sObjectChildRelationshipRow(childObject));
                                    //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent
                                    if (mapObjectCurrent.containsKey(childObjectTypeName)) {
                                        mapObjectCurrent.get(childObjectTypeName).add(childObject);
                                    } else {
                                        mapObjectCurrent.put(childObjectTypeName, new List<sObject>{childObject});
                                    }
                                }
                                //** Add myChildRelationshipRows to a myChildRelationships record with relationship information
                                if (!myChildRelationshipRows.isEmpty()) {
                                    myChildRelationships.add(new sObjectChildRelationship(currentLevel,
                                        childObjectTypeName,
                                        childObjectFieldName,
                                        myChildRelationshipRows));
                                }
                            }
                        }
                    }
                }
            }
            //** Populate Map<Integer, Map<String, List<Id>>> mapObject
            if (!mapObjectCurrent.isEmpty()) {
                mapObject.put(currentLevel, mapObjectCurrent);
            }
        }
        //** Establish a list of Ids per each sObjectType for the result queries
        for (sObjectChildRelationship rel : myChildRelationships) {
            for (sObjectChildRelationshipRow row : rel.myChildRelationshipRowList) {
                if (!mapObjectIds.containsKey(rel.relationshipName)) {
                    mapObjectIds.put(rel.relationshipName, new List<Id>());
                }
                mapObjectIds.get(rel.relationshipName).add(row.sObjectOriginal.Id);
                if (!mapRelationshipFields.containsKey(rel.relationshipName)) {
                    mapRelationshipFields.put(rel.relationshipName, rel.relationshipFieldName);
                }
            }
        }
        //** Loop through each sObjectType to query the results for use later
        for (String objName : mapObjectIds.keySet()) {
            //** List of all Ids for the records to be cloned
            List<Id> sObjectIds = new List<Id>();
            sObjectIds = mapObjectIds.get(objName);
            //** List of all fields for the records to be cloned 
            List<String> sObjectFields = new List<String>();
            //** sObjectType
            Schema.SObjectType sObjectType = sObjectIds.get(0).getSObjectType();
            //** Get all current fields from the object
            if (sObjectType != null) {
                Map<String, Schema.SObjectField> fieldMap = sObjectType.getDescribe().fields.getMap();
                for (String fieldName : fieldMap.keySet()) {
                    if (fieldMap.get(fieldName).getDescribe().isCreateable()) {
                        sObjectFields.add(fieldName);
                    }
                }
            }
            for (Integer i = 0; i < sObjectFields.size(); i++) {
                if (UtilsVariables.getFieldExclusions(sObjectType.getDescribe().getName()).contains(sObjectFields.get(i))) {
                    sObjectFields.remove(i);
                }
            }
            //** If there are no records sent into the method, then return an empty list
            if (sObjectIds != null && !sObjectIds.isEmpty() && !sObjectFields.isEmpty()) {
                //** Construct a SOQL query to get all field values of all records (sort Id ascending)
                String sObjectFieldsQuery = 'SELECT ' + sObjectFields.get(0);
                for (Integer i = 1; i < sObjectFields.size(); i++) {
                    sObjectFieldsQuery += ', ' + sObjectFields.get(i);
                }
                sObjectFieldsQuery += ' FROM ' + sObjectType.getDescribe().getName();
                sObjectFieldsQuery += ' WHERE Id IN (\'' + sObjectIds.get(0) + '\'';
                for (Integer i = 1 ; i < sObjectIds.size() ; i++) {
                    sObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\'';
                }
                sObjectFieldsQuery += ')';
                System.debug('##### sObjectFieldsQuery: ' + sObjectFieldsQuery);
                List<sObject> sObjectFieldsQueryResults = Database.query(sObjectFieldsQuery);
                Map<Id, sObject> mapObjectFieldsQueryResults = new Map<Id, sObject>();
                for (sObject obj : sObjectFieldsQueryResults) {
                    mapObjectFieldsQueryResults.put(obj.Id, obj);
                }
                mapObjectQueries.put(objName, mapObjectFieldsQueryResults);
            }
        }
        //** Loop through each level to insert while adding the correct parent identification
        for (Integer currentLevel = 0 ; currentLevel < 20 ; currentLevel++) {
            List<sObject> sObjectsToClone = new List<sObject>();
            List<Id> listIdOld = new List<Id>();
            List<Id> listIdNew = new List<Id>();
            for (sObjectChildRelationship cloneChildRelationship : myChildRelationships) {
                if (cloneChildRelationship.relationshipLevel == currentLevel) {
                    sObjectsToClone = new List<sObject>();
                    for (sObjectChildRelationshipRow cloneChildRelationshipRow :
                            cloneChildRelationship.myChildRelationshipRowList) {
                        listIdOld.add(cloneChildRelationshipRow.sObjectOriginal.Id);
                        sObject orig = mapObjectQueries.get(cloneChildRelationship.relationshipName).get(
                            cloneChildRelationshipRow.sObjectOriginal.Id);
                        sObject clone = UtilsGeneral.newBlankSObject(orig);
                        Map<String, Schema.SObjectField> fieldMap = clone.getSObjectType().getDescribe().fields.getMap();
                        System.debug('##### Clone: Before = ' + clone);
                        for (String fieldName : fieldMap.keySet()) {
                            if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() &&
                                    !UtilsVariables.getFieldExclusions(clone.getSObjectType().getDescribe().getName()).contains(
                                    fieldName)) {
                                clone.put(fieldName, orig.get(fieldName));
                                System.debug('##### Clone: Field Clone = ' + fieldName + ': ' + orig.get(fieldName));
                            }
                            if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() &&
                                    UtilsVariables.getFieldDefaults(clone.getSObjectType().getDescribe().getName()).contains(
                                    fieldName)) {
                                clone.put(fieldName,
                                    UtilsVariables.getFieldDefault(clone.getSObjectType().getDescribe().getName(), fieldName));
                                System.debug('##### Clone: Field Default = ' + fieldName + ': ' + UtilsVariables.getFieldDefault(
                                    clone.getSObjectType().getDescribe().getName(), fieldName));
                            }
                        }
                        if (cloneChildRelationship.relationshipFieldName != null) {
                            clone.put(cloneChildRelationship.relationshipFieldName, mapIdOldNew.get((Id) mapObjectQueries.get(
                                cloneChildRelationship.relationshipName).get(cloneChildRelationshipRow.sObjectOriginal.Id).get(
                                cloneChildRelationship.relationshipFieldName)));
                            System.debug('##### Clone: Field Relationship = ' + cloneChildRelationship.relationshipFieldName +
                                ': ' + mapIdOldNew.get((Id) mapObjectQueries.get(cloneChildRelationship.relationshipName).get(
                                cloneChildRelationshipRow.sObjectOriginal.Id).get(
                                cloneChildRelationship.relationshipFieldName)));
                        }
                        System.debug('##### Clone: After = ' + clone);
                        sObject cloned = clone.clone(false, true);
                        sObjectsToClone.add(cloned);
                        System.debug('##### Clone: Cloned = ' + cloned);
                    }
                    //** Insert cloned records
                    insert sObjectsToClone;
                    //** Populate list with cloned (new) Ids [assumption is it's the same order as listIdOld]
                    for (sObject newObject : sObjectsToClone) {
                        listIdNew.add(newObject.Id);
                    }
                    //** Fail gracefully if listOldId.size() != listNewId.size()
                    System.assertEquals(listIdNew.size(), listIdOld.size());
                    //** Map the original (old) Ids to the cloned (new) Ids
                    for (Integer i = 0 ; i < listIdOld.size() ; i++) {
                        mapIdOldNew.put(listIdOld.get(i), listIdNew.get(i));
                        if (chatterPost && currentLevel == 0) {
                            UtilsChatter.postChatterFeedItem(ConnectApi.FeedType.Record,
                                listIdNew.get(i),
                                UtilsVariables.getAPEXMessage('CloneChatterMessage') + listIdOld.get(i));
                        }
                    }
                }
            }
        }
        List<Id> clonedObjectIds = new List<Id>();
        for (sObject originalObject : originalObjects) {
            clonedObjectIds.add(mapIdOldNew.get(originalObject.Id));
        }
        return clonedObjectIds;
    }
    
    //** Allow pages and external services to trigger a clone from a single object with or without a Chatter post
    webService static Id clone(Id originalId, Boolean chatterPost) {
        //** Build query to return the full object from an Id
        String originalObjectQuery = 'SELECT ID';
        originalObjectQuery += ' FROM ' + originalId.getSObjectType().getDescribe().getName();
        originalObjectQuery += ' WHERE Id = \'' + originalId + '\'';
        //** Executive query to return the full object from an Id
        List<sObject> originalObject = Database.query(originalObjectQuery);
        //** Execute clone()
        List<Id> clonedIds = clone(originalObject, chatterPost);
        return clonedIds[0];
    }
    
    //** Houses the relationship information with the list of related records
    public class sObjectChildRelationship {
        //** What level of child is it?  0=Parent 1=Child 2=Grand-Child 3=Great-Grand-Child etc...
        public Integer relationshipLevel {get; set;}
        //** What table are the related records stored in?
        public String relationshipName {get; set;}
        //** What is the field name that stores the parent-child relationship?
        public String relationshipFieldName {get; set;}
        //** Houses the list of actual records
        public List<sObjectChildRelationshipRow> myChildRelationshipRowList {get; set;}
        //** Initialize
        public sObjectChildRelationship(Integer relationshipLevel,
                String relationshipName,
                String relationshipFieldName,
                List<sObjectChildRelationshipRow> myChildRelationshipRowList) {
            this.relationshipLevel = relationshipLevel;
            this.relationshipName = relationshipName;
            this.relationshipFieldName = relationshipFieldName;
            this.myChildRelationshipRowList = myChildRelationshipRowList;
        }
    }
    
    //** Houses the actual record
    public class sObjectChildRelationshipRow {
        //** What record am I cloning?
        public sObject sObjectOriginal {get; set;}
        //** Initialize
        public sObjectChildRelationshipRow(sObject sObjectOriginal) {
            this.sObjectOriginal = sObjectOriginal;
        }
    }
    
}
  **Credit goes to Christopher Alun Lewis (blog) for coming up with the inner class idea which I modified to my own requirement here.

A few notes:
  • In order to clone master detail relationships, the option to re-parent must be selected on the field setup
  • You can get the utilChatter class here: utilChatter
  • If Opportunity is one of your child objects, you need to add these field exclusions at minimum: Amount
  • If OpportunityLineItem is one of your child objects, you need to add these field exclusions at minimum: isDeleted,SortOrder,TotalPrice,ListPrice,Subtotal,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModStamp
Happy Coding!

Chatter in Apex: Posting custom Chatter FeedItems more easily

One of the first challenges I set out to tackle was to simplify the process of inserting a Chatter FeedItem.  My team had already built two use-cases for posting an automated message into a Chatter feed, but each time we needed to modify the process of those use-cases, the FeedItem posting code was getting in the way.

My goal: Create a helper utility to simplify the process

It was simple enough to deconstruct and normalize the code since there were some very obvious overlaps: Adding a TextSegment, adding a MentionSegment, etc.  At this time, those are the only two I've tackled, but I plan to continue to build this out and will post an update when it makes sense.

This makes it very easy to post a message into Chatter.  As an example, if I wanted to post a message to a record that started with an @mention to someone, then had a text body, followed by a list of CC worthy @mentions, it would look roughly like:

postChatterFeedItem(ConnectApi.FeedType.Record, recordId, userId, textBody, List<userId>);

1. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:

global class UtilsChatter {
    
    //** Turn an input String into a useful TextSegmentInput
    global static void postChatterFeedItem(ConnectApi.FeedType FeedType, Id SubjectID, ConnectApi.FeedItemInput Input) {
        ConnectApi.ChatterFeeds.postFeedItem(null, FeedType, SubjectID, Input, null);
    }
    
    //** Turn an input String into a useful TextSegmentInput
    global static ConnectApi.TextSegmentInput addTextSegment(String thisString) {
        ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput();
        textSegment.text = thisString;
        return textSegment;
    }
    
    //** Turn an input Id into a useful MentionSegmentInput
    global static ConnectApi.MentionSegmentInput addMentionSegment(Id thisId) {
        ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput();
        mentionSegment.id = thisId;
        return mentionSegment;
    }
    
    //** Create and post a message to Chatter (Text)
    global static void postChatterFeedItem(ConnectApi.FeedType FeedType, Id SubjectId, String TextString1) {
        //** Combine the needed messageSegments into one coherent messageInput
        ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
        messageInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
        messageInput.messageSegments.add(addTextSegment(TextString1));
        //** Attach the messageInput as the Input.body
        ConnectApi.FeedItemInput Input = new ConnectApi.FeedItemInput();
        Input.body = messageInput;
        //** Post to Chatter
        postChatterFeedItem(FeedType, SubjectId, Input);
    }
    
    //** Create and post a message to Chatter (Mention)
    global static void postChatterFeedItem(ConnectApi.FeedType FeedType, Id SubjectId, Id UserId1) {
        //** Combine the needed messageSegments into one coherent messageInput
        ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
        messageInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
        messageInput.messageSegments.add(addMentionSegment(UserId1));
        //** Attach the messageInput as the Input.body
        ConnectApi.FeedItemInput Input = new ConnectApi.FeedItemInput();
        Input.body = messageInput;
        //** Post to Chatter
        postChatterFeedItem(FeedType, SubjectId, Input);
    }
    
    //** Create and post a message to Chatter (Mention List)
    global static void postChatterFeedItem(ConnectApi.FeedType FeedType, Id SubjectId, List<Id> UserId1) {
        //** Combine the needed messageSegments into one coherent messageInput
        ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
        messageInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
        for (Id i : UserId1) {
            messageInput.messageSegments.add(addMentionSegment(i));
        }
        //** Attach the messageInput as the Input.body
        ConnectApi.FeedItemInput Input = new ConnectApi.FeedItemInput();
        Input.body = messageInput;
        //** Post to Chatter
        postChatterFeedItem(FeedType, SubjectId, Input);
    }
    
}

A few notes:
  • I only posted the three headlining postChatterFeedItem methods above for simplicity, but these do not give much posting flexibility in and of themselves.  I have built out many more scenarios (e.g. Mention List + Text + Mention + Text + Mention List), let me know if you want the entire class (I'll send it offline so as not to clog up a post with the code).
  • I've only tackled MentionSegment and Text Segment (no support yet for Polls, Attachments, etc), but I plan to continue to build this out and will post an update when it makes sense.
Happy Coding!

Welcome

As an analyst, I've had a relatively small amount of experience with the salesforce.com backend setup, configuration, and programming.  My expertise is in understanding object structures and relationships to facilitate better reporting than is available in the generic "Reports" and "Dashboards" tabs.

It has been my experience that salesforce.com plays to the lowest common denominator as part of their multi-tenant structure, which tends to result in only moderately useful add-ons.  Where they truly excel is in their platform design, on top of which they have built their now-famous Sales Cloud and Service Cloud.

A few years back, I had built a thrown-together workflow website to help the customer service teams route incoming purchase orders through the manual data entry teams.  After administering this site for more than two years and bumping my head against very cumbersome code updates (it was originally built on a .NET infrastructure) to enable minor changes to the user experience, I decided to learn more about the capabilities of the force.com platform and invested a few weeks to migrate the application.

That project was so successful, I began looking for other ways to augment my force.com skills.

The company I work for has now been using salesforce.com for over seven years.  As is the case with many companies when they first purchase a service, the emphasis was on speed-to-functionality rather than the best end product, which resulted in many shortcuts, which have slowly accumulated over the years to form what we refer to as "spaghetti code" where poorly written code trips over other code, causing ugly user errors and broken functionality.

My new goal: revamp the foundation from the ground up.  As I run into and overcome obstacles during this process, I endeavor to share those findings with the next do-it-yourselfer.
Happy Coding!