Building a Google Calendar Mash-up with Force.com SOA

Contents

Application overview

Google Apps are becoming a key factor in the overall web application landcape. Increasingly, organizations would like to create mash-ups that integrate data from their own application systems and Google apps. This article describes how to build a mash-up on the Force.com platform using data from Google Calendar.

The key mechanism for getting data from Google Apps are Google Data APIs (also known as GData). Google Data APIs provide protocols for reading and writing data to and from different Google Apps and services. These protocols use standard XML-based syndication formats, which presents a problem for applications looking to combine data from a Google Data API and data from another domain, such as Salesforce. Client browsers have a cross domain scripting restriction, which prevents the browser from communicating with any domain other than the domain that serves the browser page. This restriction would normally prevent a Salesforce page from communication with a Google Data API through JavaScript on the client.

To avoid this problem, Force.com SOA includes a feature called the AJAX proxy. This proxy allows client-side S-Controls to use JavaScript to communicate with third-party websites through the Salesforce.com infrastructure. Since the request from the S-Control intially goes to the Salesforce domain proxy, there is no violation of the cross domain restriction. The next section describes the specifics of using the AJAX proxy.

Using the Salesforce AJAX proxy

The application described in this article uses the Salesforce AJAX proxy to communicate with Google Calendar through a GData API. An S-Control sends web requests to the Google Calendar via the AJAX proxy.

There is a new function in the Salesforce AJAX library that performs this proxy call. The new function name is remoteFunction() and the signature for the function is sforce.connection.remoteFunction( {request object} );

This function, when called from your S-Control, would submit a request to the proxy through a URL such as the following -

https://na1.salesforce.com/services/proxy

The request object is a simple data structure that you construct which contains all the members needed for the proxy to construct an HTTP request on behalf of your client scontrol, such as URL, mimeType, requestHeaders, etc. This type of request is allowed by client browsers as it abides by the same domain security rule that browsers enforce when communicating with servers. You will see several examples of this object in the code samples below.

Once the proxy receives your request, it constructs an outgoing request to the final destination specified , and waits for the response. The timeout for the request is set to 10 seconds, so normal web requests will work well with the AJAX Proxy SOA feature.

Once the response is received, the proxy repackages the response and delivers it back to the S-Control. The remainder of this article will illustrate this process in more detail.

The really cool part of this function is that the response is already parsed into an XML node tree by the AJAX library, so that when you receive a valid response you can simply traverse it as you would any XML DOM document.

In this demonstration code, all the calls from the Salesforce AJAX proxy are being made synchronously. In most production applications, you would make these calls asynchronously to provide a more interactive user experience.

Configuring the Salesforce.com AJAX proxy

The Salesforce.com AJAX proxy must be enabled for an organization. Once the feature is enabled, the next step is to set up the domains you wish to allow the proxy to communicate with. This is done through the standard set up and configuration area of the Salesforce environment.

Image:Proxy settings.jpg

For this application, you would add the Google website as one of the allowed domains.

Image:Remote sites.jpg

Overview of the Google Data API

The Google Data API is described as a REST-based Feed rather than a SOAP-based Web Service, so you will have to construct GET and PUT statements to communicate with Google Data.

The official documentation for Google Calendar Data API is located here ( Google Calendar Data API ). In brief, if you want to read data from Google, you construct a GET HTTP request, with the content of the request being the URI of the requested data. If you want to write to Google, you specify a POST or PUT HTTP request, where the content of the request is the operation you wish to perform.

To list your calendars, you would run :

GET  http://www.google.com/calendar/feeds/myname@gmail.com/private/full


To create a new Google Event, you would execute :

POST http://www.google.com/calendar/feeds/default/private/full

In this POST request, you simply pass the event in the form of an XML Atom. Details on the Google Atom entry format can be found in the code samples below, or on the Google Data API doc.

The Application - Step 1: Login to your Google Account

Before you can get data from any Google Data API, you have to log into Google. This application code uses the AuthSub feature of Google Data API to accomplish this task.

For the purposes of this demonstration application, we store the AuthSub token for the combination of user and calendar service. The S-Control will then use merge field to authenticate with GData APIs. The specifics for Google AuthSub are found Here. This is a very secure method as only Google is presenting the login box, and your login never is stored anywhere in Salesforce, only the session token is stored.

At the highest level, you login to Google, Google comes back to Salesforce with a one time token, you then exchange that token for a long lived token and store it in the user record.

The S-Control includes this one custom merge field to store the google calendar session returned from AuthSub process :

/* 
 * global set from custom merge field on the user object when SControl loads
 */
var sess = "{!$User.GOOG_Cal_Session__c}";

More info on the GData support for these approaches is Here.

The merge field is used to construct all calls to the GData API, but you may be asking by now, how do we obtain that session id the first time?

The process (in detail) goes like this: The SControl presents the Google Accounts login page, found at this address :

https://www.google.com/accounts/AuthSubRequest

In addition, you specify where Google should send the user when authentication is complete, this is done with a name value pair, the name you specify is "next", this is where Google sends the single use token.

Here is the full request, note that the location is basically asking Google to come back to the same page that we are running now (the scontrol itself):

var scontrol_url = window.location.href;
window.location.href = 
 "https://www.google.com/accounts/AuthSubRequest?" + 
 "next=" + escape( scontrol_url ) + 
 "&scope=https%3A%2F%2Fwww.google.com%2Fcalendar%2Ffeeds%2F&session=1&secure=0";

The response from Google comes in the form of a single use token appended to the URL of your SControl, which you can retrieve using the $Request merge field. The code looks like this:

var token = "{!$Request.token}";

Finally, your scontrol can exchange that token for a long lived session token which is bound to the Google account and service ( scope) that you applied for when requesting the first token.

Here is that portion of the scontrol, the second part of the AuthSub process, getting the long lived session id:

var response ;
sforce.connection.remoteFunction( {
 url : this.go + '/accounts/AuthSubSessionToken',
 mimeType: "text/plain",
 requestHeaders: {
    		"Content-Type":"application/x-www-form-urlencoded",	
		"Authorization"  : 'AuthSub token=' + token 
      },
  method: "GET",
  async: false,    // we really need the auth token to proceed, so wait 		
  onFailure : function(resp, req) {
      	throw ('failed login');
  }, 
  onSuccess : function(resp, req) {
      	response = resp;
  }
});

When the Salesforce AJAX Proxy returns the response object from this call, you can use attributes of that object to construct all future calls to the Google Data API. The following JavaScript pattern replacement operation is used to extract the needed data.

this.auth = response.match(/Auth\=.*$/mg)[0].replace(/Auth\=/,'');

Before completing this login process, you must save this session id for use when the user runs another calendar API call. No login will be needed in the future for this user if we store the Auth code. Here is the AJAX to store the Google Session id :

// store this token into the user object
var _tmp = new sforce.SObject('User');
_tmp.Id =  '{!User.Id}';
_tmp.GOOG_Cal_Session__c = this.auth;
sforce.connection.update([_tmp]);

The Auth token can also be revoked, there is code in the AppExchange package to perform that operation.

Step 2: List your Google Calendars

Now that your application has successfully been authenticated with Google Accounts, you are ready to make some requests. The first thing you can do is to list your calendars. You could show your calendars with a couple of simple calls, such as

 var myCals = g.getFeed( g.feeds + '/'+username );	
 var entries = g.getEntryList(myCals);

Since this demo application is going to do a bit more than simply list calendars, the application uses a custom defined GoogleCalendar object to receive the results of the getFeed call. A simplified form of the GoogleCalendar object is shown below.

GoogleCalendar = function() {
  this.service = 'cl'; // sheets, calendar or ..
  this.go = 'https://www.google.com';
  this.feeds = this.go + '/calendar/feeds';
  this.auth  = null; 
  this.gsessionid = null;
  // default request headers, most calls use this
  this.requestHeaders =  {
  	"Content-Type":"application/atom+xml",
  	'X-If-No-Redirect':'1'  // HXR will not handle a redirect
  };
  if ( this.auth != null ) { 
    this.requestHeaders.Authorization = 'AuthSub token=' + this.auth;
  }
};


The actual code to retrieve your Google Calendar feed includes additional callbacks for success and failure. This sample does not check every possible error response; instead, errors are logged and an exception is thrown. A production application would make additional efforts here to report common errors to the user.

GoogleCalendar.prototype.getFeed = function (feedspec) {
  var request = null;
  sforce.debug.log('getFeed ( '+ feedspec);
  sforce.connection.remoteFunction( {
    url : this.url( feedspec ),	
    mimeType: "text/xml",
    requestHeaders: this.requestHeaders ,
    method: "GET",
    async: false, 
    onFailure : function fail(response,request) {
	if (sforce.debug.trace) { 
		sforce.debug.log("Failed GData :<br>" + response);
		sforce.debug.log(request.getAllResponseHeaders());
        }
    },
    onSuccess : function(resp,_request) { 
  	request = _request; 
    }
  });
  	
  if ( ! request ) 
  	throw( "could not get feed : "+this.url( feedspec )); 
  return request.responseXML;
};

