Unloading Salesforce CRM Data with Outbound Messages

No software application is an island, and Salesforce provides two great mechanisms for getting data in and out of your org. To get bits in, there’s the Data Loader and other tools that use the same APIs. To get data out, Salesforce provides a system for pushing outbound messages to an external web service.

Whenever data is edited, or some other internal event occurs, a Salesforce workflow can queue an outbound message to another system.

While there are several off-the-shelf tools for loading data, implementing an outbound message listener is still a custom affair that requires database and web development skills. If you don’t mind exposing an end-point to pass data to a stored procedure, then read on ..

There are plenty of devils in the details, but here’s the basic 1-2-3-4:

1. A workflow rule queues an outbound message.
2. The outbound message contains a bundle of fields from one of your Salesforce objects.
3. A web service endpoint (that you create) receives the message and updates an external system (that you control).
4. Optionally, the service responds and updates the original recordThe linch pin is step 3. While there are tools that simplify writing web services, you still need to write some code. The Salesforce API Developers Guide provides an example of a simple listener class, but it’s really a bit too simple.

classMyNotificationListener : INotificationBinding
{
publicnotificationsResponse notifications(notifications n)
{
notificationsResponse r = newnotificationsResponse();
r.Ack = true;
return r;
}
}

The notificationsResponse method is the end-point for a SOAP web services call. The message bundle is passed over as a “notification”. If records are being updated in bulk (say by a data loader), Salesforce may include up to 100 notifications in a single SOAP envelope. A better example of a notificationResponse would be:

classMyNotificationListener : INotificationBinding
{
publicnotificationsResponse notifications(notifications n)
{
foreach (AccountNotification message in n.Notification)
{
// … handle message …

}
notificationsResponse r = newnotificationsResponse();
r.Ack = true;
return r;
}
}

Something to consider when handling a message is that Salesforce may send the same message multiple times. Each message has an ID, and if handling the same message twice would be a bad thing, you should cache the ID and check it before processing. Also, in real life, we should catch exceptions and log errors.

