# Saturday, July 10, 2010


Facebook has nearly 500 million users (at the time of this writing) and are poised to transform how customer relationships are acquired, cultivated, and supported.

Consider the evolution of Contact management over the past 50 years:
  • Paper: Write names, addresses, birthdays, and other notes down on paper
  • Rolodex: Everyone trades business cards and keeps a local paper copy
  • PC Software: Rolodex moved to PC (Spreadsheets, Goldmine, Act!)
  • Client / Server Software: Many people in same office share common contact database (Siebel, MS CRM)
  • Software as a Service: Contacts moved to Internet hosted server. Accessible from anywhere (Salesforce.com)
  • Crowd sourced: 3rd parties pooling contact information to improve data quality, keep lists up to date (Plaxo, Jigsaw)
  • Social Networking: Contact maintains singular identity. Always up to date. Control of privacy and disclosure (LinkedIn, Facebook)
The consumer now has more power than ever. Consumers now generate more information about themselves than can be generated by 3rd parties.

Buying a list of leads that may be interested in buying camping gear will have no where near the effectiveness of publishing an ad on Facebook targeted at consumers with a self-identified interest in camping (See The Role of Advertising at Facebook).

Consumers will increasingly defer to their friends recommendations on which restaurants to visit, which shoes to buy, and which cars to drive.

CRM systems no longer contain the master records for Contact information.

Businesses must evolve past collecting and cleansing Contacts and instead collect meta-information that refers to customer-managed online profiles, such as Facebook and LinkedIn, for these resources are now truly authoritative. When there is a conflict between CRM Contact information and an online social network profile, the social profile will be master record.

Websites must evolve to become applications. Web forms soliciting contact information will become a thing of the past. Consumers will not have the patience to do anything more than a single click to identify interest in a product or service.

The video embedded in this blog post (and available here) demonstrates a Cool Sites application integrated with Facebook Connect and Salesforce CRM.
Saturday, July 10, 2010 4:31:06 PM (Pacific Daylight Time, UTC-07:00)
# Thursday, July 08, 2010

No, not this Trigger... keep reading...

Trigger development (apologies to Roy Rogers' horse) is not done on a daily basis by a typical Force.com Developer.

In my case, Trigger development is similar to using regular expressions (regex) in that I often rely on documentation and previously developed code examples to refresh my memory, do the coding, then put it aside for several weeks/months.

I decided to create a more fluent Trigger template to address the following challenges and prevent me from repeatedly making the same mistakes:

  • Bulkification best practices not provisioned by the Trigger creation wizard
  • Use of the 7 boolean context variables in code (isInsert, isBefore, etc...) greatly impairs readability and long-term maintainability
  • Trigger.old and Trigger.new collections are not available in certain contexts
  • Asynchronous trigger support not natively built-in

The solution was to create a mega-Trigger that handles all events and delegates them accordingly to an Apex trigger handler class.

You may want to customize this template to your own style. Here are some design considerations and assumptions in this template:

  • Use of traditional event method names on the handler class (OnBeforeInsert, OnAfterInsert)
  • Maps are used where they are most relevant
  • Objects in map collections cannot be modified, however there is nothing in the compiler to prevent you from trying. Remove them whenever not needed.
  • Maps are most useful when triggers modify other records by IDs, so they're included in update and delete triggers
  • Encourage use of asynchronous trigger processing by providing pre-built @future methods.
  • @future methods only support collections of native types. ID is preferred using this style.
  • Avoid use of before/after if not relevant. Example: OnUndelete is simpler than OnAfterUndelete (before undelete is not supported)
  • Provide boolean properties for determining trigger context (Is it a Trigger or VF/WebService call?)
  • There are no return values. Handler methods are assumed to assert validation rules using addError() to prevent commit.

References:
Apex Developers Guide - Triggers
Steve Anderson - Two interesting ways to architect Apex triggers

AccountTrigger.trigger

trigger AccountTrigger on Account (after delete, after insert, after undelete, 
after update, before delete, before insert, before update) {
	AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size);
	
	if(Trigger.isInsert && Trigger.isBefore){
		handler.OnBeforeInsert(Trigger.new);
	}
	else if(Trigger.isInsert && Trigger.isAfter){
		handler.OnAfterInsert(Trigger.new);
		AccountTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet());
	}
	
	else if(Trigger.isUpdate && Trigger.isBefore){
		handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap);
	}
	else if(Trigger.isUpdate && Trigger.isAfter){
		handler.OnAfterUpdate(Trigger.old, Trigger.new, Trigger.newMap);
		AccountTriggerHandler.OnAfterUpdateAsync(Trigger.newMap.keySet());
	}
	
	else if(Trigger.isDelete && Trigger.isBefore){
		handler.OnBeforeDelete(Trigger.old, Trigger.oldMap);
	}
	else if(Trigger.isDelete && Trigger.isAfter){
		handler.OnAfterDelete(Trigger.old, Trigger.oldMap);
		AccountTriggerHandler.OnAfterDeleteAsync(Trigger.oldMap.keySet());
	}
	
	else if(Trigger.isUnDelete){
		handler.OnUndelete(Trigger.new);	
	}
}

