--- salesforce_api.admin.new.inc	2009-05-26 20:04:38.000000000 -0400
+++ salesforce_api.admin.inc	2009-05-30 02:22:57.000000000 -0400
@@ -174,6 +174,7 @@ function salesforce_api_fieldmap_add_for
   $form_state['redirect'] = SALESFORCE_PATH_FIELDMAPS .'/'. $index .'/edit';
 }
 
+
 /**
  * Creates a new fieldmap in the database and returns its index.
  *
@@ -408,6 +409,124 @@ function theme_salesforce_api_fieldmap_e
 }
 
 /**
+ * Ask salesforce for a list of objects and display a checklist for the user.
+ * Based on user selection, set up or tear down cached/synched Salesforce data.
+ * @TODO make this more user friendly. At the moment it's possible for an admin user to blow away 
+ * their entire local SalesForce cache with a few clicks. This is not necessarily desirable.
+ *
+ * @param string $form_state 
+ * @return void
+ * @author aaron
+**/
+function salesforce_api_admin_object(&$form_state) {
+  $objects = salesforce_api_describeGlobal();
+  $data = salesforce_api_get_types(array('full' => FALSE));
+  $defaults = array_keys($data);
+	
+	if(empty($objects->types)) {
+		drupal_set_message(t('There was an error retrieving the list of SalesForce objects. Please verify that your SalesForce instance is properly configured.'), 'error');
+		return;
+	}
+	
+	$options = array_combine($objects->types, $objects->types);
+	$fields = array('objects' => array(
+    		'#type' => 'checkboxes',
+    		'#title' => 'SalesForce Objects',
+    		'#description' => 'Check the SalesForce objects you would like to synchronize locally.',
+    		'#options' => $options,
+    		'#default_value' => $defaults,
+		  ),
+		'#theme' => 'salesforce_api_object_options',
+		'submit' => array(
+		  '#type' => 'submit', 
+		  '#value' => 'Save'
+		),
+	);
+	return $fields;
+}
+
+/**
+ * FAPI submit handler 
+ **/
+ function salesforce_api_admin_object_submit($form, &$form_state) {
+	$values = $form_state['values']['objects'];
+	$real_types = array();
+	foreach($values as $i => $t) {
+		if(empty($t)) {
+		  $type = $form['objects']['#options'][$i];
+		  db_query("DELETE FROM {salesforce_objects} WHERE name = '%s'", $type);
+		  continue;
+		}
+		$real_types[] = $t;
+	}
+  
+  if(!empty($real_types)) {
+  	$objects = salesforce_api_describeSObjects($real_types);
+  	foreach($objects as $i => $o) {
+  		$id = null;
+  		foreach($o->fields as $j => $field) {
+  			if($field->type == 'id') {
+  				$id = $field->name;
+  				break;
+  			}
+  		}
+  		if(empty($id)) { continue; }
+  		$obj = new SObjectDefinition($o->name);
+      $obj->validate = TRUE;
+      //in this case, if the cache is empty, we want to create a new one.
+      //don't validate in this case.
+  		if(empty($obj->cache)) {
+  		  $obj->validate = FALSE;
+  		}
+  		$obj->primarykey = $id;
+  		$obj->cache = $o;
+  		if(!$obj->save()) {
+  		  $repl = array('@name' => $o->name, '@msg' => $obj->getError());
+  			form_set_error($o->name, 
+  			  t('Unable to update object cache for "@name" at this time: @msg', $repl));
+  			$_SESSION['objects_error'][$o->name] = $o->name;
+  		}
+  	}
+  }
+  
+	if(empty($_SESSION['messages']['error'])) {
+		drupal_set_message(t('SalesForce object cache has been updated.'));
+	}
+}
+
+function salesforce_api_admin_object_settings($form_state, $type) {
+  return array('settings' => array(
+    '#type' => 'markup', 
+    '#value' => 'Placeholder for per-object configuration settings.'
+    )
+  );
+}
+
+/**
+ * Theming function for @see salesforce_api_admin_setup
+ * For locally-cached SF Objects, add a "configure" or "re-map" link next to the checkbox
+**/
+function theme_salesforce_api_object_options($element = null) {
+  if(empty($element['objects']['#options'])) { return drupal_render($element); }
+  $objects = $element['objects'];
+  $options = $objects['#options'];
+  foreach($options as $i) {
+    if(empty($objects[$i]['#value'])) { continue; }
+    $link = l('configure', SALESFORCE_PATH_OBJECT.'/'.$i);
+    if($_SESSION['objects_error'][$i]) {
+      $element['objects'][$i]['#prefix'] = '<div class="error">';
+      $element['objects'][$i]['#suffix'] = '</div>';
+      unset($_SESSION['objects_error'][$i]);
+      $link = l('re-map fields', SALESFORCE_PATH_OBJECT.'/'.$i);
+    }
+    $element['objects'][$i]['#title'] .= ' | ' . $link;
+  }
+  unset($_SESSION['objects_error']);
+  return drupal_render($element);
+}
+
+//End CMT
+/**
  * Demonstrates some of the API functionality through the Salesforce class and
  * fieldmap functionality.
  *
@@ -503,3 +622,257 @@ function salesforce_api_demo($demo = NUL
 
   return $output;
 }
+
+/**
+ * Wrapper class for the object returned from a SFAPI call to describeSObject(s)
+**/
+class SObjectDefinition {
+  //@string (40), the formal, machine-readable name of this SObject
+  var $name;
+  
+  //@string (40), this object's primary SF key
+  var $primarykey;
+  
+  //@timestamp, when was the cache last updated
+  var $modified;
+  
+  //@SObjectDefinition, the local cache
+  var $cache;
+  
+  //@SObjectDefinition, backup copy of the local cache
+  private $_cache;
+  
+  //@bool, whether to validate before save
+  var $validate = TRUE;
+  
+  //@bool, is the cache valid or do we need to remap fields?
+  //this field helps eliminate unnecessary comparisons.
+  //if the cache is invalid once, and hasn't been remapped it's still invalid.
+  //don't bother validating, checking matches, or re-saving when valid = FALSE
+  var $valid = TRUE;
+  
+  //@string, set on error instead of throw'ing
+  protected $error = null;
+
+  /**
+   * Constructor. If you need a plain old SObjectDefinition and don't want to 
+   * try to load a cached definition from the database, specify options['load'] = false.
+   * Otherwise, just specify $name and the constructor will try to load the cache.
+   *
+   * @param string $name : if given, set.
+   * @param array $options : valid keys are:
+   *    @param boolean 'load' : if TRUE, $name must be set. Try to load the SObjectDefinition
+   *                            else, don't load anything.
+   * @return NULL on success or an error string on failure
+  **/
+  function __construct($name = null, $options = array()) {
+    $options = array_merge(array(
+      'load' => TRUE,
+      ), $options);
+    if(empty($name)) { 
+      return; 
+    }
+    $this->name = $name;
+    if(!empty($options['load'])) {
+      $this->load();
+    }
+    //return $this->getError();
+  }
+  
+  /**
+   * Load the SObjectDefinition cache specified by $this->name
+   * Will fail if $this->name is not set.
+   *
+   * @return FALSE on failure, TRUE otherwise
+  **/
+  function load() {
+    if(empty($this->name)) {
+      $this->error = 'Unable to load unnamed '.get_class($this) . ' object.';
+      return FALSE;
+    }
+    $result = db_query(
+	//NOTE: 'modified' was deleted from the Select statement, not sure if this should be corrected at some future point.
+      'SELECT valid, cache, primarykey FROM {salesforce_objects} WHERE name = "%s"', $this->name);
+    $data = db_fetch_array($result);
+
+    if(empty($data)) {
+      $this->error = 'No cache available for '.get_class($this) . ' "'.$this->name.'".';
+      return FALSE;
+    }
+
+    $this->valid = $data['valid'];
+    $this->cache = $this->_cache = unserialize(base64_decode($data['cache']));
+    $this->primarykey = $data['primarykey'];
+    $this->modified = $data['modified'];
+    return TRUE;
+  }
+
+  /**
+   * Write the SObjectDefinition cache locally 
+   *
+   * @TODO : How can we best use Drupal's caching engine for storage?
+   * @return void
+  **/
+  function save() {
+    $this->prepareSave();
+    if($this->validate && (!$this->valid || !$this->validates())) {
+      return FALSE;
+    }
+    if(!$this->beforeSave()) {
+      return FALSE;
+    }
+    
+    //if we're writing, all the validity checks have passed. valid = 1
+    $sql = 'REPLACE INTO {salesforce_objects} (cache, primarykey, name, valid) VALUES ("%s", "%s", "%s", 1)';
+    $result = db_query($sql, $this->encoded_cache, $this->primarykey, $this->name);
+    if($result !== FALSE) {
+      return $this->afterSave();
+    }
+    return FALSE;
+  }
+  
+  /**
+   * Prepare data for saving: 
+   *  serialize and encode the object cache.
+   *  find and set the primary key
+  **/
+  function prepareSave() {
+    $this->encoded_cache = base64_encode(serialize($this->cache));
+    //seems that salesforce has stopped setting a primarykey attribute on its objects,
+    //even though it requires a primarykey for upsert api calls.
+    //This is usually index 0, but we loop anyway.
+    foreach($this->cache->fields as $i => $d) {
+      if($d->soapType == 'tns:ID' && $d->type == 'id') {
+        $this->primarykey = $d->name;
+        break;
+      }
+    }
+  }
+
+  /**
+   * Validate that the current object is ready to be saved.
+   *
+   * @return FALSE if the save should not proceed. TRUE otherwise
+   * @see $this->valid 
+   * @see $this->validate : This function should not be invoked if $this->validate == FALSE
+  **/
+  function validates() {
+    if(!$this->valid) { 
+      $this->error = 'Database cache has been marked invalid.';
+      return FALSE; 
+    }
+    if(empty($this->cache) || !is_object($this->cache)) {
+      $this->error = 'Value to cache is not set or not valid.';
+      return FALSE;
+    } 
+    if(empty($this->name) || !is_string($this->name)) {
+      $this->error = 'Unable to save unnamed or invalidly named '.get_class($this).' object.';
+      return FALSE;
+    }
+    //store a copy of the cache and back it up, cause it's about to get overwritten
+    $this->backup_cache = $this->cache;
+    
+    if(empty($this->_cache)) {
+      $this->load();
+      $this->getError();
+      //if _cache is empty, this is a new object type. no further validation necessary.
+      if(empty($this->_cache)) { 
+        $this->cache = $this->_cache = $this->backup_cache;
+        return TRUE; 
+      }
+    }
+    
+    $this->_encoded_cache = base64_encode(serialize($this->_cache));
+
+    if(serialize($this->backup_cache) != serialize($this->_cache)) {
+      $mismatches = SObjectDefinition::getMismatches($this->_cache, $this->backup_cache);
+      if(!empty($mismatches)) {
+        $this->error = 
+          'Caches do not match. 
+          Data loss will result if object cache is updated. 
+          Please re-map fields before updating object caches.';
+        return FALSE;      
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Do cleanup and validation before saving
+   *
+   * @return TRUE if save should proceed. FALSE otherwise.
+  **/
+  function beforeSave() {
+    return TRUE;
+  }
+  
+  /**
+   * Compare remote and local SObjectDefinitions::fields and return any *local* mismatches.
+   * If there are *remote* mismatches, the additional field will not affect local datastores.
+   *
+   * @param SObjectDefinition $local : the current local cache of the object definition
+   * @param SObjectDefinition $remote : the current remote cache of the object definition
+   * @return an array containing the fields which exist locally but not remotely
+   * @author an un-wed female pyromaniac. just kidding.
+  **/
+  static function getMismatches($local, $remote) {
+    // at this point, we don't actually care if the *entire* cache matches, just "fields"
+    if(empty($local->fields) || !is_array($local->fields)) {
+      return $remote->fields;
+    }
+    
+    $mismatches = $remote_fields = $local_fields = array();
+    foreach($remote->fields as $i => $r_field) {
+      $remote_fields[$r_field->name] = $r_field;
+    }
+    foreach($local->fields as $i => $l_field) {
+      $local_fields[$l_field->name] = $l_field;
+    }
+
+    foreach(array('local_fields' => 'remote_fields', 
+      'remote_fields' => 'local_fields') as $k => $v) {
+      foreach($$k as $name => $l_field) {
+        if(empty(${$v}[$name])) {
+          $mismatches[$name] = $l_field;
+          continue;
+        }
+        $r_field = ${$v}[$name];
+        $r_values = array_values(get_object_vars($r_field));
+        $l_values = array_values(get_object_vars($l_field));
+        $diff = array_diff($l_values, $r_values);
+        if(!empty($diff)) {
+          $mismatches[$name] = $l_field;
+        }
+      }
+    }
+    return $mismatches;
+  }
+  
+  /**
+   * Cleanup after ourselves
+   *
+   * @return void
+  **/
+  function afterSave() {
+    unset($this->encoded_cache);
+    unset($this->_encoded_cache);
+    $this->_cache = $this->cache;
+    $this->valid = TRUE;
+    $this->modified = time();
+    return TRUE;
+  }
+  
+  /**
+   * Return the current error status and reset the $this->error. 
+   * You're advised to use this funciton instead of accessing $this->error directly, 
+   * so that error messages are timely. Otherwise you may be looking at a stale error message.
+   *
+   * @return string $error : the error message that corresponds to the most recent error
+  **/
+  function getError() {
+    $error = $this->error;
+    $this->error = null;
+    return $error;
+  }
+  
+}
