Wednesday 25 April 2012

Clone Plus: Clone Salesforce objects with children

In a previous post, I outlined an Apex cloning method that copied an sObject in its entirety, all data values included. Recently there have been some comments and questions on the post asking if it would be possible to use the method to create a way to clone an object and its children via a custom button on an object page without extensive development expertise required.

With that in mind, I have created a little helper tool I have called "Clone Plus" that does just that. It consists of an Apex controller, a single Visualforce page and custom button.

The clone plus button can be created on custom and standard object page layouts. When clicked, the button redirects the user to a Visualforce page that displays the children that can be cloned and associated with the new object. The user selects the objects they want to clone using checkboxes, and then clicks on a clone button to finish the operation. After the saving process is completed, the user is redirected to the new object.

The clone button on the page



The child clone selection screen



The new cloned object



Create the clone button by following the following steps: 

1) Create a new Apex class (Navigate to Setup -> Develop -> Apex Classes and click new). Copy in the following code into the class body:

public class ClonePlusController {

  public List<relatedObjects> objectChildren  { get; set; }
  public String               objectTypeName  { get; set; }
  public String               objectName      { get; set; }
   
  private SObject headSObject, headClone;
  
  // Initialisation method called when the clone plus page is loaded.
  // Use the id page parameter to find out what kind of 
  // object we are trying to clone.
  // Then load the object from the database.
  // Finally call the populateObjectChildren method to      
  public void initialiseObjectsForCloning()
  {

    // Here we generate a keyprefixmap using the global describe 
    // Then compare that to our object to determine type.  
    Map<String, Schema.SObjectType> gd = 
                   Schema.getGlobalDescribe(); 
       
    Map<String,String> keyPrefixMap = 
                  new Map<String,String>{};
          
    for(String sObj : gd.keySet()){
      Schema.DescribeSObjectResult r =  gd.get(sObj).getDescribe();
      keyPrefixMap.put(r.getKeyPrefix(), r.getName());
    }
      
    String objectID = 
           ApexPages.currentPage().getParameters().get('id');
    
    String objectTypeKey = 
           objectId.subString(0,3);
      
    objectTypeName = 
           keyPrefixMap.get(objectTypeKey);
      
    String primaryObjectQueryString = 'SELECT Id, Name FROM '
                                    + objectTypeName
                                    + ' WHERE Id = \''
                                    + objectId
                                    + '\'';
    
    headSObject = Database.query(primaryObjectQueryString);
    objectName          = '' + headSObject.get('Name');
    populateObjectChildren();    
  }

  // Get all of the children of the current object that have a 
  // object type contained in the child object types page parameter.
  // Not restricting the child objects to particular types 
  // results in unclonable system objects being added to the options, 
  // which we need to avoid (You will not want to clone these!)
  // Making these object type choices also allows us 
  // focus our efforts on the specific kinds of objects 
  // we want to allow users to clone.  
  public void populateObjectChildren()
  {
       
    objectChildren = new List<relatedObjects>{};
        
    Set<String> childObjectTypes = new Set<String>{};
    
    // read the object types from the page parameter.    
    childObjectTypes.addAll(
         ApexPages.currentPage().getParameters()
                                .get('childobjecttypes')
                                .split(',')
    );
    
    // Use the sobjecttype describe method to retrieve all 
    // child relationships for the object to be cloned.    
    Schema.DescribeSObjectResult headDescribe = 
           headsObject.getSObjectType().getDescribe();
    
    List<Schema.ChildRelationship> childRelationships = 
           headDescribe.getChildRelationships(); 
    
    // Iterate through each relationship, 
    // and retrieve the related objects.       
    for (Schema.ChildRelationship childRelationship : 
                                  childRelationships)
    {
      Schema.SObjectType childObjectType = 
                         childRelationship.getChildSObject();
      
      // Only retrieve the objects if their type is 
      // included in the page argument.          
      if (childObjectTypes.contains(
                           childObjectType.getDescribe().getName()))
      {
        List<relatedObjectRow> relatedObjects = 
                         new List<relatedObjectRow>{};
                
        Schema.SObjectField childObjectField = 
                         childRelationship.getField();
                
        String relatedChildSObjectsquery = 
               'SELECT ID, Name FROM ' 
             + childObjectType.getDescribe().getName()
             + ' WHERE '
             + childObjectField.getDescribe().getName()
             + ' = \'' 
             + headsObject.Id
             + '\''; 
                                                        
        for (SObject childObject : 
             Database.query(relatedChildSObjectsquery))
        {
          relatedObjects.add(new relatedObjectRow(childObject));
        }
            
        if (!relatedObjects.isEmpty())
        {
          objectChildren.add(new relatedObjects(relatedObjects, 
                childObjectType.getDescribe().getLabelPlural(), 
                childObjectField.getDescribe().getName()));
        }  
      }
    }
  }
  
  // Perform the cloning process.
  // First clone the parent, then all of the child objects. 
  // Then redirect the user to the new object page.
  public PageReference doClone()
  {
    headClone = cloneObjects(new List<sObject>{headSObject}).get(0);
    
    insert headClone;
    
    cloneSelectedObjects();
    
    return new PageReference('/' + headClone.Id);
  }
  
  // Clone the selected child objects.
  // Associate the cloned objects with the new cloned parent object.
  public void cloneSelectedObjects()
  {
        
    List<sObject> clonedObjects = new List<sObject>{};
    List<sObject> selectedRelatedObjects;
     
    for (relatedObjects relatedObject : objectChildren)
    {
      selectedRelatedObjects = new List<sObject>{};  
      clonedObjects = new List<sObject>{};  
      
      for (relatedObjectRow row : relatedObject.objectRows) 
      {
        if (row.selected)
        {
          selectedRelatedObjects.add(row.obj);
        }
      }
      
      if (!selectedRelatedObjects.isEmpty())
      {
        clonedObjects = cloneObjects(selectedRelatedObjects);
        
        for (sObject clone : clonedObjects)
        {
          clone.put(relatedObject.relatedFieldName, headClone.Id);  
        }
        
        insert clonedObjects;
      }
    }
  }

  // Clone a list of objects to a particular object type
  // Parameters 
  // - List<sObject> sObjects - the list of objects to be cloned 
  // The sObjects you pass in must include the ID field, 
  // and the object must exist already in the database, 
  // otherwise the method will not work.
  public static List<sObject> cloneObjects(List<sObject> sObjects){
                                                
    Schema.SObjectType objectType = sObjects.get(0).getSObjectType();
    
    // A list of IDs representing the objects to clone
    List<Id> sObjectIds = new List<Id>{};
    // A list of fields for the sObject being cloned
    List<String> sObjectFields = new List<String>{};
    // A list of new cloned sObjects
    List<sObject> clonedSObjects = new List<sObject>{};
    
    // Get all the fields from the selected object type using 
    // the get describe method on the object type.    
    if(objectType != null)
    {
      for (Schema.SObjectField objField : 
           objectType.getDescribe().fields.getMap().values())
      { 
        Schema.DescribeFieldResult fieldDesc = objField.getDescribe();
        // If the field type is location, then do not include it,
        // otherwise it will cause a soql exception.
        // Note that excluding the field does not stop the location from
        // being copied to the new cloned object.
        if(fieldDesc.getType() != DisplayType.LOCATION)
        {
          sObjectFields.add(fieldDesc.Name);
        }
      }
    }
    
    // If there are no objects sent into the method, 
    // then return an empty list
    if (sObjects != null || 
        sObjects.isEmpty() || 
        sObjectFields.isEmpty()){
    
      // Strip down the objects to just a list of Ids.
      for (sObject objectInstance: sObjects){
        sObjectIds.add(objectInstance.Id);
      }

      /* Using the list of sObject IDs and the object type, 
         we can construct a string based SOQL query 
         to retrieve the field values of all the objects.*/
    
      String allSObjectFieldsQuery = 'SELECT ' + sObjectFields.get(0); 
    
      for (Integer i=1 ; i < sObjectFields.size() ; i++){
        allSObjectFieldsQuery += ', ' + sObjectFields.get(i);
      }
    
      allSObjectFieldsQuery += ' FROM ' + 
                               objectType.getDescribe().getName() + 
                               ' WHERE ID IN (\'' + sObjectIds.get(0) + 
                               '\'';

      for (Integer i=1 ; i < sObjectIds.size() ; i++){
        allSObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\'';
      }
    
      allSObjectFieldsQuery += ')';
    
      system.debug('allSObjectFieldsQuery: ' + allSObjectFieldsQuery);
    
      try{
      
        // Execute the query. For every result returned, 
        // use the clone method on the generic sObject 
        // and add to the collection of cloned objects
        for (SObject sObjectFromDatabase:
             Database.query(allSObjectFieldsQuery)){
          clonedSObjects.add(sObjectFromDatabase.clone(false,true));  
        }
    
      } catch (exception e){
      }
      
    }
   
    return clonedSObjects;
    
  }
  
  // Related objects data construct - 
  // used to store a collection of child objects connected to 
  // the head object through the same relationship field.
  public class relatedObjects
  {
    public List<relatedObjectRow> objectRows { get; set; }
    public String                 pluralLabel      { get; set; }
    public String                 relatedFieldName { get; set; }
    
    public relatedObjects(List<relatedObjectRow> objectRows, 
                          String pluralLabel, 
                          String relatedFieldName) 
    {
      this.objectRows       = objectRows;
      this.pluralLabel      = pluralLabel;
      this.relatedFieldName = relatedFieldName;
    }   
  }     

  // An indidual child object row. 
  // Each instance simply contains the object definition, 
  // and a checkbox to select the row for cloning 
  // on the clone plus page.
  public class relatedObjectRow
  {
    public sObject obj      { get; set; }
    public Boolean selected { get; set; }
    
    public relatedObjectRow(Sobject obj)
    {
      this.obj      = obj;
      // All object rows are selected by default.
      this.selected     = true;
    }
    
    public String getName(){
      try{
        return '' + obj.get('Name');
      } catch (Exception e){
        return '';
      }    
    }   
  }
}

2) Create a new Visualforce Page  (Navigate to Setup -> Develop -> Pages and click new).
Enter ClonePlus in both the label and name fields.




3) Copy the following into the Visualforce Markup section:

<apex:page controller="ClonePlusController" action="{!initialiseObjectsForCloning}">
  
  <apex:sectionHeader title="Clone Plus: {!objectName}"/>

  <apex:form id="theform" >
  
    Please select the child objects you would like to clone.<br/><br/>
  
    <apex:repeat value="{!objectChildren}" var="child">
      <apex:PageBlock title="{!child.pluralLabel}"> 
        <apex:pageBlockTable value="{!child.objectRows}" 
                             var="objectRow">
          <apex:column headerValue="Clone" width="10%">
            <apex:inputCheckbox value="{!objectRow.selected}"/>
          </apex:column>
          <apex:column headerValue="Name" value="{!objectRow.name}" 
                                          width="90%"/>
        </apex:pageBlockTable>
      </apex:PageBlock>
    </apex:repeat>
    
    <apex:PageBlock >
      <apex:commandButton action="{!doClone}" value="Clone"/>
    </apex:PageBlock>
  </apex:form>  

</apex:page>

4) Navigate to the custom button and links menu for the object you want to add the clone with children functionality for.

If this is standard object, select  Setup -> Customize -> *Object Name* -> Buttons and Links. See the following example for contact.



If instead you want to clone a custom object, go to the custom objects definition page (Setup -> Create Object) and select the object from the list. Once the custom object definition page is open, scroll down to see the button and links menu. The following screenshots show how to do this for an example object called Invoice Statement




5) Click on the new button on the menu. Populate the form values as follows:




6) In the button definition body, write a URL in the following form:

/apex/ClonePlus?id={!*Parent Object Type*.Id}&childobjecttypes=*Child Object Types*

Where  *Parent Object Type*  is the API name of the object you are creating the button for, and  *Child Object Types* is a comma separated list of the types of child object you want to be able to clone alongside your main parent object.

Here are some examples to help:

If you want to clone the standard Account object from the accont page, but also want to clone related standard Contact objects at the same time:

/apex/ClonePlus?id={!Account.Id}&childobjecttypes=Contact

If you want to clone the standard Campaign object, and also want the option to clone related standard Opportunity objects, but also related child instances of a custom object called Campaign Advert (Api name: Campaign_Advert__c):

/apex/ClonePlus?id={!Campaign.Id}&childobjecttypes=Opportunity,Campaign_Advert__c

If you want to clone a custom object of type Custom Parent (Api name: Custom_Parent__c) and two related child custom object types Custom Child (Api name:  Custom_Child__c) and Another Child (Api name:  Another_Child__c):

/apex/ClonePlus?id={!Custom_Parent__c.Id}&childobjecttypes=Custom_Child__c,Another_Child__c

Your form should look something like this:



Click the save to confirm the changes.

NOTE: If you have any problems defining your URL, write a comment below with the object names and relationships and I would be happy to reply with what the value should be.

7) Finally, add the new "Clone Plus" custom button to the page layouts you want it to appear on, using the handy WYSIWYG editior.

Page Layouts for standard objects can be found through the menu ( Setup -> Customize -> *Object Name* -> Page Layouts)



For custom objects, the page layouts appear on the same object summary page as the custom button and links menu.

So there you have it, Happy Cloning!! This is a first iteration of Clone Plus. I hope to develop this concept as time moves forward, all feedback and suggestions is greatly appreciated!

A few notes:
  • Due to the generic nature of the method, you can add this functionality to as many objects as you like. You do not need to create any extra controllers or pages, they can all use the two already defined.
  • At the moment clone plus only clones direct children, not parents, grandchildren or further relationships. Of course this is possible, all it takes is some more complex method calls. For this example I wanted to keep things simple.
  • In this example I make the user define the types of object they want to clone. You may be asking, why not just clone everything? This was actually the approach I had at the beginning, but after trying it out, there are a lot of setup objects that are related to each object type behind the scenes. Most of these related objects would never be cloned, so including them does nothing but confuse the user.


Tuesday 10 April 2012

Forcewest - 17th April 2012



Calling all Salesforce professionals in the south west of England! The third Forcewest meeting will be taking place on the 17th April 2012 6pm at Totos wine bar, Bristol.


Forcewest is a unique Salesforce community group organised by Desynit that brings together local Salesforce and cloud computing professionals based in Bristol and the surrounding area. 


In this Forcewest, Simon Parker will be presenting a Salesforce implementation case study for the charity Surf Life Saving. After the presentation, Simon will be holding a Q&A session. The community will then be invited to network, exchange experiences and discuss all things Salesforce.


So if your a Salesforce developer, administrator or user, or just curious about what the local cloud community has to offer, please register your name on our Eventbrite page 


Oh and one last thing.... Desynit will be placing a tab behind the bar, so come on down and grab a free pint :D .