Tuesday, 18 December 2012

Clone Plus: A quick update

After posting the original clone plus implementation, it has been pointed out that it doesn't work for objects that do not have "Name" fields (I didn't even realise such objects existed!). However, as far as I am aware, every object has an Id field, so here is an altered implementation that will work for the objects missing the name field.

Controller:
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 object type 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
  // many uncloneable system objects being added to the possibiilites.
  // Making these object type choices also allows us 
  // pick and chose the specific kinds of objects we want 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, retrieve related objects.       
    for (Schema.ChildRelationship childRelationship : 
                                   childRelationships)
    {
      Schema.SObjectType childObjectType = 
             childRelationship.getChildSObject();
      
      // Only retrieve the objects if their type is 
      // one of those we are interested in.          
      if (childObjectTypes.contains(
          childObjectType.getDescribe().getName()))
      {
        List<relatedObjectRow> relatedObjects = 
                    new List<relatedObjectRow>{};
                
        Schema.SObjectField childObjectField = 
                            childRelationship.getField();
                
        String relatedChildSObjectsquery = 'SELECT ID 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 ' + 
                                     String.join(sObjectFields,','); 
    
      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. 
  // It simple 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 getId(){
      try{
        return '' + obj.get('Id');
      } catch (Exception e){
        return '';
      }    
    }
  }
}

Page:
<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="Id"   
                              value="{!objectRow.Id}" 
                              width="45%"/>
        </apex:pageBlockTable>
      </apex:PageBlock>
    </apex:repeat>
    
    <apex:PageBlock >
      <apex:commandButton action="{!doClone}" value="Clone"/>
    </apex:PageBlock>
  </apex:form>  

</apex:page>


At this current point in time, my plan is to develop the next release of clone plus on the AppExchange as a free app, with a much better management facility, so you can control what you are cloning much more effectively. I'll keep you posted!