Step 3: Insert a Google Calendar Event

Now that you have a calendar object that describes all of your calendars, you can start the process of adding an event to a specific calendar. For this application, you can insert an event into the default calendar for your Google account.

To insert an event into a calendar with JavaScript, you will need two things - the data you want to post, constructed into a calendar entry, and the URL to use to post the data into the Google calendar.

The data for the calendar entry will come from the Salesforce Event object. You use the Salesforce API to query the Event object for the data you need. Once the results are returned, you use the newEventEntry() method of the client library to format the data into an XML string that can be used to send the data to the Google calendar.

...
  var Events = sforce.connection.query(
  "Select Id, Subject, Location, ActivityDateTime,ActivityDate, IsAllDayEvent, DurationInMinutes, "+
 "Google_Event_Edit__c, Description, Owner.FirstName,Owner.LastName, Google_Event_Id__c from Event where Id = '{!Event.Id}' limit 1",
	eventCallback );
}
function eventCallback( Events) { 
  var ev=Events.getArray('records')[0];
 ...

 /* function that takes Salesforce Event data
   * and produces the google event xml string
   */
 var eventEntry = g.newEventEntry(ev);

The last part of this task is to format the event information into XML ( Google calendar Atom). The code listed ignores recurring events, but you can extend the sample to handle that option.

/* 
 * create a new entry atom
 * TODO add support for recurring events
 */
GoogleCalendar.prototype.newEventEntry = function( event_sobject ) {
  var startTime = event_sobject.ActivityDateTime;
  var end_date  = event_sobject.getDateTime('ActivityDateTime');
  end_date.setMinutes( event_sobject.DurationInMinutes ) ;
  var endTime = dateTimeToStringUTC( end_date	);
	
  var desc = event_sobject.Description; 
  if (!desc) desc = "";
	
  var eventEntry = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>" +
	"<title type='text'>"+ event_sobject.Subject + "</title>" +
	"<content type='text'>"+event_sobject.Description+"</content>" +
	"<gd:when startTime='" + startTime +  "' endTime='" +endTime + "'></gd:when>" +
	"<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'></gd:transparency>"+
	"<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'></gd:eventStatus>" +
	'</entry>';	
  return eventEntry;
} 

You now have the calendar event data ready to be sent to Google. The next step is to construct the call to remoteFunction() that will access Google via the Salesforce AJAX proxy.

You would use the following function to the Javascript client library to post an event -

 var xml = g.postEntry( eventEntry );

The complete code used to do the actual task of sending the calendar event through the Google API follows -

GoogleCalendar.prototype.postEntry = function(eventEntry) {
  var request_return ;
	
  sforce.connection.remoteFunction( {
     url : this.url ( this.feeds + "/"+username + "/private/full"  ),
     mimeType: "text/xml",
     requestHeaders: this.requestHeaders,
     method: "POST",
     requestData: eventEntry,
     async: false,  
     onFailure : function fail(response, request) {
  	request_return = request;
  	if ( request.status == "201" ) { 
	// for a post, the expected response from Google is 201
  	// which winds us up here
  	  if (sforce.debug.trace) {
  	    sforce.debug.log( 'code : '+  request.status + ' ' + request.statusText );			
  	  }
  	} else if ( request.status == "500" ) {
	    if (sforce.debug.trace) {
		sforce.debug.log('could not contact google, please try again later...');
	    }
  	} else { 
  		throw ('error : '+request.status + ':' + request.responseText ); 
  	} 			
     },
     onSuccess : function(response, request) { 
  		request_return = request; 
     }
  });
  	
  // this response can be used to locate the newly created event by finding the 
  // entry containing the 'id' of this event
  if ( ! request_return ) { 
	throw ('error : '+feedspec + '\n' + envelope );
  }
  return   request_return.responseXML ?  
		request_return.responseXML : request_return.status; 
};


When this code succeeds (status==201), the event has been added to Google.

The responseXML that is returned from Google (via the Proxy) can be easily parsed for a few key values, such as 'self', 'alternate', and 'edit'. These values represent the different URLs needed to access the Google event.

The JavaScript GData Client library contains a routine to extract these URLs. The g.link() function simply walks the XML node tree of the response object looking for the specified node. The results of this function are used in the following code to add the desired values to custom fields on your Salesforce Event object.

   // update the event record with the google id
   var ev = new sforce.SObject('Event');
   ev.Id =  '{!Event.Id}';
   ev.Google_Event_Id__c = g.link(xml,'self');  
   ev.Google_Event_Html__c = g.link(xml,'alternate');  
   ev.Google_Event_Edit__c = g.link(xml,'edit');
   sforce.connection.update([ev]);

And with this, you have managed to integrate your Salesforce event into your Google calendar. A copy of the event lives in both places, and you have all the information you need to reach out to the specific Google calendar event that matches a specific Salesforce Event for future interaction. You have built a Mash-up, you have taken your Salesforce CRM data and mashed it through the Google Data APIs, into Google Calendar - a giant leap forward for On Demand SOA !

Step 4: Delete Google Calendar Events

At this point, you have created a complete application, but you may have ended up like me with duplicate events in your Google calendar, due to running the code repeated to test it. I decided to add code to the demo application to perform the necessary deletions. I chose to delete any existing event and create a new one to avoid duplicates, rather then the smarter/cleaner method of updating the event, for two reasons. First, I thought it would be easier, and second, I wanted to find out if the Salesforce AJAX Proxy feature would allow me to send a method of "DELETE".

It turns out that DELETE is not supported through the Proxy as a method. The Google Data API will, however, accept a special HTTP header (X-HTTP-Method-Override) to specify a method override, this informs Google that DELETE is the desired operation. I found this detail through a bit of research on the Google Data developer boards and by asking for help from one of the Google Experts, Jeff Ragusa. Jeff writes for the Google Enterprise Blog and appeared with me at the Salesforce Developer Day May 21 2007.

The entire method is shown below.

GoogleCalendar.prototype.deleteEvent = function(feed) {
  var request_return ; 

  var deleteHeaders = this.requestHeaders; 
  deleteHeaders['X-HTTP-Method-Override'] = 'DELETE';
	
  sforce.connection.remoteFunction( {
     url : this.url( feed ),
     mimeType: "text/plain", // for some reason delete does not return xml?
     requestHeaders: deleteHeaders,

     method: "POST",
     requestData: "ts=false", // must have some content	
     async: false,  
     onFailure : function fail(response, request) {
  	request_return = request;
  	if ( request.status == "201" ) { 
	   // for a post, the expected response from google is 201
  	   // which winds us up here
  	   if (sforce.debug.trace) {
  		sforce.debug.log( 'code : '+  request.status + ' ' + request.statusText );			
  	   }
  	} else if ( request.status == "500" ) {
	   if (sforce.debug.trace) {
		sforce.debug.log( 'code : '+  request.status + ' ' + request.statusText );			
  		sforce.debug.log('could not contact google, please try again later...');
          }
  	} else { 
  		//throw ('error : '+request.status + ':' + request.responseText ); 
  	} 			
    },
    onSuccess : function(response, request) { 
  		sforce.debug.log('success 200');
  		request_return = request; 
    }
  });
  delete (deleteHeaders['X-HTTP-Method-Override']); // we are so done with this
	
  return request_return; 
};

There are two things to note about the code above. I have to remove the override when I'm done, since I added it to a global variable via the pointer to this.requestHeaders, rather than a local variable. Secondly, for some reason, this delete funtion does not return XML, rather a plain text message.

You would need to implement an update function with the Google Data API in a similar fashion, with the X-HTTP-Method-Override="PUT".

Enough reading. You can install this code and review it in detail using the provided AppExchange link below, or just run the test drive and look at the code there.

Enjoy.

AppExchange Download

To see the full source code, you can install the following AppExchange package, Google Calendar Mash-up demo. Note: You will not be able to run the code until your organization has been enabled with the Force.com SOA AJAX Proxy feature. This feature is currently scheduled for early July, 2007 availability.

This S-Control was initially demonstrated at the recent Salesforce.com Developer Conference May 21 in Santa Clara, where I discovered that more testing under IE7 was required. That testing is complete and this code has been updated to work under IE7.

If you find other problems please post here, include the environment and solution if you know it.

Thanks !

Update: This app now uses AuthSub process to achieve security login with Google, no passwords are stored!


--Ron Hess 13:55, 13 June 2007 (PDT)