AccountTriggerHandler.cls

 
public with sharing class AccountTriggerHandler {
	private boolean m_isExecuting = false;
	private integer BatchSize = 0;
	
	public AccountTriggerHandler(boolean isExecuting, integer size){
		m_isExecuting = isExecuting;
		BatchSize = size;
	}
		
	public void OnBeforeInsert(Account[] newAccounts){
		//Example usage
		for(Account newAccount : newAccounts){
			if(newAccount.AnnualRevenue == null){
				newAccount.AnnualRevenue.addError('Missing annual revenue');
			}
		}
	}
	
	public void OnAfterInsert(Account[] newAccounts){
		
	}
	
	@future public static void OnAfterInsertAsync(Set<ID> newAccountIDs){
		//Example usage
		List<Account> newAccounts = [select Id, Name from Account where Id IN :newAccountIDs];
	}
	
	public void OnBeforeUpdate(Account[] oldAccounts, Account[] updatedAccounts, Map<ID, Account> accountMap){
		//Example Map usage
		Map<ID, Contact> contacts = new Map<ID, Contact>( [select Id, FirstName, LastName, Email from Contact where AccountId IN :accountMap.keySet()] );
	}
	
	public void OnAfterUpdate(Account[] oldAccounts, Account[] updatedAccounts, Map<ID, Account> accountMap){
		
	}
	
	@future public static void OnAfterUpdateAsync(Set<ID> updatedAccountIDs){
		List<Account> updatedAccounts = [select Id, Name from Account where Id IN :updatedAccountIDs];
	}
	
	public void OnBeforeDelete(Account[] accountsToDelete, Map<ID, Account> accountMap){
		
	}
	
	public void OnAfterDelete(Account[] deletedAccounts, Map<ID, Account> accountMap){
		
	}
	
	@future public static void OnAfterDeleteAsync(Set<ID> deletedAccountIDs){
		
	}
	
	public void OnUndelete(Account[] restoredAccounts){
		
	}
	
	public boolean IsTriggerContext{
		get{ return m_isExecuting;}
	}
	
	public boolean IsVisualforcePageContext{
		get{ return !IsTriggerContext;}
	}
	
	public boolean IsWebServiceContext{
		get{ return !IsTriggerContext;}
	}
	
	public boolean IsExecuteAnonymousContext{
		get{ return !IsTriggerContext;}
	}
}
Thursday, July 08, 2010 2:16:12 PM (Pacific Daylight Time, UTC-07:00)
# Tuesday, June 29, 2010
There are really only 2 tech blogs that I read; TechCrunch.com and ReadWriteWeb.com (RWW). They both provide balanced coverage of the consumer and enterprise markets while remaining objective. You don't get the sense that site sponsors and advertisers are driving the stories. I admire their integrity.

I've been particularly impressed with RWW's coverage on the cloud and the Internet of Things, so I was absolutely thrilled when Alex Williams ran with a story yesterday about Chatter Bot titled "How to Connect an Office Building to an Activity Stream". Check it out! (A hint on the title of this blog :-) )

Tuesday, June 29, 2010 11:32:15 AM (Pacific Daylight Time, UTC-07:00)
# Thursday, June 24, 2010

Chatter Developer Challenge / Hackathon 2010 Roundup

The Chatter Developer Challenge sponsored by Salesforce encouraged Developers to create a wide variety of applications that demonstrate the new Salesforce Chatter API.

The challenge culminated in a Hackthon event on June 22nd 2010 at the San Jose Convention Center where prizes were awarded for various applications.

My entry, Chatter Bot, demonstrated the use of Chatter within a Facility Management application that captured physical world events and moved them to the cloud to produce Chatter feed posts.

Chatter Bot is a system comprised of 4 major components:

  • Arduino board with motion and light sensors (C/C++)
  • Proxy Service (Java Processing.org Environment)
  • Salesforce Sites HTTP Listener (Visualforce/Apex)
  • Facility Management App (Force.com database and web forms)
(Source code to all components available at the bottom of this post)

I was elated to learn a few days before the hackathon that Chatter Bot had been selected as a finalist and I was strongly encouraged to attend. So I packed up Chatter Bot to take the 2 hour flight from Portland to San Jose.

It wasn't until I arrived at the airport that it suddenly dawned on me how much Chatter Bot bares a striking resemblance to a poorly assembled explosive device. Apparently the TSA agent handling the X-Ray machine thought so too and I was taken aside for the full bomb sniffing and search routine.

It crossed my mind to add a bit levity to the situation by making some kind of remark, but I quickly assessed that I was probably one misinterpreted comment away from being whisked off in handcuffs to some TSA lockup room. Ironically, I had no problem with security in San Jose coming back. They must be accustomed to these types of devices in Silicon Valley.

Upon arriving in San Jose, I setup Chatter Bot and configured the San Jose Convention Center (SJCC) as a Building Facility (Custom object) to be monitored.

Several assets were created to represent some rooms within the SJCC.

Finally, the Chatter Bot was associated with a particular room (Asset) through an intersection object called AssetSensors that relates a device ID (usually a MAC address) and an Asset.

Within minutes the motion and light sensors were communicating to the cloud via my laptop and reporting on activity in the Hackathon room.

Given the high quality and functionality of fellow competitors apps, such as the very cool Chatter for Android app by Jeff Douglas, and observations from the public voting, I thought Chatter Bot might be a little too "out of the box" to take a prize. It was a genuinely surreal and surprising moment when I learned Chatter Bot received the grand prize.

Thank you Salesforce for hosting such a great event and thank you to the coop-etition for the encouraging exchange of ideas and feedback during the challenge!

Arduino Sensor

/////////////////////////////
//VARS
//the time when the sensor outputs a low impulse
long unsigned int lowIn;

//the amount of milliseconds the sensor has to be low 
//before we assume all motion has stopped
long unsigned int pause = 5000;

boolean lockLow = true;
boolean takeLowTime;  

int LDR_PIN = 2;    // the analog pin for reading the LDR (Light Dependent Resistor)
int PIR_PIN = 3;    // the digital pin connected to the PIR sensor's output
int LED_PIN = 13;

byte LIGHT_ON    = 1;
byte LIGHT_OFF   = 0;
byte previousLightState  = LIGHT_ON;
int lightLastChangeTimestamp = 0;
unsigned int LIGHT_ON_MINIMUM_THRESHOLD = 1015;
unsigned long lastListStateChange = 0; //Used to de-bounce borderline transitions.

// Messages
int SENSOR_MOTION = 1;
int SENSOR_LIGHT  = 2;

/////////////////////////////
//SETUP
void setup(){  
  //PIR initialization
  pinMode(PIR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(PIR_PIN, LOW);
  
  Serial.begin(9600);
  
  InitializeLED();
  InitializeLightSensor();
  InitializeMotionSensor();
}

////////////////////////////
//LOOP
void loop(){
  
  if(digitalRead(PIR_PIN) == HIGH){
    digitalWrite(LED_PIN, HIGH);   //the led visualizes the sensors output pin state
    if(lockLow){
      //makes sure we wait for a transition to LOW before any further output is made:
      lockLow = false;
      writeMeasure(SENSOR_MOTION, HIGH);
      delay(50);
      digitalWrite(LED_PIN, LOW);   //the led visualizes the sensors output pin state
    }
    takeLowTime = true;
  }

  if(digitalRead(PIR_PIN) == LOW){
    digitalWrite(LED_PIN, LOW);  //the led visualizes the sensors output pin state
    if(takeLowTime){
      lowIn = millis();          //save the time of the transition from high to LOW
      takeLowTime = false;       //make sure this is only done at the start of a LOW phase
    }
    
    //if the sensor is low for more than the given pause, 
    //we assume that no more motion is going to happen
    if(!lockLow && millis() - lowIn > pause){
      //makes sure this block of code is only executed again after 
      //a new motion sequence has been detected
      lockLow = true;
      writeMeasure(SENSOR_MOTION, LOW);
      delay(50);
    }
  }
  
  ProcessLightSensor();
}

boolean InitializeLED(){
  Serial.println("INIT: Initializing LED (should see 3 blinks)... ");
  for(int i=0; i < 3; i++){
    digitalWrite(LED_PIN, HIGH);
    delay(500);
    digitalWrite(LED_PIN, LOW);
    delay(500);
  }
}

//the time we give the motion sensor to calibrate (10-60 secs according to the datasheet)
int calibrationTime = 10;

boolean InitializeMotionSensor(){
  //give the sensor some time to calibrate
  Serial.print("INIT: Calibrating motion sensor (this takes about ");
  Serial.print(calibrationTime);
  Serial.print(" seconds) ");
  for(int i = 0; i < calibrationTime; i++){
    Serial.print(".");
    delay(1000);
  }
  Serial.println(" done");
  Serial.println("INIT: SENSOR ACTIVE");
  delay(50);
}

boolean InitializeLightSensor(){
  Serial.print("INIT: Initializing light sensor. Light on threashold set to ");
  Serial.println(LIGHT_ON_MINIMUM_THRESHOLD);
  Serial.println("INIT: 20 samples follow...");
  for(int i = 0; i < 20; i++){
    int lightLevelValue = analogRead(LDR_PIN);
    Serial.print("INIT: ");
    Serial.println(lightLevelValue);
  }
}

boolean ProcessLightSensor(){
  byte currentState = previousLightState;
  int lightLevelValue = analogRead(LDR_PIN);  // returns value 0-1023. 0=max light. 1,023 means no light detected.
  
  if(lightLevelValue < LIGHT_ON_MINIMUM_THRESHOLD){
     currentState = LIGHT_ON;
  }
  else{
     currentState = LIGHT_OFF;
  }
  
  if(LightStateHasChanged(currentState) && !LightStateIsBouncing() ){
    previousLightState = currentState; 
    
    if(currentState == LIGHT_ON){
      writeMeasure(SENSOR_LIGHT, HIGH);
    }
    else{
      writeMeasure(SENSOR_LIGHT, LOW);
    }
    
    delay(2000);
    lightLastChangeTimestamp = millis();
    
    return true;
  }
  else{
    return false; 
  }
}

boolean LightStateHasChanged(byte currentState){
   return currentState != previousLightState; 
}

//De-bounce LDR readings in case light switch is being quickly turned on/off
unsigned int MIN_TIME_BETWEEN_LIGHT_CHANGES = 5000;
boolean LightStateIsBouncing(){
   if(millis() - lightLastChangeTimestamp < MIN_TIME_BETWEEN_LIGHT_CHANGES){
      return true; 
   }
   else{
      return false; 
   }
}

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; 
char deviceID[ ] = "007DEADBEEF0";
//Format MEASURE|version|DeviceID|Sensor Type|State (on/off)
void writeMeasure(int sensorType, int state){
  Serial.print("MEASURE|v1|");
  
  Serial.print(deviceID);
  Serial.print("|");
  
  if(sensorType == SENSOR_MOTION)
    Serial.print("motion|");
  else if(sensorType == SENSOR_LIGHT)
    Serial.print("light|");
  else
    Serial.print("unknown|");
  
  if(state == HIGH)
    Serial.print("on");
  else if(state == LOW)
    Serial.print("off");
  else
    Serial.print("unknown");
  
  Serial.println("");
}

Chatter Bot Proxy (Processing.org Environment)

import processing.serial.*;

Serial port;
String buffer = "";

void setup()
{
    size(255,255);
    println(Serial.list());
    port = new Serial(this, "COM7", 9600);
}

void draw()
{
  if(port.available() > 0){
    int inByte = port.read();
    print( char(inByte) );
    if(inByte != 10){ //check newline
      buffer = buffer + char(inByte);
    }
    else{
       if(buffer.length() > 1){
          if(IsMeasurement(buffer)){
              postToForce(buffer);
          }
          buffer = "";
          port.clear();
       }
    }
  }
}

boolean IsMeasurement(String message){
  return message.indexOf("MEASURE") > -1;
}

void postToForce(String message){
  String[] results = null;
  try
  {
    URL url= new URL("http://listener-developer-edition.na7.force.com/api/measure?data=" + message);
    URLConnection connection = url.openConnection();

    connection.setRequestProperty("User-Agent",  "Mozilla/5.0 (Processing)" );
    connection.setRequestProperty("Accept",  "text/plain,text/html,application/xhtml+xml,application/xml" );
    connection.setRequestProperty("Accept-Language",  "en-us,en" );
    connection.setRequestProperty("Accept-Charset",  "utf-8" );
    connection.setRequestProperty("Keep-Alive",  "300" );
    connection.setRequestProperty("Connection",  "keep-alive" );
    
    results = loadStrings(connection.getInputStream());  
  }
  catch (Exception e) // MalformedURL, IO
  {
    e.printStackTrace();
  }

  if (results != null)
  {
    for(int i=0; i < results.length; i++){
      println( results[i] );
    }
  }
}

Visualforce Site Chatter Listener

<apex:page controller="measureController" action="{!processRequest}" 
contentType="text/plain; charset=utf-8" showHeader="false" 
standardStylesheets="false" sidebar="false">
{!Result}
</apex:page>

Controller

public with sharing class measureController {
	public void processRequest(){
    	if(Data != null){
    		system.debug('data= ' + Data);
    	}
    	
    	CreateFeedPosts();
    }
    
    private void CreateFeedPosts(){
    	if(AssetDeviceBindings.size() == 0)
    		return;
    	
    	for(AssetSensor__c binding : AssetDeviceBindings){
	    	FeedPost newFeedPost = new FeedPost();
	    	newFeedPost.parentId = binding.Asset__c;
			newFeedPost.Type = 'TextPost';
	        newFeedPost.Body = FeedPostMessage();
	        insert newFeedPost;
    	}
    }
    
    private string FeedPostMessage(){
    	if(AssetDeviceBindings.size() == 0)
    		return '';
    	
    	if(SensorType == 'motion'){
    		if(State == 'on')
    			return 'Motion detected';
    		else
    			return 'Motion stopped';
    	}
    	else if(SensorType == 'light'){
    		return 'Lights turned ' + State;
    	}
    	else
    		return 'Unknown sensor event';
    }
    
    private List<AssetSensor__c> m_assetSensor = null;
    public List<AssetSensor__c> AssetDeviceBindings{
    	get{
    		if(m_assetSensor == null){
    			m_assetSensor = new List<AssetSensor__c>();
    			if(DeviceID != null){
    				m_assetSensor = [select Id, Name, Asset__c, DeviceID__c from AssetSensor__c where DeviceID__c=:DeviceID limit 500];
    			}
    		}
    		return m_assetSensor;
    	}
    }
    
    private integer EXPECTED_MESSAGE_PARTS = 5;
    private integer DATA_MESSAGE_TYPE = 0;
    private integer DATA_VERSION	= 1;
    private integer DATA_DEVICEID	= 2;
    private integer DATA_SENSOR_TYPE= 3;
    private integer DATA_STATE		= 4;
    
    private List<string> m_dataParts = null;
    public List<string> DataParts{
    	get{
    		if(m_dataParts == null && Data != null){
    			m_dataParts = Data.split('\\|');
    		}
    		return m_dataParts;
    	}
    }
    
    public string Version{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_VERSION];
    		}
    		else
    			return null;
    	}
    }
    
    public string DeviceID{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_DEVICEID];
    		}
    		else
    			return null;
    	}
    }
    
    public string SensorType{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_SENSOR_TYPE];
    		}
    		else
    			return null;
    	}
    }
    
    public string State{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_STATE];
    		}
    		else
    			return null;
    	}
    } 
    
    private string m_data = null;
    public string Data{
    	get{
    		if(m_data == null && ApexPages.currentPage().getParameters().get('data') != null){
    			m_data = ApexPages.currentPage().getParameters().get('data');
    		}
    		return m_data;
    	}
    }
    
    private string m_result = '';
    public String Result{
    	get{
    		return 'ok';
    	}
    }
    
    public static testMethod void tests(){
    	Asset testAsset = new Asset();
    	testAsset.Name = 'Test Asset';
    	testAsset.AccountID = [select Id from Account order by CreatedDate desc limit 1].Id;
    	insert testAsset;
    	
    	AssetSensor__c binding = new AssetSensor__c();
    	binding.Name = 'Test Binding';
    	binding.DeviceID__c = '007DEADBEEF9';
    	binding.Asset__c = testAsset.Id;
    	insert binding;
    	
    	measureController controller = new measureController();
    	controller.processRequest();
    	system.assert(controller.Data == null);
    	system.assert(controller.DataParts == null);
    	system.assert(controller.Version == null);
    	system.assert(controller.DeviceID == null);
    	system.assert(controller.SensorType == null);
    	system.assert(controller.State == null);
    	
    	string TEST_MEASURE = 'MEASURE|v1|007DEADBEEF9|motion|on';
    	ApexPages.currentPage().getParameters().put('data', TEST_MEASURE);
    	controller = new measureController();
    	controller.processRequest();
    	system.assert(controller.Data == TEST_MEASURE);
    	system.assert(controller.DataParts != null);
    	system.assert(controller.DataParts.size() == 5);
    	system.assert(controller.Version == 'v1');
    	system.assert(controller.DeviceID == '007DEADBEEF9');
    	system.assert(controller.SensorType == 'motion');
    	system.assert(controller.State == 'on');
    	
    	system.assert(controller.AssetDeviceBindings != null);
    	system.assert(controller.AssetDeviceBindings.size() == 1);
    	system.assertEquals('007DEADBEEF9', controller.AssetDeviceBindings[0].DeviceID__c);
    	system.assertEquals(testAsset.Id, controller.AssetDeviceBindings[0].Asset__c);
    	
    	system.assert(controller.Result == 'ok');
    }
}
Thursday, June 24, 2010 6:03:10 PM (Pacific Daylight Time, UTC-07:00)
# Monday, June 21, 2010
I'm very happy to announce that Cool Sites was released this weekend. Cool Sites provides a gallery of pre-built page templates, plugins, and sites built on Salesforce Sites.

Basic web content management tools and workflows for creating navigation menus and web pages are included. Check it out! http://www.getcoolsites.com


Monday, June 21, 2010 3:43:39 PM (Pacific Daylight Time, UTC-07:00)
# Monday, June 07, 2010

Here's a fun video put together for my Salesforce Chatter Developer Challenge entry.

Monday, June 07, 2010 9:57:05 AM (Pacific Daylight Time, UTC-07:00)
# Friday, May 28, 2010
A true REST interface with full support for HTTP Verbs, status codes, and URIs is currently not available on the Salesforce.com platform. However, a simple REST-like interface for getting objects can be developed using Salesforce Sites, Visualforce, and Apex.

This example uses a free Developer Edition with a Site named 'api' that uses only 2 Visualforce pages named 'rest' and 'error'. The rest page accepts a single parameter named 'soql', executes the SOQL query, and returns a JSON formatted response.



The error page is also used to generically handle all 40x and 50x errors.



The body of the error page returns a simple JSON message that the api is unavailable.
<apex:page contenttype="application/x-JavaScript; charset=utf-8" 
showheader="false" standardstylesheets="false" sidebar="false">
{"status": 500, "error": "api currently not available"}
</apex:page>

The rest Visualforce page (full source at bottom of this post) accepts a SOQL parameter and returns JSON results. To get a list of all Leads with their First and Last names, you'd use the SOQL

select Id, FirstName, LastName from Lead
and pass this query to the REST interface in a GET format such as (example here)
http://cubic-compass-developer-edition.na7.force.com/api?soql=select%20Id,%20FirstName,%20LastName%20from%20Lead

Note that the rest page is defined as the default handler for the site named 'api', so it's not required in the URL.
This simple interface supports any flavor of SOQL, including the WHERE and LIMIT keywords, so you can pass queries like

select Id, FirstName, LastName from Lead where LastName='Smith' limit 20
REST interfaces often assume the unique ID of an object is the last portion of the URL request. This can similarly be achieved with a query like (example here)
select Id, FirstName, LastName from Lead where Id='00QA00000019xkpMAA' limit 1

All of these example queries will only return the Id field by default. To fix this, update the Sites Public Access Settings and grant Read access to the Lead object.





The new URL rewriting feature in Summer 10 provides some additional options the necessary means to implementing a RESTful interface with full support for object URIs and linking.

Visualforce Source Code for rest.page
<apex:page controller="RESTController" action="{!processRequest}" 
contentType="application/x-JavaScript; charset=utf-8" showHeader="false" 
standardStylesheets="false" sidebar="false">
{!JSONResult}
</apex:page>
Apex Source Code for RESTController.cls
public with sharing class RESTController {
	public void processRequest(){
		validateRequest();		
    	if( HasError )
    		return;
    	
    	//Add support for other types of verbs here
    	processGetQuery();
    }
    
    static final string ERROR_MISSING_SOQL_PARAM = 'Bad Request. Missing soql parameter';
    static final string ERROR_SOBJECT_MISSING	 = 'Bad Request. Could not parse SObject name from SOQL';
    static final string ERROR_FROM_MISSING		 = 'Bad request. SOQL missing FROM keyword';
    public void validateRequest(){
    	if(Query == null){
    		errorResponse(400, ERROR_MISSING_SOQL_PARAM);
    	}
    	else if(sObjectName == null){
    		//Force a get of object name property.
    		//Detailed error response should already be logged by sObjectName parser
    	}
    }
    
    public boolean HasError = False;
    private void errorResponse(integer errorCode, string errorMessage){
    	JSONResponse.putOpt('status', new JSONObject.value(errorCode));
    	JSONResponse.putOpt('error', new JSONObject.value(errorMessage));
    	HasError = True;
    }
        
    public void processGetQuery(){
    	Map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(SObjectName).getDescribe().fields.getMap();
    	List<JSONObject.value> objectValues = new List<JSONObject.value>();
    	List<sObject> resultList = Database.query(Query);
 		
    	for(sObject obj : resultList){
    		JSONObject json = new JSONObject();
    		json.putOpt('id', new JSONObject.value( obj.Id ));
    		for(SObjectField field : fieldMap.values() ){
    			try{
    				string f = field.getDescribe().getName();
    				string v = String.valueOf( obj.get(field) );
    				json.putOpt(f, new JSONObject.value( v ));
    			}
    			catch(Exception ex){
    				//Ignore. Field not included in query
    			}
    		}
			objectValues.add(new JSONObject.value(json));
    	}
    	JSONResponse.putOpt('status', new JSONObject.value(200));
    	JSONResponse.putOpt('records', new JSONObject.value(objectValues));
    }
    
    private string m_query = null;
    public string Query{
    	get{
    		if(m_query == null && ApexPages.currentPage().getParameters().get('soql') != null){
    			m_query = ApexPages.currentPage().getParameters().get('soql');
    		}
    		return m_query;
    	}
    }

	static final string SOQL_FROM_TOKEN = 'from ';    
    private string m_sObject = null;
    public string sObjectName{
    	get{
    		if(m_sObject == null && Query != null){
    			string soql = Query.toLowerCase();
    			
    			integer sObjectStartToken = soql.indexOf(SOQL_FROM_TOKEN);
    			if(sObjectStartToken == -1){
    				errorResponse(400, ERROR_FROM_MISSING);
    				return null;
    			}
    			sObjectStartToken += SOQL_FROM_TOKEN.length(); 
    			
    			integer sObjectEndToken = soql.indexOf(' ', sObjectStartToken);
    			if(sObjectEndToken == -1)
    				sObjectEndToken = soql.length();
    			
    			m_sObject = Query.substring(sObjectStartToken, sObjectEndToken);
    			m_sObject = m_sObject.trim();
    			system.debug('m_sObject = ' + m_sObject);
    		}
    		return m_sObject;
    	}
    }
    
    private JsonObject m_jsonResponse = null;
    public JSONObject JSONResponse{
    	get{
    		if(m_jsonResponse == null)
    			m_jsonResponse = new JSONObject();
    		return m_jsonResponse;
		}
		set{ m_jsonResponse = value;}
	}
    
	public String getJSONResult() {
    	return JSONResponse.valueToString();
	}
	
	public static testMethod void unitTests(){
		RESTController controller = new RESTController();
		controller.processRequest();
		system.assertEquals(True, controller.HasError);
		system.assertEquals(True, controller.JSONResponse.has('status'));
		system.assertEquals(400, controller.JSONResponse.getValue('status').num);
		system.assertEquals(True, controller.JSONResponse.has('error'));
		system.assertEquals(ERROR_MISSING_SOQL_PARAM, controller.JSONResponse.getValue('error').str);
		
		controller = new RESTController();
		ApexPages.currentPage().getParameters().put('soql', 'select Id fro Lead');
		controller.processRequest();
		system.assertEquals(True, controller.HasError);
		system.assertEquals(True, controller.JSONResponse.has('status'));
		system.assertEquals(400, controller.JSONResponse.getValue('status').num);
		system.assertEquals(ERROR_FROM_MISSING, controller.JSONResponse.getValue('error').str);
		
		controller = new RESTController();
		ApexPages.currentPage().getParameters().put('soql', 'select Id from Lead');
		controller.processRequest();
		system.assertEquals(False, controller.HasError);
		system.assertEquals('Lead', controller.sObjectName);
		
		Lead testLead = new Lead(FirstName = 'test', LastName = 'lead', Company='Bedrock', Email='fred@flintstone.com');
        insert testLead;
        
        controller = new RESTController();
		ApexPages.currentPage().getParameters().put('soql', 'select Id from Lead where email=\'fred@flintstone.com\'');
		controller.processRequest();
		system.assertEquals(False, controller.HasError);
		system.assertEquals('Lead', controller.sObjectName);
		system.assertEquals(True, controller.JSONResponse.has('records'));
	}
}
Friday, May 28, 2010 12:34:08 PM (Pacific Daylight Time, UTC-07:00)
# Friday, May 14, 2010

What if the buildings you worked in could participate in Salesforce Chatter feeds? What if the products you shipped could automatically create Cases in Salesforce when they needed servicing? More objects are becoming embedded with sensors and gaining the ability to communicate. This is enabling the next major advancement in the cloud; the Internet of "Things".

Force.com provides an ideal platform for sensor data with the ability to relate information in the physical world to native or custom objects. My previous blog post on Chatter highlighted the Salesforce user experience and ability for people to interact in the cloud. This post demonstrates using Force.com and Chatter to capture information from objects in the physical world and posting to Chatter feeds using the Chatter web services API.

This application is a very basic Facility Management app. There is a single custom object named "Building" that is comprised of many "Assets", such as Conference Room, Main Entry, Air Conditioner, and Heating System. (See this application in action in the video at the end of this blog post).

Sensors on these assets report their readings to Salesforce in the form of Chatter FeedPost records so that when someone walks into a conference room, the Asset record for that room is updated with Chatter information to the effect of "Motion detected in Conference Room".

The feed posts appear to be created by a Salesforce User named "Environment Bot". This is essentially an API user account for reporting environmental sensor activity.

People can comment on the bot's posts. For example, building maintenance personnel may notice the temperature increasing in some rooms and post comments like "Hey, is anyone on this? The AC appears to be broken on the 3rd floor". Or, a night watchman may notice movement around the Main Entry after business hours and log some comments about what he noticed on patrol. Chatter Bot also has the ability to post pictures and share them as links to Chatter Feeds.

Because the facility management application is utilizing the Chatter API, other Chatter enabled apps may be used to augment and enhance the application. For example, installing the Chatter Timelines application from Ron Hess provides a nice linear visualization of what sensor events occurred and when.

The Chatter Bot is built using an Arduino Duemilanove electronics prototyping platform with Ethernet shield and a Radio Shack breadboard with motion and light sensors.

The Arduino sketch source code just runs in a loop polling the sensors and then notifies Salesforce via a proxy service when environmental changes are detected.

I initially designed the Chatter bot to talk directly to the cloud, but later discovered there are more benefits in having bots communicate through a proxy service to Salesforce.

Industry applications
There are a number of possible industry applications that can leverage this framework:

  • Continuous Emissions Monitoring (CEM) Systems
  • Home / Business Alarm Systems
  • Shipment / Automobile location tracking
  • Environmental Control Systems
  • Healthcare Biosensors

If you'd like to learn more about interfacing Salesforce with the physical world via sensors, then please vote for my proposed session "The Chatter of Things" for Dreamforce in December 2010 to see Chatter Bot live and in action.

The number of objects far exceeds the number of people and there is great potential in using Force.com to enable the Internet of Things. There are many more enhancements I'll be making to this platform. I look forward to sharing them.

Video
This video provides a brief demonstration (4:44) of the Facility Management Chatter Bot in action.

Friday, May 14, 2010 9:46:00 AM (Pacific Daylight Time, UTC-07:00)
# Thursday, April 29, 2010

The VMForce value proposition:
  1. Download Eclipse and SpringSource
  2. Signup for a Salesforce Development account and define your data model
  3. Write your Java app using objects that serialize to Salesforce
  4. Drag and drop your app onto a VMWare hosted service to Force.com to deploy
The partnership breaks down as:
  1. VMWare hosts your app
  2. Salesforce hosts your database
The 2 are seamlessly integrated so that Java Developers can effectively manage the persistence layer as a black box in the cloud without worrying about setting up an Oracle or MySql database, writing stored procedures, or managing database performance and I/O.

