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!