log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
foreach (AccountNotification message in n.Notification)
{
sf = message.sObject;
try
{
// – Have we met? –
if (HttpRuntime.Cache[message.Id] != null)
{
log.Warn(string.Format(“Warn: SKIPPING (sf.id=\”{0}\”) in message (id=\”{1}\”)”, sf.Id, message.Id));
continue;
}
lock (cacheLock)
{
// The cache will only last as long as the App Pool
HttpRuntime.Cache.Insert(message.Id, sf.Id, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromHours(2));
log.Info(string.Format(“Info: PROCESSING (sf.id=\”{0}\”) in message (id=\”{1}\”)”,sf.Id, message.Id));
}

// … handle message …
}
catch (Exception ex)
{
// Catch any exceptions raised in foreach loop
String errorMessage = ex.Message + “ — “ + ex.StackTrace;
log.Error(String.Format(“Error: EXCEPTION (sf.id=\”{0}\”, error=\”{1}\””, sf.Id, ex.Message + “ — “ + ex.StackTrace));
}
} // end foreach loop

When developing an outbound message service, you should test it under maximum load. What will happen if someone uses the Apex Dataloader to touch every record in an object, potentially creating an outbound message for every record? If that happens, Salesforce is going to collect the bundles into envelopes of 100 records each, and send them down to your web service as quickly as it can. If your service can’t keep up, Salesforce will try again later, in increasing intervals, for up to 24 hours.

Under “Setup/Monitor/Outbound Messages”, Salesforce provides a display where you can watch the records pass through in real time. If in your tests, you see Salesforce binding up, it’s a good idea to queue the records, and process them at your system’s pace.

For .NET, one approach would be to spread-out the work using a ThreadPool, but a better approach can be to use a Producer/Consumer Queue. (The example code is written for .NET 3.5. A 4.0 implementation may differ. See C# 4.0 in a Nutshell for more examples and background information.)

public notificationsResponse notifications(notifications n)
{
log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
if (myQueue == null) lock (queueLock)
{
// Double-check pattern
if (myQueue == null) myQueue = new AccountQueue();
}
log.Info(“Info: Enqueuing and acknowledging notifications”);
myQueue.EnqueueTask(n);
notificationsResponse r = new notificationsResponse();
r.Ack = true;
return r;
}

// Producer/Consumer Queue to throttle account upserts notifications
// and prevent swamping of server during DataLoader updates.
// http://www.albahari.com/threading/part2.aspx#_WaitHandle_Producer_Consumer_Queue
class AccountQueue : IDisposable
{
EventWaitHandle _wh = new AutoResetEvent(false);
Thread _worker;
readonlyobject _locker = newobject();
Queue _tasks = new Queue();
public AccountQueue()
{
_worker = new Thread(Work);
_worker.Start();
}
publicvoid EnqueueTask(notifications task)
{
lock (_locker) _tasks.Enqueue(task);
_wh.Set();
}
publicvoid Dispose()
{
EnqueueTask(null); // Signal the consumer to exit.
_worker.Join(); // Wait for the consumer’s thread to finish.
_wh.Close(); // Release any OS resources.
}
void Work()
{
while (true)
{
notifications task = null;
lock (_locker)
if (_tasks.Count > 0)
{
task = _tasks.Dequeue();
if (task == null) return;
}
if (task != null)
{
doWork(task); // Handle message here
}
else
_wh.WaitOne(); // No more tasks - wait for a signal
}
}
}

static readonly private object cacheLock = newobject();
static public void doWork(notifications n) {
// … code from prior example …
}

Along the way, we slipped in a reference to Log4Net. Configuring Log4Net is outside the scope of this article, but you can dig into the gory details at logging.apache.org. Highly recommended!

While our non-trivial example has grown quickly, it does provide the backbone for a robust Listener that can keep pace with Salesforce, even when you are stuck with a pokey API or under-powered server.

In a followup blog, we close the loop by showing how a listener can respond back to Salesforce.

Learn. Salesforce. Now.

The job market for qualified Salesforce workers is booming, and as the economy recovers, it can only grow hotter. If you’re “transitioning”, now may be a great time to learn Salesforce and add another skill to your resume.

While there is no substitute for work experience, Salesforce offers a dazzling array of self-service learning tools. Even better, you can create a free development org and practice what you learn. Salesforce dev orgs expire after a year, and have some performance restrictions, but they are otherwise equivalent to a production org. Tracking your transition efforts can be great way to practice a new skill and step up your job search at the same time.

A great starting point is to target certification. By studying for the exam, you expose yourself to the depth and breadth of the Salesforce platform. (See my certification blog for more.)

If you are a visual learner, YouTube is brimming with juicy Salesforce videos. Everyone might not be able to fly to SFO for DreamForce, but anyone can enjoy the presentations online, from the comfort of a local Internet connection.

For old-school learners, Salesforce.com For Dummies, Salesforce.com Secrets of Success, Salesforce Handbook, and Development with the Force.com Platform, are a fantastic foursome. And, if you’d like to keep it all on the cloud, all four are also available for the Amazon Kindle.

They say when one door closes, another one opens. Today is the first day of Spring, and it could be a great day to open up to the many opportunities available on and around Salesforce CRM.

Toggling PDF Mode In a Visualforce Apex Page

Salesforce.com has made it easy to blend custom Visualforce pages into a standard Salesforce CRM site. Using the standard stylesheets and a smattering of markup, we can cobble up a custom Account, Contact, or Opportunity page, with an absolute minimum of effort.

It’s also easy to construct a wholly custom page resemble a native Salesforce CRM page. While the page looks great, it may not be printer-friendly, and users may have trouble sharing the output with their non-Salesforce brethren. One solution is to render the pages as a very-printable PDF. Salesforce.com has also made the PDF option blazingly easy. Just include a “renderas=PDF” attribute at the top of your page and – voilà – instant PDF.

Of course, we don’t want to maintain two versions of the page, or give either our pretty or printable versions, and so the next thing is be able to switch between the HTML and PDF versions.

Happily, there is a venerable blog posting that provides a great leg up, but I ran into an issue where I wanted to offer another option on top of PDF/HTML.

Aside from PDF printing, we also wanted to support two flavors of the same database query. One version aggregated rows with a status column set to either “Value A” or “Value B”, and a second version aggregated rows set to “Value C”. Our case was different, but a common case might a report that showed “Cold or Warm” leads, and another that showed only “Hot” leads.

Bottom line is that we wanted to present the page as either HTML or PDF, using either Query A or Query B, and keep all the markup and source code together.

Here’s my solution:


// and the backend


/**

  • Computes and transforms data for use by the Campaign Profile page.
    */
    public class Foo_Ctrl {

    // PDF parameter.
    private final static String PDF = ‘p’;

    // HTML parameter.
    private final static String MODE = ‘m’;

    // Stores Account to use with the Campaign Profile.
    private Account myAccount = null;

    // Stores set of query constraints based on Mode parameter.
    // “AND My_Mode__c IN :myMode”
    private Set myMode = null;

    // Assembles and returns URI reflecting current parameter settings.
    private String doEnable(Boolean doPDF, Boolean doRollup) {

    String parameters = '/apex/Foo?';
    if (myAccount!=null) parameters += 'id=' + MyID;
    if (doPDF) parameters += '&' + PDF + '=true';
    if (doRollup) parameters += '&' + MODE + '=true';
    return parameters;
    

    }

    /**

    • Stores current or desired state “Status”.
      */
      public boolean IsModeB { get; set; }

      /**

    • Reflects absence of “Rollup” parameter.
      */
      public boolean IsModeA {
      get {return !IsModeB;}
      set {IsModeB = !value;}
      }

      /**

    • Handles runtime “PDF” parameter ‘p’.
      */
      public boolean IsPDF { get; set; }

      /**

    • Reflects absence of “PDF” parameter.
      */
      public boolean IsHTML {
      get {return !IsPDF;}
      set {IsPDF = !value;}
      }

      /**

    • Returns current account ID, or null.
      */
      public Id MyId {
      get {return (myAccount==null) ? null : myAccount.Id;}
      }

      /**

    • Returns updated URI including PDF parameter,
    • and the current value of MODE parameter.
      */
      public String doEnablePDF {
      get{return doEnable(true,IsModeB);}
      }

      /**

    • Returns updated URI excluding PDF parameter,
    • and the including current value of MODE parameter.
      */
      public String doEnableHTML {
      get{return doEnable(false,IsModeB);}
      }

      /**

    • Returns updated URI including MODE parameter,
    • and the current value of PDF parameter.
      */
      public String doEnableModeB {
      get{return doEnable(IsPDF,true);}
      }

      /**

    • Returns updated URI excluding MODE parameter,
    • and including the current value of PDF parameter.
      */
      public String doEnableModeA {
      get{return doEnable(IsPDF,false);}
      }

      /**

    • Constructs the controller object, capturing the current Account and year.
      */
      public Foo_Ctrl(ApexPages.StandardController controller) {
      if (controller.getId() == null) {
      ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, 'Id is required'));
      
      } else {
      if (myAccount == null) myAccount = (Account) controller.getRecord();
      
      }
      // Instatiate the rest, to avoid runtime errors …
      IsPDF = (ApexPages.currentPage().getParameters().get(PDF)==’true’);
      IsModeB = (ApexPages.currentPage().getParameters().get(MODE)==’true’);
      myMode = new Set();
      if (IsModeB) {
      myMode.add('Status B');
      myMode.add('Status C');
      
      } else {
      myMode.add('Status A');
      
      }
      }
      }

// And, of course, a test suite

@IsTest
public class NU_TEST_Foo_Page {

static Account myAccount = null;
static Foo_Ctrl myCtrl = null;

static private void mySetUp() {
Test.setCurrentPageReference(Page.Foo);
// Constructor Scaffolding
if (myAccount==null) {
myAccount = new Account(name=’Alpha Bravo’,
NU_Andar_Account_Number__c=’1’);
insert myAccount;
myCtrl = new Foo_Ctrl(new ApexPages.StandardController(myAccount));
}
}

static testMethod void PDF_Test() {
    mySetUp();
    String nextPage = myCtrl.doEnablePDF;
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '&' + Foo_Ctrl.PDF + '=true',nextPage);
}

static testMethod void HTML_Test() {
    mySetUp();
    String nextPage = myCtrl.doEnableHTML;
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '',nextPage);         
}

static testMethod void ModeB_Test() {
    mySetup();
    String nextPage = myCtrl.doEnableModeB;
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '&' + Foo_Ctrl.MODE + '=true',nextPage);
}

static testMethod void ModeA_Test() {
    mySetUp();
    String nextPage = myCtrl.doEnableModeA;
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '',nextPage);         
}

static testMethod void PDF_ModeB_Test() {
    mySetUp();
    ApexPages.currentPage().getParameters().put(Foo_Ctrl.PDF, 'true');
    Foo_Ctrl ctrlPDF = new Foo_Ctrl(new ApexPages.StandardController(myAccount));
    String nextPage = ctrlPDF.doEnableModeB;
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '&' + Foo_Ctrl.PDF + '=true' + '&' + Foo_Ctrl.MODE + '=true',nextPage);
    System.assertEquals(true,ctrlPDF.isPDF);
    System.assertEquals(false,ctrlPDF.isHTML);
    ctrlPDF.isHTML = true;
    System.assertEquals(false,ctrlPDF.isPDF);
}

static testMethod void ModeB_PDF_Test() {
    mySetUp();
    ApexPages.currentPage().getParameters().put(Foo_Ctrl.MODE, 'true');
    Foo_Ctrl ctrlMode = new Foo_Ctrl(new ApexPages.StandardController(myAccount));
    String nextPage = ctrlMode.doEnablePDF;
    System.assertEquals(true,ctrlMode.isModeB);
    System.assertEquals(false,ctrlMode.isModeA);       
    System.assertEquals('/apex/Foo?id=' + myAccount.Id + '&' + Foo_Ctrl.PDF + '=true' + '&' + Foo_Ctrl.MODE + '=true',nextPage);
}

 static testMethod void PDF_Flag_Test() {
    mySetUp();
    ApexPages.currentPage().getParameters().put(Foo_Ctrl.PDF, 'true');
    Foo_Ctrl ctrlPDF = new Foo_Ctrl(new ApexPages.StandardController(myAccount));
    System.assertEquals(true,ctrlPDF.isPDF);
    System.assertEquals(false,ctrlPDF.isHTML);
    ctrlPDF.isHTML = true;
    System.assertEquals(false,ctrlPDF.isPDF);
}

 static testMethod void Mode_Flag_Test() {
    mySetUp();
    ApexPages.currentPage().getParameters().put(Foo_Ctrl.MODE, 'true');
    Foo_Ctrl ctrlMode = new Foo_Ctrl(new ApexPages.StandardController(myAccount));
    System.assertEquals(true,ctrlMode.isModeB);
    System.assertEquals(false,ctrlMode.isModeA);
    ctrlMode.isModeA = true;
    System.assertEquals(false,ctrlMode.isModeB);
}

}

For more Visualforce PDF trickery, see this nifty blog on May The Force Be With You.

Salesforce Certification - A Win/Win

General knowledge of database or website technologies is invaluable, but at the the end of the day, we implement projects in specific products. A Ph.D. in computer science doesn’t mean someone is a SQL Server 2008 rock star.

A key benefit of working with Salesforce CRM is a top-knotch professional certification program that recognizes individuals who have passed detailed proficiency exams. Salesforce.com offers two distinct certification paths, Administrator and Developer.

Certified Administrators employ broad knowledge of the Salesforce CRM platform. Administrators provide first-tier support for other users, create reports and dashboards, define validations, construct workflows and approval processes, and handle the day-to-day oversight of a Salesforce organization.

Certified Developers evince comprehensive knowledge of the Force.com development platform. Developers create custom objects, database triggers, Visualforce pages, and other custom components. Developers may also work with the web services API to integrate Salesforce CRM with other products.

To prove higher levels of proficiency, Certified Administrators may earn additional credentials as Sales or Service Cloud Implementation Experts, and Certified Developers can go on to earn a Technical Architect credential. For the overachiever, there are also Advanced Administrator and Advanced Developer certifications.

