Skip to content

Instantly share code, notes, and snippets.

@swapnilshrikhande
Last active September 7, 2017 07:05
Show Gist options
  • Save swapnilshrikhande/bca7a904a0907657a519aac194b8e58f to your computer and use it in GitHub Desktop.
Save swapnilshrikhande/bca7a904a0907657a519aac194b8e58f to your computer and use it in GitHub Desktop.
Generic SObject Clone Utility
public with sharing class SObjectClone {
public final Map<String, Id> recordTypeMap;
public Map<String,Map<String,String>> relationshipDetailMap;
public Schema.SObjectType objectType;
public Schema.FieldSet fieldSet;
public Set<String> relationshipNameSet;
public CloneInitializer initializer;
public Id cloneRecordTypeId;
public static final String FILTER_CLAUSE = ' WHERE ID IN :recordIdList';
public CloneConfig config;
public class CloneConfig {
//clone config
public Boolean preserveId;
public Boolean isDeepClone;
public Boolean preserveReadonlyTimestamps;
public Boolean preserveAutonumber;
//default values
public CloneConfig(){
this.preserveId = false;
this.isDeepClone = true;
this.preserveReadonlyTimestamps = false;
this.preserveAutonumber = false;
}
}
//initialize cloner for a particular type
public SObjectClone(Schema.SObjectType targetObject) {
recordTypeMap = Utils.getRecordTypeMapForObjectGeneric(targetObject);
relationshipDetailMap = Utils.getRelationshipDetails(targetObject);
objectType=targetObject;
}
public SObjectClone setCloneConfig(CloneConfig config){
this.config = config;
return this;
}
public SObjectClone setFieldSet(Schema.FieldSet fieldSet){
this.fieldSet = fieldSet;
return this;
}
public SObjectClone setRelationshipNameSet(Set<String> relationshipNameSet){
this.relationshipNameSet = relationshipNameSet;
return this;
}
public SObjectClone setInitializer(CloneInitializer initializer){
this.initializer = initializer;
return this;
}
public SObjectClone setTargetRecordTypeId(String targetRecordTypeDevName){
this.cloneRecordTypeId = recordTypeMap.get(targetRecordTypeDevName);
return this;
}
//clone SObject base on source record id and fieldset
// context : New SObject is initialized based on the context passed,
// which specifies the buisness process the cloning is used in current context.
public SObject clone(Id recordId){
Map<Id,SObject> clonedMap = cloneSObjectList(new Id[]{recordId});
return clonedMap.get(recordId);
}
public Map<Id,SObject> cloneSObjectList(List<Id> recordIdList){
//sanity checks, allow injecting config variable from external context
config = config==null ? new CloneConfig() : config;
//source id verses cloned SObject map
Map<Id,SObject> clonedSObjectMap = new Map<Id,SObject>();
//generate select for the SObject and passed fieldset
String query = Utils.generateQuery(objectType,
fieldSet,
relationshipNameSet,
FILTER_CLAUSE );
Map<Id,SObject> sourceSObjectMap = new Map<Id,SObject>( (List<SObject>)Database.query(query) );
for ( SObject SObjectRecord : sourceSObjectMap.values() ){
clonedSObjectMap.put( SObjectRecord.Id,
SObjectRecord.clone(config.preserveId,
config.isDeepClone,
config.preserveReadonlyTimestamps,
config.preserveAutonumber));
}
if( initializer != null ){
for(Id parentSObjectId : clonedSObjectMap.keySet()){
SObject clonedSObject = clonedSObjectMap.get(parentSObjectId);
system.debug('>>> cloneRecordTypeId : '+cloneRecordTypeId);
clonedSObject.put('RecordTypeId',cloneRecordTypeId);
clonedSObject = initializer.initialize(parentSObjectId, clonedSObject);
}
}
if( false == clonedSObjectMap.isEmpty() ){
insert clonedSObjectMap.values();
}
List<SObject> clonedRelatedRecordList = new List<SObject>();
//clone related records
SObject sourceSObject;
SObject clonedSObject;
Map<Integer,List<SObject>> dataToInsertMap = new Map<Integer,List<SObject>>();
String fieldName;
Integer relationShipCount=0;
Integer bucketNumber =0;
Map<String,String> relationFieldDetailsMap;
for(Id parentId : recordIdList ){
sourceSObject = sourceSObjectMap.get(parentId);
clonedSObject = clonedSObjectMap.get(parentId);
relationShipCount = 0;
for(String relationName : relationshipNameSet ){
if( relationName == null )
continue;
relationFieldDetailsMap = relationshipDetailMap.get(relationName.toLowerCase());
if( relationShipDetailMap == null )
continue;
//this code is required to make sure single bucket has records of at max 10 different relationships
//only, as salesforce does not allow dml insert on generic list containing data from more than 10 different objects
++relationShipCount;
bucketNumber = Integer.valueOf(relationShipCount/10);
if(dataToInsertMap.get(bucketNumber)==null){
dataToInsertMap.put(bucketNumber,new List<SObject>());
}
clonedRelatedRecordList = sourceSObject.getsObjects(relationName);
if( null != clonedRelatedRecordList ){
for(SObject clonedRecord : clonedRelatedRecordList.deepClone() ){
fieldName = relationFieldDetailsMap.get('RELATIONSHIP_FIELD');
clonedRecord.put(fieldName,clonedSObject.Id);
dataToInsertMap.get(bucketNumber).add(clonedRecord);
}
}
}
}
//insert related records
for( Integer key : dataToInsertMap.keySet() ){
if( null != dataToInsertMap.get(key) && false == dataToInsertMap.get(key).isEmpty() ){
insert dataToInsertMap.get(key);
}
}
return clonedSObjectMap;
}
}
@isTest
private class SObjectCloneTest {
// method to setup test data for the test class
@testSetup static void setupTestData() {
// account
Account accountObj = new Account();
accountObj = FRNOrgTestDataFactory.createInsComapnyAccountRecord(accountObj);
insert accountObj;
system.assertNotEquals(accountObj.Id, null);
//facility programs
Facility_Program__c fcltyPrgrm = new Facility_Program__c();
fcltyPrgrm = FRNOrgTestDataFactory.createFcltyPrgrmRecord(fcltyPrgrm,1);
fcltyPrgrm.FRN_Facility__c = accountObj.Id;
insert fcltyPrgrm;
system.assertNotEquals(fcltyPrgrm.Id, null);
}
// method to test SObjectClone()
static testMethod void SObjectCloneTest() {
Test.startTest();
SObjectClone cloneCls = new SObjectClone(Account.sObjectType);
Test.stopTest();
System.AssertEquals(cloneCls.objectType, Account.sObjectType);
}
// method to test SObjectClone()
static testMethod void setFieldSetTest() {
Test.startTest();
SObjectClone cloneCls = new SObjectClone(Account.sObjectType)
.setFieldSet(SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone);
Test.stopTest();
//System.AssertEquals(cloneCls.fieldSet, SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone);
}
// method to test SObjectClone()
static testMethod void setTargetRecordTypeIdTest() {
Test.startTest();
SObjectClone cloneCls = new SObjectClone(Account.sObjectType)
.setTargetRecordTypeId('Insurance_Company');
Test.stopTest();
//System.AssertEquals(cloneCls.fieldSet, SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone);
}
// method to test SObjectClone()
static testMethod void setCloneConfigTest() {
Test.startTest();
//SObjectClone cloneCls = new SObjectClone(Account.sObjectType);
SObjectClone.CloneConfig configCls = new SObjectClone.CloneConfig();
configCls.preserveId = true;
configCls.isDeepClone = true;
configCls.preserveReadonlyTimestamps = true;
configCls.preserveAutonumber = true;
SObjectClone cloneCls = new SObjectClone(Account.sObjectType);
cloneCls = cloneCls.setCloneConfig(configCls);
Test.stopTest();
System.AssertEquals(cloneCls.config.preserveId, true);
System.AssertEquals(cloneCls.config.isDeepClone, true);
System.AssertEquals(cloneCls.config.preserveReadonlyTimestamps, true);
System.AssertEquals(cloneCls.config.preserveAutonumber, true);
}
// method to test setRelationshipNameSet()
static testMethod void setRelationshipNameSetTest() {
Test.startTest();
Set<String> relationShipSet = new Set<String>{'Facility_Programs__r'};
SObjectClone cloneCls = new SObjectClone(Account.sObjectType)
.setRelationshipNameSet(relationShipSet);
Test.stopTest();
System.AssertEquals(cloneCls.relationshipNameSet.size(), relationShipSet.size());
}
// method to test SObjectClone()
static testMethod void cloneTest() {
Test.startTest();
Account accountObj = [SELECT Id,
(SELECT id
FROM Facility_Programs__r)
FROM Account
LIMIT 1];
Set<String> relationShipSet = new Set<String>{'Facility_Programs__r'};
InitializerTestCls initCls = new InitializerTestCls();
initCls.accountName = 'Clone Insurance Company';
SObjectClone.CloneConfig configCls = new SObjectClone.CloneConfig();
configCls.preserveId = false;
configCls.isDeepClone = true;
configCls.preserveReadonlyTimestamps = true;
configCls.preserveAutonumber = true;
Account cloneAccount = (Account)new SObjectClone(Account.sObjectType)
.setInitializer(initCls)
.setRelationshipNameSet(relationShipSet)
.setCloneConfig(configCls)
.setFieldSet(SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone)
.setTargetRecordTypeId('Insurance_Company')
.clone(accountObj.Id);
Test.stopTest();
Account accountToAssert = [SELECT id,
Name,
RecordTypeId,
(SELECT Id
FROM Facility_Programs__r)
FROM Account
WHERE Name = 'Clone Insurance Company'];
System.assertNotEquals(accountToAssert, null);
System.assertEquals(accountToAssert.Facility_Programs__r.Size(), accountObj.Facility_Programs__r.Size());
}
// cloned account initializer
public class InitializerTestCls implements CloneInitializer{
public String accountName = '';
public SObject initialize(Id parentId, SObject clonedRecord){
Account clonedAccount = (Account)clonedRecord;
clonedAccount.Name = accountName;
return clonedAccount;
}
}
}
// Class which contains useful utility methods required across classes
public class Utils {
public static final String OBJECTNAME = 'RELATIONSHIP_OBJECT';
public static final String FIELDNAME = 'RELATIONSHIP_FIELD';
public static final String QUERY_TEMPLATE = 'SELECT {0} FROM {1} {2}';
public static final String NESTED_QUERY = '({0})';
public static final String COMMA_SEPARATOR = ', ';
public static final String FIELD_ID = 'Id';
//cache holders
public static Map<String, Schema.SObjectType> GLOBALDESCRIBE;
//Sobject wise relationships details
public static Map<String, Map<String,Map<String,String>> > objectWiseRelationshipMap;
//cache methods
public static Schema.SObjectType getSObjectType(String objectName){
//lazy load
if(null == GLOBALDESCRIBE){
GLOBALDESCRIBE = Schema.getGlobalDescribe();
}
return GLOBALDESCRIBE.get(objectName);
}
//static initializers
static {
objectWiseRelationshipMap = new Map<String, Map<String,Map<String,String>> >();
}
//Util methods start
//Gerate query for all fields in the fieldset and all fields from the related objects
//relatedRelationshipNames is a map of
// Relationshipname => Related ObjectName
public static String generateQuery( Schema.sObjectType fromObject
, Schema.FieldSet fieldSet
, Set<String> relationshipNameSet
, String whereClause
){
List<String> queryFieldList;
if( fieldSet != null ){
queryFieldList = getFieldListFor(fieldSet);
} else {
queryFieldList = getFieldListFor(fromObject);
}
//generate nestedQueryClauses
if( null != relationshipNameSet ){
queryFieldList.addAll( getInnerQueryList(fromObject,relationshipNameSet) );
}
String query = generateQuery(''+fromObject, queryFieldList, whereClause);
return query;
}
public static List<String> getInnerQueryList(Schema.sObjectType fromObject,Set<String> relationshipNameSet){
List<String> queryFieldList = new List<String>();
String objectName;
List<String> relatedFieldList;
String relatedQuery;
Map<String,Map<String,String>> relationDetailsMap = getRelationshipDetails(fromObject);
System.debug('relationDetailsMap='+relationDetailsMap.keySet());
Map<String,String> relationshipDetailMap;
for( String relationshipName : relationshipNameSet ){
relationshipName = relationshipName!=null ? relationshipName.toLowerCase() : null;
relationshipDetailMap = relationDetailsMap.get(relationshipName);
if( relationshipDetailMap == null ){
System.debug('relationshipDetailMap is null for '+relationshipName);
continue;
}
objectName = relationshipDetailMap.get('RELATIONSHIP_OBJECT');
relatedFieldList = getFieldListFor(getSObjectType(objectName));
relatedQuery = generateQuery( relationshipName
, relatedFieldList
, '');
relatedQuery = String.format(NESTED_QUERY,new String[]{relatedQuery});
queryFieldList.add(relatedQuery);
}
return queryFieldList;
}
//generate query for fields within a field set
public static String generateQuery( Schema.sObjectType fromObject
, Schema.FieldSet fieldSet
, String whereClause){
return generateQuery(''+fromObject, getFieldListFor(fieldSet), whereClause);
}
//generate query for fieldnames passed in
public static String generateQuery( String fromObject
, List<String> fieldNameSet
, String whereClause){
//sanity check
whereClause = whereClause == null ? '' : whereClause;
//local variables
String queryFieldsClause = '';
for(String fieldName : fieldNameSet) {
//if not first time here, add a ,
if(false == String.isBlank(queryFieldsClause)){
queryFieldsClause += COMMA_SEPARATOR;
}
queryFieldsClause += fieldName;
}
//Corner cases if no fields in the field set field set
//1
if( true == String.isBlank(queryFieldsClause) ){
queryFieldsClause += FIELD_ID;
}
String query = String.format(QUERY_TEMPLATE, new String[]{
queryFieldsClause
, ''+fromObject
, whereClause
});
return query;
}
//Supporting methods
public static List<String> getFieldListFor(Schema.FieldSet fieldSet){
List<String> fieldNameSet = new List<String>();
for(Schema.FieldSetMember fieldSetMember : fieldSet.getFields()) {
fieldNameSet.add(fieldSetMember.getFieldPath());
}
return fieldNameSet;
}
public static List<String> getFieldListFor(Schema.sObjectType fromObject){
system.debug('fromObject='+fromObject);
List<String> fieldNameSet = new List<String>();
Schema.DescribeFieldResult dfr;
for(Schema.SObjectField fieldInstance : fromObject.getDescribe().fields.getMap().values()) {
dfr = fieldInstance.getDescribe();
//@TODO TO remove as all fields should be cloned
//if( dfr.isAccessible() ){
fieldNameSet.add( dfr.getname() );
//}
}
return fieldNameSet;
}
public static Boolean startsWith(String sourceString, String character){
//sanity
if( sourceString == null ){
return false;
}
return sourceString.trim().startsWith(character);
}
public static Map<String,Map<String,String>> getRelationshipDetails(Schema.SObjectType parentSobjectType){
Map<String,Map<String,String>> relationDetailsMap = objectWiseRelationshipMap.get(''+parentSobjectType);
//if in cache return immediately
if( null != relationDetailsMap ){
return relationDetailsMap;
} else {
relationDetailsMap = new Map<String,Map<String,String>>();
}
Schema.DescribeSObjectResult describeResult = parentSobjectType.getDescribe();
String fieldName;
Map<String,String> detailsMap;
String childRelationshipName;
for(Schema.ChildRelationship childRelationship : describeResult.getChildRelationships()){
childRelationshipName = childRelationship.getRelationshipName();
if( childRelationshipName == null )
continue;
//objectname
detailsMap = new Map<String,String>();
detailsMap.put(Utils.OBJECTNAME, ''+childRelationship.getChildSObject());
//field
fieldName = childRelationship.getField().getDescribe().getName();
detailsMap.put(Utils.FIELDNAME, fieldName);
childRelationshipName = childRelationshipName.toLowerCase();
relationDetailsMap.put( childRelationshipName, detailsMap );
}
objectWiseRelationshipMap.put(''+parentSobjectType,relationDetailsMap);
return relationDetailsMap;
}
//record type details utility methods
//Record types cache
private static Map<Schema.SObjectType,Map<String,Id>> rtypesCache;
private static List<sObject> results;
static {
rtypesCache = new Map<Schema.SObjectType,Map<String,Id>>();//convenient map, formatted from results.
results = new List<sObject>();//contains all recordtypes retrieved via SOQL
}
// Returns a map of active, user-available RecordType IDs for a given SObjectType,
// keyed by each RecordType's unique, unchanging DeveloperName
public static Map<String, Id> getRecordTypeMapForObjectGeneric(Schema.SObjectType token) {
// Do we already have a result?
Map<String, Id> mapRecordTypes = rtypesCache.get(token);
// If not, build a map of RecordTypeIds keyed by DeveloperName
if (mapRecordTypes == null) {
mapRecordTypes = new Map<String, Id>();
rtypesCache.put(token,mapRecordTypes);
} else {
// If we do, return our cached result immediately!
return mapRecordTypes;
}
// Get the Describe Result
Schema.DescribeSObjectResult obj = token.getDescribe();
//Check if we already queried all recordtypes.
if (results == null || results.isEmpty()) {
// Obtain ALL Active Record Types
// (We will filter out the Record Types that are unavailable
// to the Running User using Schema information)
String soql = 'SELECT Id, Name, DeveloperName, sObjectType FROM RecordType WHERE IsActive = TRUE';
try {
results = Database.query(soql);
} catch (Exception ex) {
results = new List<SObject>();
}
}
// Obtain the RecordTypeInfos for this SObjectType token
Map<Id,Schema.RecordTypeInfo> recordTypeInfos = obj.getRecordTypeInfosByID();
// Loop through all of the Record Types we found,
// and weed out those that are unavailable to the Running User
for (SObject rt : results) {
if (recordTypeInfos.get(rt.Id) != null) {
if (recordTypeInfos.get(rt.Id).isAvailable()) {
// This RecordType IS available to the running user,
// so add it to our map of RecordTypeIds by DeveloperName
mapRecordTypes.put(String.valueOf(rt.get('DeveloperName')),rt.Id);
}
else {
System.debug('The record type ' + rt.get('DeveloperName') + ' for object ' + rt.get('sObjectType') + ' is not availiable for the user.');
}
}
}
return mapRecordTypes;
}
public static String currentTimeLong(){
return System.now().format('MM/dd/yyyy hh:mm:ss a', UserInfo.getTimeZone().getID());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment