Manual Share of records through SOAP Webservices: Partner WSDL or Enterprise WSDL or Custom Code

Recently I was writing about solution to bug in new FF managed package which breaks S2S connection which required change in account trigger logic to automatically share that account or those accounts being updated to partner organization.
Now I would like to discuss possible solutions in comparison on example of manual insert\update share.
There was a beautiful article on this topic, I borrowed a basic concept from this article when I started working on this issue.

Let’s consider basic version of method to insert a record in a partner org.

This simple method takes Id of local record as parameter and performs insert in a partner org using Partner WSDL.
For debug purposes we may use http://requestb.in/ instead or actual server url. After successful insert in partner org we update value of specific field called FF_Account_Id__c which contains id of the record in the partner org.

webservice static void share(Id localId) {   
        List<Account> acct = new List<Account>();
        String sql = 'SELECT Name,Phone,Website FROM Account WHERE ID=:localId';
        acct = Database.Query(sql);
        partnerSoapSforceCom.Soap con = new partnerSoapSforceCom.Soap();
        partnerSoapSforceCom.LoginResult loginResult = con.login('some@user.name', 'PasswordAnDwEiRdSeCuRiTyTOkeN');    
        con.SessionHeader = new partnerSoapSforceCom.SessionHeader_element();
        con.endpoint_x = isDebug ? 'http://requestb.in/10xcdxy9' : loginResult.ServerUrl;
        System.debug(LoggingLevel.ERROR, '@@@ con.endpoint_x: ' + con.endpoint_x );
        con.Sessionheader.sessionid = loginResult.sessionid;
        List<sobjectPartnerSoapSforceCom.sObject_x> sObjects = new List<sobjectPartnerSoapSforceCom.sObject_x>();           
        sobjectPartnerSoapSforceCom.sObject_x tmpObj = new sobjectPartnerSoapSforceCom.sObject_x();
        for(Account ac:acct)
        {
            tmpObj.type_x = 'Account';
            tmpObj.Name = ac.Name;            
            tmpObj.Phone = ac.Phone;            
            tmpObj.Website = ac.Website; 

System.debug(LoggingLevel.ERROR, '@@@ ac: ' + ac );
System.debug(LoggingLevel.ERROR, '@@@ acsoap: ' + tmpObj );
            sObjects.add(tmpObj);
        }
        partnerSoapSforceCom.SaveResult[]srs = con.create(sObjects);

        String s = '';
        for ( partnerSoapSforceCom.SaveResult sr: srs ) {
            if (!sr.success ) {
                for ( partnerSoapSforceCom.Error e: sr.errors) {
                    s += 'SC: ' + e.statusCode + ' | EM: ' + e.message + ' | fields: ' + (e.fields == null ? '' : String.join(e.fields, '; '));
                }
                throw new WebS2SDMLException(s);
            }  else {
                System.debug(LoggingLevel.ERROR, '@@@ sr: ' + sr );
                System.debug(LoggingLevel.ERROR, '@@@ sr.Id: ' + sr.Id );
                acct[0].FF_Account_Id__c = sr.Id;
                update acct[0];
            }
            
        }
    }

The next code sample is more elegant and uses custom setting for storing fields which we have to map and user credentials from partner org while in the previous version we just enumerated fields explicitly and hard coded user credentials (which may be a quick solution but also bad practice).

webservice static void e_share(Id localId)  {   
    	Map<String, WebS2SFieldCopy__c> fieldsMap = WebS2SFieldCopy__c.getAll();
        if ( fieldsMap.isEmpty() ) {
            throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c is not setup');
        }
        String SFLName;
        for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
            if ( !String.isBlank( webS2SFieldCopy.SF_Login_Name__c ) ) {
                SFLName = webS2SFieldCopy.SF_Login_Name__c;
                break;
            }
        }
        if ( String.isBlank( SFLName ) ) {
            throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c does not save SF_Login_Name__c ');
        }
        Salesforce_Login__c sfl = [ SELECT Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c WHERE Name = :SFLName LIMIT 1 ];
        
        List<Account> acct = new List<Account>();
        String sql = 'SELECT Name,' + String.join( new List<String>( fieldsMap.keySet() ), ',') + ' FROM Account WHERE ID=:localId';
        acct = Database.Query(sql);
        
        enterpriseSoapSforceCom.Soap con = new enterpriseSoapSforceCom.Soap();
              
        enterpriseSoapSforceCom.LoginResult loginResult = con.login(sfl.Username__c, sfl.Password_Series__c + sfl.Current_Security_Token__c);    
        
        con.SessionHeader = new enterpriseSoapSforceCom.SessionHeader_element();
        con.endpoint_x =loginResult.ServerUrl;
        con.Sessionheader.sessionid = loginResult.sessionid;
        
        List<sobjectEnterpriseSoapSforceCom.sObject_x> sObjects = new List<sobjectEnterpriseSoapSforceCom.sObject_x>();           
        sobjectEnterpriseSoapSforceCom.sObject_x tmpObj = new sobjectEnterpriseSoapSforceCom.sObject_x();
        for(Account ac:acct)
        {
			/*for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
                tmpObj.setField(webS2SFieldCopy.Partner_Field_Name__c, ac.get(webS2SFieldCopy.Name));
            }*/
            /* Account Phone, Bank City, Billing Country, Billing State/Province, Billing Street, Billing Zip/Postal Code, CRM Account ID, Website */
            tmpObj.type_x = 'Account';
             
            // tmpObj.put('Phone') = ac.Phone; 
            tmpObj.Name = ac.Name;      
            tmpObj.Phone = ac.Phone;      
            tmpObj.Website = ac.Website;  
                 
            sObjects.add(tmpObj);
        }
        List<enterpriseSoapSforceCom.SaveResult> sr = con.create(sObjects); 
         
        if (!sr[0].success ) {
            String s;
            for ( enterpriseSoapSforceCom.Error e: sr[0].errors) {
                s += 'SC: ' + e.statusCode + ' | EM: ' + e.message + ' | fields: ' + String.join(e.fields, '; ');
            }
            throw new WebS2SDMLException(s);
        }
    }

However, we still can’t do something like

for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
                tmpObj.put(webS2SFieldCopy.Partner_Field_Name__c, ac.get(webS2SFieldCopy.Name));
            }

or

for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
                tmpObj.setField(webS2SFieldCopy.Partner_Field_Name__c, ac.get(webS2SFieldCopy.Name));
            }