Since Salesforce.com releases significant new enhancements to the platform every four months, like clockwork, we all have to maintain our certifications by taking release-specific exams, for each certification. The maintenance exams are not as grueling as the initial exams, but earning and keeping certifications demonstrates commitment to the Salesforce platform.

At NimbleUser, our Salesforce implementation teams include a mix of Certified Administrators and Certified Developers. Having certified consultants on board helps us provide our customers with the highest possible level of service.

Visit certification.salesforce.com to learn more, or visit the NimbleUser web site to find out more about the peeps.

Managing a Related Object via a Checkbox in Salesforce CRM

This Force.com recipe shows how to use a checkbox to manage a related object via an Apex trigger.

Problem: An off-the-shelf integration requires the existence of a specific Salesforce CRM Opportunity object. When the object is present, the integration acts on the Account related to the Opportunity. When the object is not present. the integration bypasses the Account.

User Story: As a CRM User, I need to easily manage the Opportunity that signals the integration, for example, by selecting a checkbox that creates or removes the related integration object.

Solution: Implement an Account trigger that observes the checkbox and inserts or deletes the related integration object.


// trigger class

/**

  • Manages a2z Opportunity by referring to the
  • “Is a2z Import” checkbox on the Account object.
  • When an a2z Opportunity exists, the Account is imported to a2z.
  • The trigger handles three transitional states:
  • (1) if insert and doImport, insert opp;
  • (2) if update and !doImport and hasOpp, delete opp;
  • (3) if update and doImport and !hasOpp, insert opp.
  • By inference, the trigger also handles:
  • (4) if insert and !doImport, exit;
  • (5) if update and doImport and hasOpp, exit;
  • (6) if !doImport and !hasOpp, exit.
    */
    trigger NU_a2zCreateOpportunity on Account (after insert, after update) {

    // (1) On insert, if isImport, insert opp
    if (Trigger.isInsert) {

    List delta = new List();
    for (Account a : Trigger.new) {
        if (a.NU_isA2zImport__c) {
            delta.add(NU_a2zOpps.newOpp(a));
        }        
    }
    if (delta.size()>0) insert(delta);
    

    }

    // (2) On update, if not isImport and haveOpp, delete opp
    // (3) On update, if isImport and not haveOpp, insert opp
    if (Trigger.isUpdate) {

    if (Trigger.new.size() == 1) {
        Account a = Trigger.new.get(0);
        Boolean hasOpp = NU_a2zOpps.hasOpp(a);
        if (!a.NU_isA2zImport__c && hasOpp) {
            delete(NU_a2zOpps.getOpp(a)); // (2)
        }  
        if (a.NU_isA2zImport__c && !hasOpp) {
            insert(NU_a2zOpps.newOpp(a)); // (3)
        }    
    
    } else {
        Map opps = NU_a2zOpps.getOpps(Trigger.new);
        Set ids = opps.Keyset();
        Set dels = new Set();
        List deletes = new List();
        List inserts = new List();
        for (Account a : Trigger.new) {
            if (!a.NU_isA2zImport__c && ids.contains(a.Id)) {
                deletes.add(opps.get(a.Id)); // (2)
            }  
            if (a.NU_isA2zImport__c && !ids.contains(a.Id)) {
                inserts.add(NU_a2zOpps.newOpp(a)); // (3)
            }    
        } 
        if (deletes.size()>0) delete(deletes);
        if (inserts.size()>0) insert(inserts);
    }
    

    }
    }


    // test class