This is all great news for Java Developers. It's yet another storage option on the VMWare cloud (I'm assuming VMWare remains fairly agnostic beyond this relationship and Force.com becomes one of many persistence options to Spring Source developers).

For larger organizations already using Salesforce but developing their custom Java apps, this opens up some new and attractive options.

Existing Salesforce Developers may have wondered if Java would replace Apex and Visualforce, prompting a Salesforce blog post aptly titled "In case you were wondering...". In short, "no". Apex and Visualforce will continue to evolve and be the primary platform for developing Salesforce native apps. I personally will continue to use Apex and Visualforce for all development when the data is stored in Salesforce unless compelling requirements come along to use VMForce (most likely that have particular DNS, bandwidth, or uptime needs).

So why the partnership between VMWare and Salesforce? When Salesforce announced Apex back in 2007 it was met with broad acceptance, but some common criticisms were:
  • Why another DSL (Domain Specific Language)?
  • Why can't I leverage my existing Java skills to write business apps?
  • Salesforce is written in Java. Can I upload my existing Java apps to the cloud?
These criticisms were coupled with some looming 800 pound Gorillas in the room (Amazon and VMWare) pushing virtualization as the basis for cloud computing while Salesforce promoted the non-virtualized, multi-tenant path to cloud computing.

They both can't be right. Or can they? CIO's are being bombarded with virtualization as a viable cloud computing solution, so I think Salesforce has wisely taken a step back and taken a position that says "We do declarative, hosted databases better than anyone else. Go ahead and pursue the virtualization path for your apps and leverage our strength in data management as the back end".

Over time, the bet is that VMForce customers will also discover the declarative configuration tools for form-based CRUD (Create/Read/Update/Delete) apps can meet the rapid prototyping and development needs of most any line of business apps.

For object-oriented developers, Salesforce provides a persistence layer that meets or exceeds any ORM (Object Relational Mapping) or NoSQL solution. The impedance mismatch between objects and relational databases is widely known, and VMForce solves this problem very elegantly.

I think other ORM Developer communities, such as Ruby on Rails Developers, will appreciate what is being offered with VMForce, prompting some to correctly draw parallels between VMForce and Engine Yard.

In my experience working with Azure, I cannot emphasize enough how difficult it was to work through the database and storage aspects on even the most simplest application design. Sure, C# is a dream to work with (compared to both Java and Apex) and ASP.NET works well enough for most applications, but Microsoft leaves so many data modeling and storage decisions to the Developer in the name flexibility, which ultimately means sacrificing simplicity, reliability, and in some cases scalability.

Some final thoughts, observations and questions on VMForce:
  • Are there any debugging improvements when using VMForce relative to Apex/VF?
  • The connection between VMWare and Salesforce is presumably via webservices and not natively hosted in the same datacenter. Does this imply some performance and latency tradeoffs when using VMForce? (Update: No. Per the comment from David Schach, the app VM is running in the same datacenter as the Force.com DB)
  • Licensing: No idea what the pricing will be. Will there be a single biller or will Developers get separate invoices from VMWare and Salesforce for bandwidth/computing and storage?
  • It strikes me as quite simple to develop Customer/Partner portals or eCommerce solutions in Java that skirt the limitations of some Salesforce license models when supporting large named-user/low authentication audiences. Will Salesforce limit the types and numbers of native objects that can be serialized through VMForce?
  • Will VMForce apps be validated and listed on the AppExchange? If so, will they be considered hybrid or native? What security review processes will be enforced?
  • Why only the teaser? Ending a great demo with "and it should be available sometime later this year" just seemed deflating. I think Business Users and Developers respond to this kind of promotion much differently. It would be far better to leave Developers with some early release tools in hand immediately after the announcement and capitalize on the moment. Business Users, however, can be shown Chatter, and other future release features, to satiate their long term interests.

Update:Jesper Joergensen has an excellent blog post that answers many of these questions. Thanks Jesper!

Thursday, April 29, 2010 2:38:34 PM (Pacific Daylight Time, UTC-07:00)
# Tuesday, March 09, 2010

If you just want the high level summary, I can spare you the time of reading this lengthy blog article and summarize Chatter in the following image.

Salesforce Chatter is basically Facebook for the enterprise and one of the greatest things to come along since sliced bread (besides Jack Bauer). Chatter is a collaboration platform that supports status publishing and the ability to follow people and objects (Salesforce records).

After seeing a Tweet with instructions to email iwantchatter@salesforce.com to participate in the pilot program, I contacted Salesforce and got on the waiting list. I executed some standard legal agreements (Chatter is still considered pre-launch) and Chatter was enabled in our Salesforce org within a couple days. I would suggest "selling" your org in the body of your pilot program request with facts that might help the already overwhelmed Salesforce staff determine which clients might make the best case studies for using Chatter.

Chatter enables the new UI theme, which I've been requesting for several weeks since the launch of Spring '10. Awesome news since this was not available with the initial Spring 10 rollout.


Setting expectations with users.
I was the eager admin excited to get my hands on new features, then it dawned on me that other users might have questions about the change. In a company of < 10 users, this is no big deal. But I'm guessing a larger org may want to do a more methodical rollout.

After enabling Chatter I sent out an email to everyone simply stating "This is going to rock. If you've used Facebook, then you'll understand what the new feature is about. There's also a new theme activated."

Some Salesforce admins on Twitter have suggested just enabling the new UI, setting off the fire alarm as a distraction, then running out of the building. Whatever works! :-) My feeling is that there should be no delay enabling the new UI. The majority of users will love it.


Email Alerts
One feature that really stands out is the ability to receive an email alert whenever certain events occur. I think this is a smart move on Salesforce's part. Each user has the new ability to enable/disable email alerts under Personal Setup "My Chatter Settings".