which is great disadvantage of automatically generated Apex Classes for both Partner and Enterprise WSDL (in comparison to some standard libraries which they provide for Java or C#).

To make update instead of insert, we just have to change line

partnerSoapSforceCom.SaveResult[]srs = con.create(sObjects);

to line

        partnerSoapSforceCom.SaveResult[]srs = con.update_x(sObjects);

or line

enterpriseSoapSforceCom.SaveResult[]srs = con.create(sObjects);

to line

        enterpriseSoapSforceCom.SaveResult[]srs = con.update_x(sObjects);

Let’s consider just simple code for S2S update for single record using Partner WSDL

    /**
     * S2S Update for a single record; using Partner WDSL imported
	 */
    webservice static void patch(Id localId) {   
        List<Account> acct = new List<Account>();
        String sql = 'SELECT FF_Account_Id__c,Name,Phone,Website,Type,Industry,AccountNumber,Description,FAX,Rating FROM Account WHERE ID=:localId';
        acct = Database.Query(sql);
        
        partnerSoapSforceCom.Soap con = new partnerSoapSforceCom.Soap();
              
        partnerSoapSforceCom.LoginResult loginResult = con.login('login@user.name', 'PasswordSeCuRiTyTOkeN');    

        
        con.SessionHeader = new partnerSoapSforceCom.SessionHeader_element();
        con.endpoint_x = loginResult.ServerUrl;
        System.debug(LoggingLevel.ERROR, '@@@ con.endpoint_x: ' + con.endpoint_x );
        con.Sessionheader.sessionid = loginResult.sessionid;
        
        List<sobjectPartnerSoapSforceCom.sObject_x> sObjects = new List<sobjectPartnerSoapSforceCom.sObject_x>();           
        sobjectPartnerSoapSforceCom.sObject_x tmpObj = new sobjectPartnerSoapSforceCom.sObject_x();
        for(Account ac:acct)
        {
            tmpObj.type_x = 'Account';
            tmpObj.Id = ac.FF_Account_Id__c;
            tmpObj.Name = ac.Name;            
            tmpObj.Phone = ac.Phone;            
            tmpObj.Website = ac.Website; 

System.debug(LoggingLevel.ERROR, '@@@ ac: ' + ac );
System.debug(LoggingLevel.ERROR, '@@@ acsoap: ' + tmpObj );
            sObjects.add(tmpObj);
        }
        partnerSoapSforceCom.SaveResult[]srs = con.update_x(sObjects);
        String s = '';
        for ( partnerSoapSforceCom.SaveResult sr: srs ) {
            if (!sr.success ) {
                for ( partnerSoapSforceCom.Error e: sr.errors) {
                    s += 'SC: ' + e.statusCode + ' | EM: ' + e.message + ' | fields: ' + (e.fields == null ? '' : String.join(e.fields, '; '));
                }
                throw new WebS2SDMLException(s);
            }
            
        }
    }

Next thing that we might think of is to update multiple records at a time.

/**
     * S2S Update for a multiple records; using Partner WDSL imported
* up means abbreviation of Update-Patch
	 */
    webservice static void up(SObject[] scope) {   
        partnerSoapSforceCom.Soap con = new partnerSoapSforceCom.Soap();
        partnerSoapSforceCom.LoginResult loginResult = con.login('login@user.name', 'PasswordSeCuRiTyTOkeN');    
        
        con.SessionHeader = new partnerSoapSforceCom.SessionHeader_element();
        con.endpoint_x = loginResult.ServerUrl;
        System.debug(LoggingLevel.ERROR, '@@@ con.endpoint_x: ' + con.endpoint_x );
        con.Sessionheader.sessionid = loginResult.sessionid;
        
        List<sobjectPartnerSoapSforceCom.sObject_x> sObjects = new List<sobjectPartnerSoapSforceCom.sObject_x>();           
System.debug(LoggingLevel.ERROR, '@@@ scope.size(): ' + scope.size() );
        System.debug(LoggingLevel.ERROR, '@@@ scope: ' + scope );
        for(Account ac:(Account[]) scope)
        {
            sobjectPartnerSoapSforceCom.sObject_x tmpObj = new sobjectPartnerSoapSforceCom.sObject_x();
            tmpObj.type_x = 'Account';
            tmpObj.Id = ac.FF_Account_Id__c;
            tmpObj.Name = ac.Name;            
            tmpObj.Phone = ac.Phone;            
            tmpObj.Website = ac.Website; 

System.debug(LoggingLevel.ERROR, '@@@ ac: ' + ac );
System.debug(LoggingLevel.ERROR, '@@@ acsoap: ' + tmpObj );
            sObjects.add(tmpObj);
        }
        partnerSoapSforceCom.SaveResult[]srs = con.update_x(sObjects);
        String s = '';
        for ( partnerSoapSforceCom.SaveResult sr: srs ) {
            if (!sr.success ) {
                for ( partnerSoapSforceCom.Error e: sr.errors) {
                    s += 'SC: ' + e.statusCode + ' | EM: ' + e.message + ' | fields: ' + (e.fields == null ? '' : String.join(e.fields, '; '));
                }
                throw new WebS2SDMLException(s);
            } 
            
        }
    }

Finally, let’s make it perfect. In case if we don’t want to change the code each time whenever we have to remove field from connection or add new field pair to connection, then let’s write some custom code which would do whatever we need

/**
     * S2S Update for a single record; using custom code
	 */
    webservice static void uPatchX(Id localId)
    {   
    	Map<String, WebS2SFieldCopy__c> fieldsMap = WebS2SFieldCopy__c.getAll();
        if ( fieldsMap.isEmpty() ) {
            throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c is not setup');
        }
        String SFLName;
        for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
            if ( !String.isBlank( webS2SFieldCopy.SF_Login_Name__c ) ) {
                SFLName = webS2SFieldCopy.SF_Login_Name__c;
                break;
            }
        }
        if ( String.isBlank( SFLName ) ) {
            throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c does not save SF_Login_Name__c ');
        }
        Salesforce_Login__c sfl = [ SELECT Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c WHERE Name = :SFLName LIMIT 1 ];
         
        
        List<Account> acct = new List<Account>();
        String sql = 'SELECT FF_Account_Id__c,' + String.join( new List<String>( fieldsMap.keySet() ), ',') + ' FROM Account WHERE ID=:localId';
        acct = Database.Query(sql);
        
        
        Map<String, String> fieldsValuesMap = new Map<String, String>();
        
		fieldsValuesMap.put('Id', acct[0].FF_Account_Id__c);
        for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
            fieldsValuesMap.put( webS2SFieldCopy.Partner_Field_Name__c, String.valueOf( acct[0].get( webS2SFieldCopy.Name ) ) );
        }
        
 
        String result = updatex('Account', fieldsValuesMap);
        Boolean success = getValueFromXMLString(result, 'success') == 'true';
        if ( !success ) {
            String s = getValueFromXMLString(result, 'message');
            throw new WebS2SDMLException(s);
        }
    }

