Recipes by Category

App Distribution (2) Bundle logic, interface and services for distribution. App Logic (37) The Apex programming language, workflow and formulas for logic. Collaboration (6) The Salesforce Chatter collaboration platform. Database (29) Data persistence, reporting and analytics. Integration (33) Web Service APIs and toolkits for integration. Security (9) Platform, application and data security. Tools (4) Force.com tooling User Interface (36) Visualforce MVC and metadata-drive user interfaces. Web Sites (12) Public web sites and apps with optional user registration and login.
Beta Feedback
Cookbook Home » Scheduling an Apex Call From the Command Line

Scheduling an Apex Call From the Command Line

Post by Olivier_Nepomiachty  (2011-06-20)

Status: Unverified
Level: advanced

Problem

You want to execute an Apex method or an Apex batch from the command line, regardless of platform, using Perl and cURL. You may want to do this because:
  • Customer IT rules compliance: all the scheduled batches should be centralized.
  • Integration: a customer needs to execute an Apex batch (e.g. after a daily data load.)
  • Customer IT does not have resources with the required skills to write and maintain an application that will deal with Force.com Web Service APIs.

Solution big picture

This is a very simple solution that might be easily integrated in a shell script. The advantages are: no sweat for customer IT and the new capability of launching heavy processes running on the Force.com side.

Technical solution in a nutshell

A shell script calls a cURL command that deals with the Force.com API. cURL posts a SOAP login message to Force.com and opens a session. Then, cURL calls your Apex method exposed as a web service.

The use case we will walk through

Let's assume that your customer has a nightly batch that imports new leads (with an ETL for example), but the phone numbers format does not meet his CTI requirements, and so the system cannot match the inbound calls. We need to apply a filter on the phone numbers. The process stands in a simple string replacement using a regular expression: we will suppress all the none numerical characters with the pattern [^\d]*.

Solution deep dive

Why Perl?