As much as Google Wave, Wikis, and other social business software may promote the benefits of replacing email with collaboration platforms, it's just never really panned out. There are just too many Outlook and Gmail users out there with investments in email filters and routing rules for driving business process. I left these features enabled (the default setting).

Based on past experience, I had a concern that Chatter emails might eventually overwhelm my inbox (which I have a particular GTD obsession for managing), so before proceeding any further I created a GMail label and filtering rule specifically for Chatter.

Now all emails from "Salesforce Chatter" automatically get tagged and sorted into their own folder in GMail. The equivalent can be easily accomplished in Outlook Rules. This might be a good tip for Salesforce Admins to share in their Chatter rollout email.


Chatter Settings and Feed Tracking

Administrators can define which objects are enabled for Chatter collaboration and which fields on those objects will trigger automatic Chatter updates.

This is a very simple and easy to use 2 panel user interface with Objects on the left and fields on the right. You select which fields will trigger a Chatter alert when modified. The left panel has an excellent UI element that tells you how many fields are being tracked on that particular object, so you don't have to drill down to each object one at a time to identify feed tracking hot spots.

If you've worked with object history tables in Salesforce, you'll be familiar with what this interface is providing. Now with Chatter, in addition to logging history changes, you're also posting messages to the Chatter stream. History table and Chatter feeds are 2 completely separate features, athough they are semantically the same.

Some objects had feed tracking enabled by default. Most did not. Of the ones enabled, they had 2-5 fields already pre-selected. I could not discern any particular pattern as to how or why certain defaults were configured. I'd say the defaults look "balanced" and it does appear that someone put some thought into a reasonable amount of feed traffic on frequently used CRM object/field combinations. There is a "Restore Defaults" link in the right panel of each object. Clicking these restores the defaults.


People and Profile Tabs
One final Administrative step is to add the People and Profile tabs to your main applications. Just as you can view/manage your profile and find your friends in Facebook, Chatter provides Profile and People tabs to accomplish similar tasks.

Chatter will work without these tabs, but users will only be able to incrementally discover other people who comment on particular objects. I added these 2 tabs to all our applications to get the full benefit of Chatter and apply some consistency in the UI. The People tab provides a list view of all "Colleagues" within the Salesforce Org. The Profile tab allows users to define how they appear to other people; including photo, status, and description.

The "Update Photo" feature with image cropper file is probably one of the first features Chatter users will use on the Profile page.

I found it interesting that I could, as a System Administrator, edit other peoples profiles. That initially struck me as "big brother-ish" since I'm so accustomed to passively using social media platforms, and not actually administrating them. The Chatter Profile pages also contain a link to the existing User Detail page template, which I know Admins will appreciate.

One thing I really like about Chatter is that Salesforce didn't complicate the configuration by providing a full access control list (ACL) wrapper with specific Read/Write permissions per object. If you can view a object, you can jump right in and chime in on Chatter without wondering if you only have read only permissions to watch what other people are saying, but not be able to contribute yourself.

Granted somebody at some time likely raised the concern "But what if some CEO only wants employees to read his status messages and not comment on them?". I'm glad Salesforce resisted that level of access permissions in Chatter.


Following
The first introduction to "Following" will likely be on the People page where users are given the opportunity to subscribe to what particular people are posting as their status message. In such a small org, such as ours, you can follow everyone with just a few clicks. But it made me wonder if a "Follow All" button might be handy for larger orgs.

Chatter uses what is commonly referred to as an "asymmetric follow" architecture. In other words, I can follow you but you don't necessarily have to follow me. This is how Twitter works. Facebook, however, uses a symmetric system where we must both mutually agree to be friends to follow each others posts and activities.

It makes sense Salesforce would not want to use Facebook's symmetric following because it's assumed right out of the box all users are colleagues in a single organization. You only need to decide which colleagues activity you want in your stream.

Maybe one day when Chatter is enabled in a Salesforce-to-Salesforce configuration it may be beneficial to limit who is following your activity (for example, would Michael Dell want all his suppliers following his Chatter simply because a SForce-to-SForce bridge was enabled? I'd guess not.... but only Michael can answer that question).

I can see dialogues taking place in the workspace along the lines of "Yeah, I track that industry pretty closely. Follow me on Chatter if you want more information".


Using Chatter
I never really used the Salesforce Home page for much more than reviewing my Tasks lists. 99% of my time in Salesforce has aways been working in records. But that now changes with Chatter since the Home page is the central hub for aggregating all the people and content you are following. The home page is now "the business stream" and the potential opportunity for exploiting its power is huge.


(Note about Screenshot: Yes, Chatter can be pretty boring when you're the first person using it. Fortunately, I have the StanBot API User to keep me company (future post) until adoption catches on with the others :-) )

The first time you drill down on any record details with Chatter feeds enabled you're prompted with some next step options and the option to view a 2 minute video on Chatter.

As developer, we can all appreciate the detail that goes into not only developing a new feature, but also deflecting support calls and questions with simple, easy to understand tutorials and documentation. I give Salesforce 5 out of 5 stars here.

Chatter is so well designed and so very similar to Facebook and other social apps, that I'd be surprised if 80% of Salesforce users couldn't click on "Close", skip the tutorials, and figure out most of Chatter on their own.


Collaboration and Development In The Cloud
While there are many cool features in Chatter, the fact that this platform is hosted in the cloud and can be extended to include pretty much any web service into the business stream is what makes it so powerful. There is no software to install, any Admin can setup Chatter in just a few minutes, and collaboration is baked into the platform as a core feature (ie there's no additional license fee to use Chatter).

Part 2 of my Chatter review will get into the specifics of Chatter enabling existing Salesforce apps and taking a peek into the Chatter API and new types of apps that can be developed. Stay tuned!

Tuesday, March 09, 2010 1:55:26 PM (Pacific Standard Time, UTC-08:00)