We use here some generic method

/**
     * Custom code to perform S2S Update for a single record 
	 */
    public static String updatex(String soType, Map<String, String> fieldsMap ) {
        String fieldTemplate = '<{0} xmlns="urn:sobject.partner.soap.sforce.com">{1}</{0}>';
        String fieldValues = '';
        for ( String key: fieldsMap.keySet() ) {
            String value = fieldsMap.get( key );
            fieldValues += String.format(fieldTemplate, new String[]{key, value});
        }
         
        String[] susi = WebS2SServices.login(); 
        System.debug(LoggingLevel.ERROR, '@@@ susy: ' + susi );
        String template = '<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><env:Header><SessionHeader xmlns="urn:partner.soap.sforce.com"><sessionId>{0}</sessionId></SessionHeader></env:Header><env:Body><update xmlns="urn:partner.soap.sforce.com"><sObjects><type xmlns="urn:sobject.partner.soap.sforce.com">{1}</type>{2}</sObjects></update></env:Body></env:Envelope>';
        String endpoint = 'https://test.salesforce.com/services/Soap/u/26.0';
 
       	return makeHTTPPatchCall( susi[0], susi[1], String.format(template, new String[]{susi[1], soType, fieldValues}) );
    }

Method getValueFromXMLString I have borrowed somewhere but refactored it a little bit

private static string getValueFromXMLString(string xmlString, string keyField){
        return (xmlString.contains('<' + keyField + '>') && xmlString.contains('</' + keyField + '>')) ? xmlString.substringBetween('<' + keyField + '>', '</' + keyField + '>') : '';
}

Method makeHTTPPatchCall is almost identical to some method makeHTTPCall borrowed from the internet

private static string makeHTTPPatchCall(string endPoint, String sessionId, string soapBody){
   		Http hLLogin = new Http();
		HttpRequest reqLLogin = new HttpRequest();
		reqLLogin.setTimeout(60000);
		reqLLogin.setEndpoint(endPoint);  
		reqLLogin.setMethod('POST');
		reqLLogin.setHeader('SFDC_STACK_DEPTH', '1');
                reqLLogin.setHeader('Authorization', 'Bearer ' + sessionId );
		reqLLogin.setHeader('SOAPAction','DoesNotMatter'); 
		reqLLogin.setHeader('Accept','text/xml');  
		reqLLogin.setHeader('Content-type','text/xml');    
		reqLLogin.setHeader('charset','UTF-8'); 
		reqLLogin.setBody(soapBody);	
		HttpResponse resLLogin = hLLogin.send(reqLLogin);
		return resLLogin.getBody();
}

Finally, I define two custom exception in my class

public class WebS2SConfigException extends Exception {}
    public class WebS2SDMLException extends Exception {}

Please like if you find this post useful.
Thanks for your attention.

Advertisements
This entry was posted in apex, S2S, SOAP, Webservices, WSDL and tagged , , , , , , . Bookmark the permalink.

One Response to Manual Share of records through SOAP Webservices: Partner WSDL or Enterprise WSDL or Custom Code

  1. Pingback: S2S Connection through SOAP Web Services as solution to FF c2g Accounting bug related to Connection User and Account Trigger logic | patlatus

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s