Interface for invoking Salesforce.com REST calls over SSL with OAUTH authentication. This interface is designed to simplify the interaction between mbed devices and salesforce.com web services.

Dependencies:   HTTPClient-SSL MbedJSONValue

Dependents:   StatusReporter

SalesforceInterface.h

Committer:
ansond
Date:
2014-09-26
Revision:
22:3363752cd523
Parent:
21:e5a4471a46fb

File content as of revision 22:3363752cd523:

/* Copyright C2014 ARM, MIT License
 *
 * Author: Doug Anson (doug.anson@arm.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files the "Software", to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 
 #ifndef _SALESFORCE_INTERFACE_H_
 #define _SALESFORCE_INTERFACE_H_
 
 // Logger
 #include "Logger.h"
 
 // SSL-based HTTP support
 #include "HTTPClient.h"
 
 // JSON parsing support
 #include "MbedJSONValue.h"
  
 // convenience macros
 #define  DEFINE_BUFFER(x)              char x[MAX_BUFFER_LENGTH+1]
 #define  RESET_BUFFER(x)               memset(x,0,MAX_BUFFER_LENGTH+1)
 #define  ALLOC_BUFFER(x)               DEFINE_BUFFER(x);RESET_BUFFER(x)
 
 #define  DEFINE_SML_BUFFER(x)          char x[MAX_SMALL_BUFFER_LENGTH+1]
 #define  RESET_SML_BUFFER(x)           memset(x,0,MAX_SMALL_BUFFER_LENGTH+1)
 #define  ALLOC_SML_BUFFER(x)           DEFINE_SML_BUFFER(x);RESET_SML_BUFFER(x)
 
 // Default salesforce API version (must be in XX.Y format and must be a string)
 #define SALESFORCE_API_VERSION_LENGTH  10
 #ifndef SALESFORCE_API_VERSION
    #define SALESFORCE_API_VERSION      "28.0"
 #endif
 
 // verbose debugging within SalesforceInterface
 #if ENABLE_DEBUG_LOGGING
    #define DEBUG(...)                  { LOG_CONSOLE(__VA_ARGS__); }
 #else
    #define DEBUG(...)                  { ; }
 #endif 
 
 // HTTP Verbs
 typedef enum {
     GET,
     PUT,
     POST,
     DELETE,
     NUM_VERBS
 } HttpVerb;
 
 // Supported input data types for PUT and POST (Defined by HTTPClient-SSL/data support...)
 typedef enum {
     JSON,              // ContentType: application/json
     PLAIN_TEXT,        // ContentType: plain/text
     FORM_MAPPED,       // ContentType: application/x-www-form-urlencoded
     NUM_TYPES
 } InputDataTypes;
 
 // OAUTH structure
 typedef struct {
     bool   valid;
     string id;
     string issued_at;
     string token_type;
     string instance_url;
     string signature;
     string access_token;
 } OauthToken;
 
 /**
 * Salesforce Interface 
 * SalesforceInterface provides a simple C++ API into the REST-based Salesforce.com APIs
 *
 * Example Project: http://mbed.org/users/ansond/code/df-2014-salesforce-testharness-k64f/ 
 *
 * @code
 
 #include "Definitions.h"       // definitions including platform specifics...
 #include "Logger.h"
 
 // include salesforce.com credentials
 #include "sf_creds.h"
 
 // our Serial port
 #include "BufferedSerial.h"
 BufferedSerial pc(USBTX, USBRX);
 
 // Ethernet
 #include "EthernetInterface.h"
 EthernetInterface ethernet;
 
 // HTTP 
 #include "HTTPClient.h"
 HTTPClient http;
 
 // Salesforce.com Interface
 #include "SalesforceInterface.h"
  
 // test case persistence
 char *object_name = NULL;
 char *account_name = NULL;
 char *updated_account_name = NULL;
 char *external_id_field_name = NULL;
 char *external_id_field_value = NULL;
 DEFINE_SML_BUFFER(record_id);
    
 // *************** Test Cases ************************
 
 void Test_getSalesforceToken(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nGetting Salesforce Token...");
     logger->turnLEDPurple();
     
     // get the salesforce token     
     char *id = sf->getSalesforceToken();
     if (id != NULL && strlen(id) > 0)
         logger->log("Saleforce token: %s",id);
     else
         logger->log("Unable to get Saleforce token");
     logger->turnLEDGreen();
 }
 
 void Test_query(Logger *logger,SalesforceInterface *sf,char *query_str) {
     logger->log("\r\n\r\nExecuting test query: %s",query_str);
     logger->turnLEDPurple();
     if (query_str != NULL && strlen(query_str) > 0) { 
         ALLOC_BUFFER(response);
         char *answer = sf->query(query_str,response,MAX_BUFFER_LENGTH);
         if (answer != NULL) logger->log("query result: %s",answer);
         else logger->log("query - NULL result");
     }
     else {
        logger->log("Unable to perform query as we do not have our salesforce token");
     }
     logger->turnLEDGreen();
 }
 
 void Test_create(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting create()");
     logger->turnLEDPurple();
     
     // create a new record
     MbedJSONValue new_record;
     new_record["name"] = account_name;
     
     // DEBUG
     logger->log("Create: new record: %s",new_record.serialize().c_str());
     
     // create...
     MbedJSONValue response = sf->createRecord(object_name,new_record);
     
     // display the result
     char *result = (char *)response.serialize().c_str();
     if (result != NULL && strlen(result) > 0 && strcmp(result,"null") != 0) {
        // save off the token if we succeeded
        logger->log("Create: result: %s",result);
        logger->log("Create: http_code=%d",sf->httpResponseCode());
        RESET_SML_BUFFER(record_id);
        strcpy(record_id,(char *)response["id"].get<std::string>().c_str());
     }
     else {
        // failure
        logger->log("Create: FAILED http_code=%d",sf->httpResponseCode());
     }
     logger->turnLEDGreen();
 }
 
 void Test_read(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting read()");
     logger->turnLEDPurple();
          
     // DEBUG
     logger->log("Read: reading: %s from %s",record_id,object_name);
     
     // read...
     MbedJSONValue response = sf->readRecord(object_name,record_id);
     
     // display the result
     char *result = (char *)response.serialize().c_str();
     if (result != NULL && strlen(result) > 0 && strcmp(result,"null") != 0) {
        // save off the token if we succeeded
        logger->log("Read: result: %s",result);
        logger->log("Read: http_code=%d",sf->httpResponseCode());
     }
     else {
        // failure
        logger->log("Read: FAILED http_code=%d",sf->httpResponseCode());
     }
     
     logger->turnLEDGreen();
 }
 
 void Test_create_external_id(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting create(ExternalID)");
     logger->turnLEDPurple();
     
     // create a new record
     MbedJSONValue new_record;
     new_record[external_id_field_name] = external_id_field_value;
     
     // DEBUG
     logger->log("create(ExternalID): new record: %s",new_record.serialize().c_str());
     
     // create...
     MbedJSONValue response = sf->createRecord(object_name,new_record);
     
     // display the result
     char *result = (char *)response.serialize().c_str();
     if (result != NULL && strlen(result) > 0 && strcmp(result,"null") != 0) {
        // save off the token if we succeeded
        logger->log("create(ExternalID): result: %s",result);
        logger->log("create(ExternalID): http_code=%d",sf->httpResponseCode());
        RESET_SML_BUFFER(record_id);
        strcpy(record_id,(char *)response["id"].get<std::string>().c_str());
     }
     else {
        // failure
        logger->log("create(ExternalID): FAILED http_code=%d",sf->httpResponseCode());
     }
     logger->turnLEDGreen();
 }
 
 void Test_read_by_external_id_and_value(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting read(externalID)...");
     logger->turnLEDPurple();
          
     // DEBUG
     logger->log("read(externalID): reading: %s from %s with value %s",object_name,external_id_field_name,external_id_field_value);
     
     // read (external ID)...
     MbedJSONValue response = sf->readRecord(object_name,external_id_field_name,external_id_field_value);
     
     // display the result
     char *result = (char *)response.serialize().c_str();
     if (result != NULL && strlen(result) > 0 && strcmp(result,"null") != 0) {
        // save off the token if we succeeded
        logger->log("read(externalID): result: %s",result);
        logger->log("read(externalID): http_code=%d",sf->httpResponseCode());
     }
     else {
        // failure
        logger->log("read(externalID): FAILED http_code=%d",sf->httpResponseCode());
     }
     
     logger->turnLEDGreen();
 }
 
 void Test_update(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting update()");
     logger->turnLEDPurple();
     
     // update am existing record - assume "name" is the proper key for the record you wish to update...
     MbedJSONValue changed_record;
     changed_record["name"] = updated_account_name;
     
     // DEBUG
     logger->log("Update: updated record: %s",changed_record.serialize().c_str());
     
     // update...
     bool updated = sf->updateRecord(object_name,record_id,changed_record);
     
     // display the result
     if (updated) {
        // SUCCESS
        logger->log("Update: successful! http_code=%d",sf->httpResponseCode());
     }
     else {
        // failure
        logger->log("Update: FAILED http_code=%d",sf->httpResponseCode());
     }
     logger->turnLEDGreen();
 }
 
 void Test_upsert_external_id(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting upsert(ExternalID)");
     logger->turnLEDPurple();
     
     // update am existing record - assume "name" is the proper key for the record you wish to update...
     MbedJSONValue changed_record;
     changed_record["name"] = updated_account_name;
     
     // DEBUG
     logger->log("upsert(ExternalID): upserted record: %s",changed_record.serialize().c_str());
     
     // Upsert...
     bool updated = sf->upsertRecord(object_name,external_id_field_name,external_id_field_value,changed_record);
     
     // display the result
     if (updated) {
        // SUCCESS
        logger->log("upsert(ExternalID): successful! http_code=%d",sf->httpResponseCode());
     }
     else {
        // failure
        logger->log("upsert(ExternalID): FAILED http_code=%d",sf->httpResponseCode());
     }
     logger->turnLEDGreen();
 }
 
 void Test_delete(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nExecuting delete()");
     logger->turnLEDPurple();
     
     // DEBUG
     logger->log("Delete: deleting: %s from %s",record_id,object_name);
     
     // delete...
     bool deleted = sf->deleteRecord(object_name,record_id);
     
     // display the result
     if (deleted) {
        // SUCCESS
        logger->log("Delete: successful! http_code=%d",sf->httpResponseCode());
     }
     else {
        // failure
        logger->log("Delete: FAILED http_code=%d",sf->httpResponseCode());
     }
     
     logger->turnLEDGreen();
 }
 
 void Test_reset_auth(Logger *logger,SalesforceInterface *sf) {
     logger->log("\r\n\r\nForcing API to reset OAUTH token and Salesforce Token...");
     logger->turnLEDPurple();
     sf->resetSalesforceToken();
     logger->turnLEDGreen();
 }
 
 // *************** Test Cases ************************
 
 // Main Task...
 void mainTask(void const *v) {
        
    // create our object instances 
    Logger logger(&pc,NULL);    
    SalesforceInterface *sf = NULL;
    
    // announce
    logger.log("\r\n\r\nARM Salesforce Interface Testharness v%s",APP_VERSION);
    logger.turnLEDBlue();
    
    // initialize Ethernet
    logger.log("Initializing Ethernet...");
    ethernet.init();
    
    // get a DHCP address and bring the network interface up
    logger.log("Getting IP Address...");
    logger.turnLEDOrange();
    if (ethernet.connect() == 0) {
        // log our IP address (DHCP)
        logger.log("IP Address: %s",ethernet.getIPAddress());
        
        // allocate the Salesforce.com interface
        logger.log("Allocating Saleforce.com interface...");
        sf = new SalesforceInterface(&http,&logger);
        
        // set our Salesforce.com credentials
        sf->setCredentials(username,password,client_id,client_secret);
        
        // *************** BEGIN TEST CASES *****************        

        // configuration for the test cases        
        object_name             = "Account";       // use the account object
        account_name            = "ARM";           // add this record (name)
        updated_account_name    = "ARM Holdings";  // update the existing record's name to this
        external_id_field_name  = "Device__c";     // External ID field name
        external_id_field_value = "ABC123";        // External ID field value
        RESET_SML_BUFFER(record_id);               // buffer for the record's token
        
        // Perform a Create
        Test_create(&logger,sf);
                
        // Perform a Read
        Test_read(&logger,sf);
        
        // Perform a Query
        Test_query(&logger,sf,"SELECT Id,Name FROM Account LIMIT 5");
        
        // Perform an Update
        Test_update(&logger,sf);
                
        // Perform a second Read to visually confirm the update above...
        Test_read(&logger,sf);
        
        // Perform a Create (External ID)
        Test_create_external_id(&logger,sf);
        
        // Perform an Upsert
        Test_upsert_external_id(&logger,sf);
        
        // Perform a read of the external ID'ed specified by a given value
        Test_read_by_external_id_and_value(&logger,sf);
        
        // force the API to re-acquire the OAUTH token and Salesforce Token 
        Test_reset_auth(&logger,sf);
        
        // Perform a Read (should re-acquire the OAUTH token and Salesforce Token)
        Test_read(&logger,sf);
        
        // Perform a Delete
        Test_delete(&logger,sf);
                
        // reset the record token buffer
        // RESET_SML_BUFFER(record_id);
        
        // Perform a Read - should error out
        Test_read(&logger,sf);
                        
        // reset the record token buffer
        RESET_SML_BUFFER(record_id);
        
        // *************** BEGIN TEST CASES *****************      
        
        // entering main loop
        logger.log("All tests complete...\r\nExiting...");
        logger.turnLEDBlue();
        exit(0);
     }
     else {
         logger.log("No Network... Exiting...");
         logger.turnLEDRed();
         exit(1);
     }     
  }
  
  // main entry
  int main() {
     Thread workerTask(mainTask, NULL, osPriorityNormal, STACK_SIZE);
     while (true) {
        Thread::wait(10*WAIT_TIME_MS);
     }
  }

 * @endcode
 *
 */       
 class SalesforceInterface {
    private:
        Logger          *m_logger;
        bool             m_logger_internal;
        HTTPClient      *m_http;
        char            *m_username;
        char            *m_password;
        char            *m_client_id;
        char            *m_client_secret;
        bool             m_have_creds;
        OauthToken       m_oauth_token;
        HTTPResult       m_http_status;
        int              m_http_response_code;
        char             m_http_redirection_url[MAX_BUFFER_LENGTH+1];
        char             m_salesforce_id[MAX_BUFFER_LENGTH+1];
        char             m_salesforce_api[SALESFORCE_API_VERSION_LENGTH];
        char             m_error_buffer[MAX_SMALL_BUFFER_LENGTH+1];
        
    public:
        /**
        Default constructor
        @param http HTTPClient instance
        @param pc optional RawSerial for debugging output. If NULL, there will be no debugging output 
        */
        SalesforceInterface(HTTPClient *http,RawSerial *pc = NULL); 

        /**
        Alternative constructor
        @param http HTTPClient instance
        @param logger optional Logger instance (See Logger for more info). If NULL, there will be no debugging output 
        */
        SalesforceInterface(HTTPClient *http,Logger *logger = NULL); 
        
        /**
        Default destructor
        */
        virtual ~SalesforceInterface();
        
        /**
        Establish salesforce.com credentials
        @param username salesforce.com account user name
        @param password salesforce.com account password. The password must be of the form [password][security token]
        @param client_id salesforce.com connected application "customer key" value
        @param client_secret salesforce.com connected application client secret value
        */
        void setCredentials(char *username,char *password,char *client_id,char *client_secret);
        
        /**
        Get our salesforce.com token
        @param fetch boolean that will direct the interface to fetch the token if not already done (default = true)
        @return our salesforce token in JSON format or NULL if in error
        */
        char *getSalesforceToken(bool fetch = true);
        
        /**
        Force the interface to re-acquire the OAUTH token and salesforce token
        */
        void resetSalesforceToken();
        
        /**
        Set our salesforce.com API version
        @param version integer value (positive)
        */
        void setSalesforceAPIVersion(int version);
        
        /**
        Set our salesforce.com API version
        @param version string value (format "X.Y")
        */
        void setSalesforceAPIVersion(char *version);
        
        /**
        Get our salesforce.com API version
        @return string containing our salesforce.com API version or NULL if in error
        */
        char *getSalesforceAPIVersion();
        
        /**
        Salesforce.com API SOQL QUERY method to invoke ad-hoc SOQL queries into salesforce.com
        @param query_str character string with the SOQL query to invoke
        @param output_buffer allocated result buffer to use
        @param output_buffer_length allocated result buffer length
        @return result of the SOQL query in JSON format or NULL if in error
        */
        char *query(char *query_str,char *output_buffer,int output_buffer_length);
 
        /**
        Salesforce.com API record creation method to create a new record within a salesforce.com object
        @param object_name name of the salesforce.com object to create the record in (i.e. "Account")
        @param record MbedJSONValue json structure that the new record will be comprised with 
        @return MbedJSONValue structure with the results of the creation operation in JSON format
        */
        MbedJSONValue createRecord(char *object_name,MbedJSONValue &record);
        
        /**
        Salesforce.com API record read method to read a record within a salesforce.com object
        @param object_name name of the salesforce.com object to read the record in (i.e. "Account")
        @param record_id salesforce.com id of the record instance to read 
        @param record_value salesforce.com id value of the record instance to read (for external ID usage - default is NULL for non-external IDs)
        @return MbedJSONValue structure with the results of the read operation in JSON format
        */
        MbedJSONValue readRecord(char *object_name,char *record_id,char *record_value = NULL);
        
        /**
        Salesforce.com API record update method to update a record within a salesforce.com object
        @param object_name name of the salesforce.com object to update the record in (i.e. "Account")
        @param record_id salesforce.com id of the record instance to read 
        @param record MbedJSONValue instance with updated data for the record
        @return true - success, false - failure
        */
        bool updateRecord(char *object_name,char *record_id,MbedJSONValue &record);
        
        /**
        Salesforce.com API record upsert (update/insert) method to update a record within a salesforce.com object for External IDs
        @param object_name name of the salesforce.com External object to upsert the record in (i.e. "FooBar_c")
        @param external_id_field_name salesforce.com id of the External record instance to upsert 
        @param external_id_field_value salesforce.com id value of the External record instance to upsert 
        @param record MbedJSONValue instance with updated data for the record
        @return true - success, false - failure
        */
        bool upsertRecord(char *object_name,char *external_id_field_name,char *external_id_field_value,MbedJSONValue &record);
        
        /**
        Salesforce.com API record delete method to delete a record within a salesforce.com object
        @param object_name name of the salesforce.com object to delete the record in (i.e. "Account")
        @param record_id salesforce.com id of the record instance to delete 
        @return true - success, false - failure
        */
        bool deleteRecord(char *object_name,char *record_id);
        
        /**
        Salesforce.com API invocation HTTP response code to aid in debugging error conditions
        @return http response code
        */
        // HTTP Error code access
        int httpResponseCode();
        
        /**
        Retrieve the last executed update, upsert, or delete response error detail
        @return http response code from last update, upsert, or delete operation
        */
        MbedJSONValue getLastError();
                      
 protected: 
        // do we have a valid salesforce token and OAUTH token?
        bool haveSalesforceToken(bool fetch = true);

        // CREATE: a record in Salesforce.com
        char *createRecord(char *object_name,char *json_data,char *output_buffer,int output_buffer_length);
        
        // READ: a specific record in Salesforce.com
        char *readRecord(char *object_name,char *record_id,char *record_value,char *output_buffer,int output_buffer_length);
        
        // UPDATE: a specific record in Salesforce.com
        bool updateRecord(char *object_name,char *record_id,char *json_data,char *output_buffer,int output_buffer_length);
        
        // UPSERT: update/insert a specific External record in Salesforce.com
        bool upsertRecord(char *object_name,char *external_id_field_name,char *external_id_field_value,char *json_data,char *output_buffer,int output_buffer_length);
                            
        // DELETE: delete a specific record in Salesforce.com
        bool deleteRecord(char *object_name,char *record_id,char *output_buffer,int output_buffer_length);
        
        // raw invocation of REST calls into Salesforce.com
        char *invoke(const char *url,char *output_buffer,int output_buffer_length);                                                                                                        // defaults to GET
        char *invoke(const char *url,char *output_buffer,int output_buffer_length,HttpVerb verb);                                                                                          // GET or DELETE with simple output
        char *invoke(const char *url,const char *input_data,const int input_data_len,char *output_buffer,int output_buffer_length);                                                        // defaults to POST with JSON input data type
        char *invoke(const char *url,const InputDataTypes input_type,const char *input_data,const int input_data_len,char *output_buffer,int output_buffer_length);                        // defaults to POST with variable input data type
        char *invoke(const char *url,const InputDataTypes input_type,const char *input_data,const int input_data_len,char *output_buffer,int output_buffer_length,const HttpVerb verb);    // full fidelity method
      
        // get our OAUTH Token
        void checkAndGetOauthToken(bool fetch = true);
        char *getOauthToken(char *output_buffer,int output_buffer_length);
     
        // convenience accessors
        Logger *logger();
        HTTPClient *http();
        OauthToken *oauth();
        HTTPResult httpStatus();
        
        // internal checkers
        bool haveCreds();
        void resetOauthToken();
        void fillOauthToken(char *token);
        bool validOauthToken(bool fetch = true);
        
        // get the specified URL from our Salesforce Token
        char *getSalesforceURL(char *key,char *url_buffer,int url_buffer_length);
       
        // simple char array replacement (modifies input string!)
        void replace(char *str,char orig_char,char new_char);
        
        // needed to replace substrings within std::string
        void replace(string& line, string& oldString, string& newString);
        
        // validate that http status is in the "n" range
        bool httpResponseCodeInRange(int n);
        
        // min() method
        int min(int value1,int value2);
        
        // look for a specific key value pair in a json message
        bool contains(char *json,char *key,char *value);
        
        // initialize
        void init(HTTPClient *http,Logger *logger,bool logger_internal);
 };
 
 #endif // _SALESFORCE_INTERFACE_H_