Why Perl rather than Ruby or PHP? Well, Perl is a popular interpreted script language that is usually preinstalled on any Unix/Linux systems. Perl provides powerful text processing facilities without the arbitrary data length limits, facilitating easy manipulation of text files. (see http://en.wikipedia.org/wiki/Perl )

Which Perl for your system?

Why cURL?

cURL is open source and stands for "Client for URLs". It is a command line tool for transferring data with URL syntax, supporting HTTP, HTTPS, FTP, SMTP, LDAP, etc. cURL handles cookies, HTTP headers, forms, all you need to mimic an internet browser. cURL main site, cURL download page.
  • Unix/Linux: build cURL from the sources (compile with openssl), download a package for your distribution (or use apt-get)
  • Windows: Get a version that support the HTTPS protocol

In action

The Perl script performs the following actions:
  • sends a login SOAP call to Force.com
  • parses the response and get the session id
  • sends a SOAP call that will execute the Apex command

We're assuming you have a Lead object in your org, and you've added a custom field:

  • Name: phone cti
  • API Name: phone_cti__c
  • type: text(50)
  • Read: all profiles
  • Write: only the System Administrator

Apex Batch

This batch makes a copy of the Lead phone field to the phone_cti__c field and applies a filter that removes all the none numeric characters. This is just some dummy code written for this recipe and not a CTI best practice! Here is the Apex code, test method is nested in the class:
/*
BatchLeadPhones Blp = new BatchLeadPhones();
ID batchprocessid = Database.executeBatch(Blp);
System.Debug('####batchprocessid='+batchprocessid);
*/
global class BatchLeadPhones implements Database.Batchable<sObject>{
    public String query;
    global database.querylocator start(Database.BatchableContext BC){
        if ((query==null) || (query=='')) query='Select phone_cti__c From Lead';
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope){
        List<Lead> Leads = new List<Lead>();
        for(sObject s : scope){
                Lead l = (Lead)s;
                String ph = l.phone_cti__c.replaceAll('[^\\d]*','');
                if (ph != l.phone_cti__c) {
                    l.phone_cti__c = ph;
                    Leads.Add(l);
                }
        }
        if (Leads.Size()>0) update Leads; 
    }
    
    global void finish(Database.BatchableContext BC){
    }
    
    // test method
    static testMethod void test_BatchLeadPhones() {
        Lead l = new lead(LastName='lead test 123', phone_cti__c='+1 (555)123-4567', company='test company 456');
        insert l;
        Test.StartTest();
        BatchLeadPhones Blp = new BatchLeadPhones();
        Blp.query = 'Select phone_cti__c From lead Where ID=\''+l.Id+'\'';
        ID batchprocessid = Database.executeBatch(Blp);
        Test.StopTest(); 
    }
}

Calling the Batch from the System Log window, run on all leads:
BatchLeadPhones Blp = new BatchLeadPhones();
ID batchprocessid = Database.executeBatch(Blp);
System.Debug('####batchprocessid='+batchprocessid);
Run on a single lead:
BatchLeadPhones Blp = new BatchLeadPhones();
Blp.query = 'Select phone, phone_cti__c From lead Where ID=\'00QA000000GCVof\'';
ID batchprocessid = Database.executeBatch(Blp);
System.Debug('####batchprocessid='+batchprocessid);
Calling the Batch from a web service: This is the Apex class exposed as a webservice:
global class callBatches {
    WebService static String CallBatchLeadPhones() {
        BatchLeadPhones Blp = new BatchLeadPhones();
        ID batchprocessid = Database.executeBatch(Blp);
        return batchprocessid;
    }

    // Test Method
    TestMethod static void test_CallBatchLeadPhones() {
        Lead l = new lead(LastName='lead test 123', phone_cti__c='+1 (555)123-4567', company='test company 456');
        insert l;
        String ID = callBatches.CallBatchLeadPhones();
        System.assert(ID != null);
    }
}

Now you need the WSDL file.

Go to Setup | App Setup | Develop | Apex Classes. On the row of the class callBatches, click on the link "WSDL" to download the WSDL. This XML file describes how to call the web service.

As I guess you are not fluent in WSDL, I recommend that you install SOAPUI on your system. SOAPUI is available as a free edition for gnu/Linux, Windows and Mac OS X; binary and source code. This recipe is not a SOAPUI tutorial; I will assume that you are familiar with this fantastic tool. You might want to provide a partner WSDL to SOAPUI and get the SOAP message to open a session in Force.com. Here is the body:

login.xml
<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Body>
<n1:login xmlns:n1="urn:partner.soap.sforce.com">
<n1:username>user@domain.com</n1:username>
<n1:password>password+token</n1:password>
</n1:login>
</env:Body>
</env:Envelope>
Replace the username, password and token by your own credentials. Force.com will return a SOAP message containing informations regarding the user settings and the organization. The session id appears here:
<sessionId>00DA0000000AXPZ!AQYAQC1_3X1zuSc47y75CU5a4omSypSox6Bg.j.hIsGDBv9hnc7b9ZAD.98ZST3jYxwqoY5TyF4VR7YDUxfWn.ZmeDnoY1Nv</sessionId>
Try this call using cURL. Open a shell:
curl --insecure --silent https://login.salesforce.com/services/Soap/u/21.0 -H "Content-Type: text/xml;charset=UTF-8" -H "SOAPAction: login" -d @login.xml > loginresponse.xml
The SOAP response is saved to loginresponse.xml.

Remark: the --insecure parameter tells cURL not to check the peer. By default, cURL is always checking. See this page to understand how to work with certificates:

Create a new SOAPUI project with your Apex class WSDL, fill the "?" parameters. The set of values is documented in the WSDL file.

The SOAP message should look like:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cal="http://soap.sforce.com/schemas/class/callBatches">
   <soapenv:Header>
      <cal:AllowFieldTruncationHeader>
         <cal:allowFieldTruncation>true</cal:allowFieldTruncation>
      </cal:AllowFieldTruncationHeader>
      <cal:DebuggingHeader>
         <!--Zero or more repetitions:-->
         <cal:categories>
            <cal:category>Apex_code</cal:category>
            <cal:level>Debug</cal:level>
         </cal:categories>
         <cal:debugLevel>Debugonly</cal:debugLevel>
      </cal:DebuggingHeader>
      <cal:CallOptions>
         <cal:client></cal:client>
      </cal:CallOptions>
      <cal:SessionHeader>
         <cal:sessionId>00DA0000000AXPZ!AQYAQC1_3X1zuSc47y75CU5a4omSypSox6Bg.j.hIsGDBv9hnc7b9ZAD.98ZST3jYxwqoY5TyF4VR7YDUxfWn.ZmeDnoY1Nv</cal:sessionId>
      </cal:SessionHeader>
   </soapenv:Header>
   <soapenv:Body>
      <cal:CallBatchLeadPhones/>
   </soapenv:Body>
</soapenv:Envelope>
Force.com will answer:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://soap.sforce.com/schemas/class/callBatches">
   <soapenv:Header>
      <DebuggingInfo>
         <debugLog>21.0 APEX_CODE,DEBUG
23:04:07.035|EXECUTION_STARTED
23:04:07.035|CODE_UNIT_STARTED|[EXTERNAL]|01pA0000002mr28|callBatches.CallBatchLeadPhones
23:04:07.035|METHOD_ENTRY|[1]|01pA0000002mr28|callBatches.callBatches()
23:04:07.035|METHOD_EXIT|[1]|callBatches
23:04:07.042|METHOD_ENTRY|[6]|01pA0000002meNJ|BatchLeadPhones.BatchLeadPhones()
23:04:07.042|METHOD_EXIT|[6]|BatchLeadPhones
23:04:07.042|CONSTRUCTOR_ENTRY|[4]|01pA0000002meNJ|<init>()
23:04:07.042|CONSTRUCTOR_EXIT|[4]|<init>()
23:04:07.042|METHOD_ENTRY|[5]|Database.executeBatch(APEX_OBJECT)
23:04:07.085|METHOD_EXIT|[5]|Database.executeBatch(APEX_OBJECT)
23:04:07.090|CODE_UNIT_FINISHED|callBatches.CallBatchLeadPhones
23:04:07.090|EXECUTION_FINISHED</debugLog>
      </DebuggingInfo>
   </soapenv:Header>
   <soapenv:Body>
      <CallBatchLeadPhonesResponse>
         <result>707A000000Cj9dKIAR</result>
      </CallBatchLeadPhonesResponse>
   </soapenv:Body>
</soapenv:Envelope>

Solution Perl Script

The Perl script is making the same actions.
  • login call (login.xml)
  • get the session id
  • insert the session id in the Apex method web service call
  • call the Apex method (request1_tpl.xml)
request1_tpl.xml is a template SOAP call. The session Id is represented by:
<cal:sessionId>#ID#</cal:sessionId>

Perl is replacing #ID# by the value of the session ID and saves the file to request1.xml.

Remember to update the instance name on line 16 (na1, na2, eu1, etc.)

#!/usr/bin/perl -w
use strict;

system('> loginresponse.xml');
system('curl --insecure --silent https://login.salesforce.com/services/Soap/u/21.0 -H "Content-Type: text/xml;charset=UTF-8" -H "SOAPAction: login" -d @login.xml > loginresponse.xml');
open (FILE, 'loginresponse.xml') or die ('cannot read loginresponse.xml');
my $xml=''; while(<FILE>) { $xml.=$_; } close FILE;
if ($xml=~m|<sessionId>([0-9a-z!_\.-]+)</sessionId>|i) {
	my $sessionid=$1;
	open (FILER, 'request1_tpl.xml') or die ('cannot read request1_tpl.xml');
	$xml=''; while(<FILER>) { $xml.=$_; } close FILER;
	$xml=~s/#ID#/$sessionid/;
	open (FILEW, '> request1.xml') or die ('cannot write to request1.xml');
	print FILEW $xml;
	close FILEW;
	system('curl --insecure --silent https://eu1-api.salesforce.com/services/Soap/class/callBatches -H "Content-Type: text/xml;charset=UTF-8" -H "SOAPAction: SessionHeader" -d @request1.xml > response1.xml');
}
exit;

Remark: the command "system('> loginresponse.xml');" is not working on a Windows system. Replace it by "system('echo . 2> loginresponse.xml');"

That's it - command line invocation of web services using Perl and cURL.

Notes

This code isn't very pretty - especially having to handle the SOAP parsing. Force.com now supports OAuth, and using that to establish an authenticated session (via the username/password flow). The REST API might also help pretty up the code!

Resource files:

code

Share

Recipe Activity - Please Log in to write a comment

Be the first to comment.

X

Vote to Verify a Recipe

Verifying a recipe is a way to give feedback to others and broaden your own understanding of the capabilities on Force.com. When you verify a recipe, please make sure the code runs, and the functionality solves the articulated problem as expected.

Please make sure:
  • All the necessary pieces are mentioned
  • You have tested the recipe in practice
  • Have sent any suggestions for improvements to the author

Please Log in to verify a recipe

You have voted to verify this recipe.