/**

  • Exercises the a2zCreateOpportunity feature set.
    */
    @isTest
    private class NU_TEST_a2zCreateOpportunity {

    /**

    • Generates a key based on system time and offset.
    • Sufficient for unit test purposes only.
      */
      static String newCompanyNumber(Integer offset, String kicker) {
      Datetime n = Datetime.now();
      return kicker + String.valueOf(n.minute()) + String.valueOf(n.millisecond()) + String.valueOf(offset);
      }

      /**

    • Generates an account with the NU_isA2zImport__c raised or lowered.
    • Assumes default is false.
      */
      static Account newAccount(Boolean doImport, Integer offset, String kicker) {
      Account a = new Account(Name = ‘Test Account’,

      CompanyNumber__c = newCompanyNumber(offset, kicker));
      

      if (doImport) a.NU_isA2zImport__c = true; // False is default
      return a;
      }

      /**

    • Generates an account with the NU_isA2zImport__c raised or lowered.
    • Assumes default is false.
      */
      static Account newAccount(Boolean doImport) {
      return newAccount(doImport, 0, ‘’);
      }

      /**

    • Inserts the given account, and returns it again from a select.
      */
      static Account doInsert(Account a) {
      insert(a);
      return [
      SELECT id, NU_isA2zImport__c, CompanyNumber__c
      FROM Account
      WHERE id = :a.id
      ];
      }

      /**

    • Exercises “Insert Import”.
      */
      static testMethod void testInsImp(){
      Account a = newAccount(true);
      Account a2 = doInsert(a);
      System.Assert(NU_a2zOpps.hasOpp(a2),’Expected opp on insert.’);
      }

      /**

    • Exercises “Insert NoImport”.
      */
      static testMethod void testInsNoImp(){
      Account a = newAccount(false);
      Account a2 = doInsert(a);
      System.Assert(!NU_a2zOpps.hasOpp(a2),’Expected no opps on insert.’);
      }

      /**

    • Creates, inserts, and updates an account,
    • changing the import settings in between.
      */
      static void updateHelper(Boolean insImp, Boolean updImp) {
      Account a = newAccount(insImp);
      Account a2 = doInsert(a);
      a2.NU_isA2zImport__c = updImp;
      update(a2);
      if (updImp) {

      System.Assert(NU_a2zOpps.hasOpp(a2),'Expected opps on update.');
      

      } else {

      System.Assert(!NU_a2zOpps.hasOpp(a2),'Expected no opps on update.');
      

      }
      }

      /**

    • Exercises “Update ImportNoImport”.
      */
      static testMethod void testUpdImpNoImp(){
      updateHelper(true,false);
      }

      /**

    • Exercises “Update NoImportImport”.
      */
      static testMethod void testUpdNoImpImp(){
      updateHelper(false,true);
      }

      /**

    • Exercises “Update ImportImport”.
      */
      static testMethod void testUpdImpImp(){
      updateHelper(true,true);
      }

      /**

    • Exercises “Update NoImportNoImport”.
      */
      static testMethod void testUpdNoImpNoImp(){
      updateHelper(false,false);
      }

      static List getInserted(List rows) {
      return [ SELECT id, NU_isA2zImport__c, CompanyNumber__c
      FROM Account
      WHERE id in :rows
      ];
      }

      /**

    • Creates, inserts, and updates an account,
      changing the import settings in between, for a batch of accounts. /
      static void batchHelper(Boolean insImp, Boolean updImp,
      Boolean contains, String kicker) {
      List rows = new List();
      for (Integer r=0; r<200; r++) {

      rows.add(newAccount(insImp, r, kicker));
      

      }
      insert(rows);
      List inserted = getInserted(rows);
      // In Apex, anything can be null
      if (updImp != null) {

      for (Account a : inserted) {
          a.NU_isA2zImport__c = updImp;            
      }
      

      }
      update(inserted);
      Set ids = NU_a2zOpps.getOpps(inserted).Keyset();
      Boolean success = true;
      if (contains) {

      for (Account a : inserted) {
          success = success &amp;&amp; ids.contains(a.Id);            
      }
      System.Assert(success,'Expected opps on batch delete.');
      

      } else {

      for (Account a : inserted) {
          success = success &amp;&amp; !ids.contains(a.Id);            
      }
      System.Assert(success,'Expected no opps on batch delete.');        
      

      }
      }

      /**

    • Exercises “Batch has flag set”.
      */
      static testMethod void batchHelperHas() {
      batchHelper(true,null,true,’A’);
      }

      /**

    • Exercises “Batch does not have flag set”.
      */
      static testMethod void batchHelperHasNot() {
      batchHelper(false,null,false,’B’);
      }

      /**

    • Exercises Batch delete.
      */
      static testMethod void batchHelperDel() {
      batchHelper(true,false,false,’C’);
      }

      /**

    • Exercises Batch insert.
      */
      static testMethod void batchHelperIns() {
      batchHelper(false,true,true,’D’);
      }
      }