/**
 * Generic Javascript helpers for CWIS.
 *
 * Part of the Collection Workflow Integration System (CWIS)
 * Copyright 2011 Internet Scout Project
 * http://scout.wisc.edu
 */

cw.provide("CW-Helpers", function(){

if (typeof jQuery == "undefined") {
    cw.error("jQuery is required for the Helpers module");
}

// helper variables
var RouterUrl = cw.getRouterUrl();

// -----------------------------------------------------------------------------
/**
* Modal confirm dialog for a link
*/
$(document).ready(function() {

  $("#dialog-confirm").dialog({
    autoOpen: false,
    modal: true,
    bgiframe: true,
  });

  $(".confirmLink").click(function(e) {
  e.preventDefault();
  var targetUrl = $(this).attr("href");

  $("#dialog-confirm").dialog('option', 'buttons', {
      "Confirm" : function() {
        window.location.href = targetUrl;
      },
      "Cancel" : function() {
        $(this).dialog("close");
      }
    });

    $("#dialog-confirm").dialog("open");
  });

  $("#dialog-confirm").tooltip();
  $(".confirmLink").tooltip();
});

// -----------------------------------------------------------------------------254

/**
 * Facilitates and caches remotely- and ansynchronously-fetched data.
 * @see RemoteData::getData()
 */
function RemoteData() {
  this.initDefaultValues();
  this.clearLocalCache();
}

/**
 * The value used to determine whether or not objects should use the global
 * caches by default.
 * @var bool DefaultUseGlobalCaches
 */
RemoteData.DefaultUseGlobalCaches = false;

/**
 * Clear a specific portion of the global caches if given an identifier or clear
 * all of them if not given one.
 * @param string identifier identifier of a specific portion of the caches
 * @see RemoteData::clearFromGlobalCaches()
 * @see RemoteData::clearLocalCache()
 */
RemoteData.clearGlobalCaches = function(identifier) {
  // clear just a specific portion of the global cache
  if (identifier) {
    delete RemoteData.GlobalCaches[identifier];
  }

  // clear the entire cache
  else {
    RemoteData.GlobalCaches = {};
  }
};

/**
 * Determine if the data identified by the given key is already cached.
 * @param string key data identifier
 * @return true if the data is cached or false otherwise
 * @see RemoteData::getData()
 */
RemoteData.prototype.isDataCached = function(key) {
  var globalCachesIdentifier;

  // need to use the global caches
  if (this.getUseGlobalCaches()) {
    globalCachesIdentifier = this.getGlobalCachesIdentifer();

    // if the data has already been cached
    return RemoteData.GlobalCaches[globalCachesIdentifier] &&
      RemoteData.GlobalCaches[globalCachesIdentifier][key];
  }

  // need to use the local cache
  else {
    return !!this._localCache[key];
  }
};

/**
 * Get data identified by the given key. This method will fetch the data from
 * the cache if possible. Otherwise, it will trigger a remote fetch call.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 * @return the data if it's already cached
 * @see RemoteData::fetchRemoteData()
 * @see RemoteData::isDataCached()
 */
RemoteData.prototype.getData = function(key, callback) {
  var globalCachesIdentifier;

  // need to use the global caches
  if (this.getUseGlobalCaches()) {
    globalCachesIdentifier = this.getGlobalCachesIdentifer();

    // if the data has already been cached
    if (RemoteData.GlobalCaches[globalCachesIdentifier] &&
        RemoteData.GlobalCaches[globalCachesIdentifier][key]) {
      if (callback) {
        callback.call(
          callback,
          key,
          RemoteData.GlobalCaches[globalCachesIdentifier][key]);
      }

      return RemoteData.GlobalCaches[globalCachesIdentifier][key];
    }

    // the data needs to be fetched first
    else {
      this.fetchRemoteData(key, callback);
      return undefined;
    }
  }

  // need to use the local cache
  else {
    // the data has already been cached
    if (this._localCache[key]) {
      if (callback) {
        callback.call(callback, key, this._localCache[key]);
      }

      return this._localCache[key];
    }

    // the data needs to be fetched first
    else {
      this.fetchRemoteData(key, callback);
      return undefined;
    }
  }
};

/**
 * This is a stub method that should contain an implementation in subclasses. It
 * is used to remotely fetch data.
 * @param string key data identifier
 * @param function callback the callback to execute when the data is available
 * @see RemoteData::getData()
 */
RemoteData.prototype.fetchRemoteData = function(key, callback) {
/**
  // use jQuery and AJAX to fetch the data
  jQuery.ajax("path/to/remote-file.php", {
    "data": {...},
    "success": cw.bindObjectMethodCallback(this, this.remoteDataCallback, key, callback),
    "error": cw.bindObjectMethodCallback(this, this.remoteDataError, key)
  });
**/
};

/**
 * Clear the cache for this class from the global caches.
 * @see RemoteData::clearGlobalCaches()
 * @see RemoteData::clearLocalCache()
 */
RemoteData.prototype.clearFromGlobalCaches = function() {
  RemoteData.clearGlobalCaches(this.getGlobalCachesIdentifer());
};

/**
 * Clear the local cache specific to this object.
 * @see RemoteData::clearGlobalCaches()
 * @see RemoteData::clearFromGlobalCaches()
 */
RemoteData.prototype.clearLocalCache = function() {
  this._localCache = {};
};

/**
 * Get the global caches identifier for this object.
 * @return the global caches identifier of this object
 */
RemoteData.prototype.getGlobalCachesIdentifer = function() {
  return this._globalCachesIdentifier;
};

/**
 * Get the value that determines whether or not this object should use the
 * global caches to cache data.
 * @return whether or not this object should use the global caches
 * @see RemoteData::setUseGlobalCache()
 */
RemoteData.prototype.getUseGlobalCaches = function() {
  return this._useGlobalCaches;
};

/**
 * Set the value that determines whether or not this object should use the
 * global caches to cache data.
 * @param bool useGlobalCaches true to use the global caches or false to not
 * @see RemoteData::getUseGlobalCache()
 */
RemoteData.prototype.setUseGlobalCaches = function(useGlobalCaches) {
  this._useGlobalCaches = useGlobalCaches;
};

/**
 * The callback for the method that initiates the remote data fetching process.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 * @param object data the data returned by the remote callback
 * @param string status text status of the error
 * @param jqXHR xhr a superset of the XMLHTTPRequest object
 * @see RemoteData::remoteDataError()
 * @protected
 */
RemoteData.prototype.remoteDataCallback = function(key, callback, data, status, xhr) {
  // cache the data
  this.cacheData(key, data);

  // execute the callback with the data
  callback.call(callback, key, data);
};

/**
 * The callback used to handle errors from the method that initiates the remote
 * data fetching process.
 * @param string key data identifier
 * @param jqXHR xhr a superset of the XMLHTTPRequest object
 * @param string status text status of the error
 * @param string errorThrown the error thrown
 * @see RemoteData::remoteDataCallback()
 * @protected
 */
RemoteData.prototype.remoteDataError = function(key, xhr, status, errorThrown) {
  console.warn("Could not fetch the data for %s", key);
};

/**
 * Cache the given data using the given key as an identifier for accessing it.
 * @param string key identifier for accessing the data
 * @protected
 */
RemoteData.prototype.cacheData = function(key, data) {
  var globalCachesIdentifier;

  // need to cache the data globally
  if (this.getUseGlobalCaches()) {
    globalCachesIdentifier = this.getGlobalCachesIdentifer();

    // initialize the global cache for the identifier, if necessary
    if (!RemoteData.GlobalCaches[globalCachesIdentifier]) {
      RemoteData.GlobalCaches[globalCachesIdentifier] = {};
    }

    // cache the data in the global cache
    RemoteData.GlobalCaches[globalCachesIdentifier][key] = data;
  }

  // need to cache the data locally
  else {
    this._localCache[key] = data;
  }
};

/**
 * Initialize default values for the object.
 * @protected
 */
RemoteData.prototype.initDefaultValues = function() {
  this._useGlobalCaches = RemoteData.DefaultUseGlobalCaches;
};

/**
 * Holds the global caches for remotely-fetched data.
 * @var object GlobalCaches
 * @protected
 */
RemoteData.GlobalCaches = {};

/**
 * The identifier used when caching data in the global caches. It is set for the
 * entire class and for any object that instantiates it. This should not be
 * changed except when defining any subclasses.
 * @var string _globalCachesIdentifier
 * @protected
 */
RemoteData.prototype._globalCachesIdentifier = "RemoteData";

/**
 * Used to determine whether or not to use the global caches when caching data.
 * @var bool _useGlobalCaches
 * @protected
 */
RemoteData.prototype._useGlobalCaches;

/**
 * Used as the local cache.
 * @var object _localCache
 * @protected
 */
RemoteData.prototype._localCache;

// -----------------------------------------------------------------------------

/**
 * Facilitates and caches remotely- and ansynchronously-fetched search results
 * for resource keyword searches.
 * @see RemoteData::getData()
 */
function ResourceKeywordSearchData() {
  ResourceKeywordSearchData.base.call(this);
} cw.extend(ResourceKeywordSearchData, RemoteData);

/**
 * Determines whether to retrieve all resource data the user can view.
 * @var bool DefaultRetrieveAllData
 */
ResourceKeywordSearchData.DefaultRetrieveAllData = false;

/**
 * Determine if the data identified by the given key is already cached.
 * @param string key data identifier
 * @return true if the data is cached or false otherwise
 * @see ResourceKeywordSearchData::getData()
 */
ResourceKeywordSearchData.prototype.isDataCached = function(key) {
  key = this.getCacheKey(key);
  return ResourceKeywordSearchData.base.prototype.isDataCached.call(this, key);
};

/**
 * Get data identified by the given key. This method will fetch the data from
 * the cache if possible. Otherwise, it will trigger a remote fetch call.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 * @return the data if it's already cached
 * @see RemoteData::fetchRemoteData()
 * @see RemoteData::isDataCached()
 */
ResourceKeywordSearchData.prototype.getData = function(key, callback) {
  key = this.getCacheKey(key);
  return ResourceKeywordSearchData.base.prototype.getData.call(this, key, callback);
};

/**
 * The method used to initiate the remote data fetching process.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 */
ResourceKeywordSearchData.prototype.fetchRemoteData = function(key, callback) {
  var searchString = key.replace(/:[^:]*:[^:]*:[^:]*$/, ""),
      data = {
        "P": "QuickSearchCallback",
        "Version": "1",
        "Context": "Resources",
        "SearchString": searchString
      };

  // only add the ExportAllData parameter if the value appears to be true
  if (this._retrieveAllData) {
    data["ExportAllData"] = true;
  }

  // only add the MaxNumSearchResults parameter if the value appears to be valid
  if (this._maxNumSearchResults) {
    data["MaxNumSearchResults"] = this._maxNumSearchResults;
  }

  // only add the ID exclusions if there are any
  if (this._idExclusions.length) {
    data["IdExclusions"] = this._idExclusions;
  }

  // only add the field ID if it appears to be valid
  if (this._fieldId) {
    data["FieldId"] = this._fieldId;
  }

  jQuery.ajax(RouterUrl, {
    "data": data,
    "success": cw.bindObjectMethodCallback(this, this.remoteDataCallback, key, callback),
    "error": cw.bindObjectMethodCallback(this, this.remoteDataError, key)
  });
};

/**
 * Get the maximum number of search results to fetch per search.
 * @return the maximum number of search results fetched per search
 */
ResourceKeywordSearchData.prototype.getMaxNumSearchResults = function() {
  return this._maxNumSearchResults;
};

/**
 * Set the maximum number of search results to fetch per search.
 * @param int maxNumSearchResults maximum number of search results per search
 */
ResourceKeywordSearchData.prototype.setMaxNumSearchResults = function(maxNumSearchResults) {
  this._maxNumSearchResults = maxNumSearchResults;
};

/**
 * Get the value that determines whether to retrieve all resource data that the
 * user can view.
 * @return the value that determines whether to retrive all resource data
 */
ResourceKeywordSearchData.prototype.getRetrieveAllData = function() {
  return this._retrieveAllData;
};

/**
 * Set the value that determines whether to retrieve all resource data that the
 * user can view.
 * @param bool whether to retrive all resource data the user can view
 */
ResourceKeywordSearchData.prototype.setRetrieveAllData = function(retrieveAllData) {
  this._retrieveAllData = retrieveAllData;
};

/**
* Get all of the currently-set resource ID exclusions.
* @return Returns the list of currently-set resource ID exclusions.
*/
ResourceKeywordSearchData.prototype.getIdExclusions = function() {
  return this._idExclusions.slice(0);
};

/**
* Replace the current resource ID exclusions with the given ID exclusions.
* @param array idExclusions Array of resource IDs of resources to exclude.
*/
ResourceKeywordSearchData.prototype.setIdExclusions = function(idExclusions) {
  this._idExclusions = idExclusions.slice(0);
};

/**
* Add a resource ID exclusions.
* @param int idExclusion Resource ID of a resource to exclude.
*/
ResourceKeywordSearchData.prototype.addIdExclusion = function(idExclusion) {
  this._idExclusions.push(idExclusion);
};

/**
* Remove a resource ID exclusion.
* @param int idExclusion Resource ID exclusion to remove.
*/
ResourceKeywordSearchData.prototype.removeIdExclusion = function(idExclusion) {
  delete this._idExclusions[this._idExclusions.indexOf(idExclusion)];
};

/**
* Clear the list of resource ID exclusions.
*/
ResourceKeywordSearchData.prototype.clearIdExclusions = function() {
  this._idExclusions = [];
};

/**
* Get the ID of the field from which to fetch values.
* @return the ID of the field from which to fetch values
*/
ResourceKeywordSearchData.prototype.getFieldId = function() {
  return this._fieldId;
};

/**
* Set the ID of the field from which to fetch values.
* @param int fieldId ID of the field from which to fetch values
*/
ResourceKeywordSearchData.prototype.setFieldId = function(fieldId) {
  this._fieldId = fieldId;
};

/**
 * Initialize default values for the object.
 * @protected
 */
ResourceKeywordSearchData.prototype.initDefaultValues = function() {
  ResourceKeywordSearchData.base.prototype.initDefaultValues.call(this);

  this._retrieveAllData = ResourceKeywordSearchData.DefaultRetrieveAllData;
  this.clearIdExclusions();
  this.fieldId = null;
};

/**
 * Get the key used internally for the cache for a data key.
 * @param string key data key
 * @protected
 */
ResourceKeywordSearchData.prototype.getCacheKey = function(key) {
  // keyword searches are case-insensitive, so normalize the string so THE and
  // the are treated exactly the same
  key = key.toLowerCase();

  // results will be different depending on the number of results, so add it to
  // the key
  key += ":" + this.getMaxNumSearchResults();

  // results will be different depending on the field ID, so add it to the key
  key += ":" + this.getFieldId();

  // results will be different depending on the ID exclusions
  key += ":" + this.getIdExclusions().sort().toString();

  return key;
};

/**
 * Holds the maximum number of search results to fetch per search.
 * @var int _maxNumSearchResults
 * @protected
 */
ResourceKeywordSearchData.prototype._maxNumSearchResults;

/**
 * Holds the value that determines whether to retrieve all resource data that
 * the user can view.
 * @var bool _retrieveAllData
 * @protected
 */
ResourceKeywordSearchData.prototype._retrieveAllData;

/**
* Holds the list of resource IDs of resources to exclude.
* @var array _idExclusions
* @protected
*/
ResourceKeywordSearchData.prototype._idExclusions;

/**
* Holds the ID of the field from which to fetch values.
* @var int _fieldId
* @protected
*/
ResourceKeywordSearchData.prototype._fieldId;

// -----------------------------------------------------------------------------

/**
 * Facilitates and caches remotely- and ansynchronously-fetched values from a
 * metadata field that can have multiple values.
 * @see MultipleValuesFieldData::getData()
 */
function MultipleValuesFieldData() {
  MultipleValuesFieldData.base.call(this);
} cw.extend(MultipleValuesFieldData, RemoteData);

/**
 * Determine if the data identified by the given key is already cached.
 * @param string key data identifier
 * @return true if the data is cached or false otherwise
 * @see MultipleValuesFieldData::getData()
 */
MultipleValuesFieldData.prototype.isDataCached = function(key) {
  key = this.getCacheKey(key);
  return MultipleValuesFieldData.base.prototype.isDataCached.call(this, key);
};

/**
 * Get data identified by the given key. This method will fetch the data from
 * the cache if possible. Otherwise, it will trigger a remote fetch call.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 * @return the data if it's already cached
 * @see RemoteData::fetchRemoteData()
 * @see MultipleValuesFieldData::isDataCached()
 */
MultipleValuesFieldData.prototype.getData = function(key, callback) {
  key = this.getCacheKey(key);
  return MultipleValuesFieldData.base.prototype.getData.call(this, key, callback);
};

/**
 * The method used to initiate the remote data fetching process.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 */
MultipleValuesFieldData.prototype.fetchRemoteData = function(key, callback) {
  var searchString = key.replace(/:[^:]*:[^:]*:[^:]*:[^:]*$/, ""),
      data = {
        "P": "QuickSearchCallback",
        "Version": "1",
        "Context": "MultipleValuesField",
        "SearchString": searchString,
        "FieldId": this._fieldId
      };

  // only add this parameter if it's set
  if (this._maxNumSearchResults) {
    data["MaxNumSearchResults"] = this._maxNumSearchResults;
  }

  // only add the ID exclusions if there are any
  if (this._idExclusions.length) {
    data["IdExclusions"] = this._idExclusions;
  }

  // only add the value exclusions if there are any
  if (this._valueExclusions.length) {
    data["ValueExclusions"] = this._valueExclusions;
  }

  jQuery.ajax(RouterUrl, {
    "data": data,
    "success": cw.bindObjectMethodCallback(this, this.remoteDataCallback, key, callback),
    "error": cw.bindObjectMethodCallback(this, this.remoteDataError, key)
  });
};

/**
 * Get the maximum number of values to return per search.
 * @return the maximum number of values returned per search
 */
MultipleValuesFieldData.prototype.getMaxNumSearchResults = function() {
  return this._maxNumSearchResults;
};

/**
 * Set the maximum number of values to return per search.
 * @param int maxNumSearchResults maximum number of values to return per search
 */
MultipleValuesFieldData.prototype.setMaxNumSearchResults = function(maxNumSearchResults) {
  this._maxNumSearchResults = maxNumSearchResults;
};

/**
 * Get the ID of the field from which to fetch values.
 * @return the ID of the field from which to fetch values
 */
MultipleValuesFieldData.prototype.getFieldId = function() {
  return this._fieldId;
};

/**
 * Set the ID of the field from which to fetch values.
 * @param int fieldId ID of the field from which to fetch values
 */
MultipleValuesFieldData.prototype.setFieldId = function(fieldId) {
  this._fieldId = fieldId;
};

/**
* Get all of the currently-set ID exclusions.
* @return Returns the list of currently-set ID exclusions.
*/
MultipleValuesFieldData.prototype.getIdExclusions = function() {
  return this._idExclusions.slice(0);
};

/**
* Replace the current ID exclusions with the given ID exclusions.
* @param array idExclusions Array of IDs of values to exclude.
*/
MultipleValuesFieldData.prototype.setIdExclusions = function(idExclusions) {
  this._idExclusions = idExclusions.slice(0);
};

/**
* Add an ID exclusions.
* @param int idExclusion ID of a value to exclude.
*/
MultipleValuesFieldData.prototype.addIdExclusion = function(idExclusion) {
  this._idExclusions.push(idExclusion);
};

/**
* Remove an ID exclusion.
* @param int idExclusion ID exclusion to remove.
*/
MultipleValuesFieldData.prototype.removeIdExclusion = function(idExclusion) {
  delete this._idExclusions[this._idExclusions.indexOf(idExclusion)];
};

/**
* Clear the list of ID exclusions.
*/
MultipleValuesFieldData.prototype.clearIdExclusions = function() {
  this._idExclusions = [];
};

/**
* Get all of the currently-set value exclusions.
* @return Returns the list of currently-set value exclusions.
*/
MultipleValuesFieldData.prototype.getValueExclusions = function() {
  return this._valueExclusions.slice(0);
};

/**
* Replace the current value exclusions with the given value exclusions.
* @param array valueExclusions Array of values to exclude.
*/
MultipleValuesFieldData.prototype.setValueExclusions = function(valueExclusions) {
  this._valueExclusions = valueExclusions.slice(0);
};

/**
* Add a value exclusion.
* @param int valueExclusion Value to exclude.
*/
MultipleValuesFieldData.prototype.addValueExclusion = function(valueExclusion) {
  this._valueExclusions.push(valueExclusion);
};

/**
* Remove a value exclusion.
* @param int valueExclusion Value exclusion to remove.
*/
MultipleValuesFieldData.prototype.removeValueExclusion = function(valueExclusion) {
  delete this._valueExclusions[this._valueExclusions.indexOf(valueExclusion)];
};

/**
* Clear the list of value exclusions.
*/
MultipleValuesFieldData.prototype.clearValueExclusions = function() {
  this._valueExclusions = [];
};

/**
* Initialize default values for the object.
* @protected
*/
MultipleValuesFieldData.prototype.initDefaultValues = function() {
  MultipleValuesFieldData.base.prototype.initDefaultValues.apply(this, arguments);
  this.clearIdExclusions();
  this.clearValueExclusions();
};

/**
 * Get the key used internally for the cache for a data key.
 * @param string key data key
 * @protected
 */
MultipleValuesFieldData.prototype.getCacheKey = function(key) {
  // searching should be case-insensitive, so normalize the string so THE and
  // the are treated exactly the same
  key = key.toLowerCase();

  // results will be different depending on the number of results, so add it to
  // the key
  key += ":" + this.getMaxNumSearchResults();

  // results will be different depending on the field ID, so add it to the key
  key += ":" + this.getFieldId();

  // results will be different depending on the ID exclusions
  key += ":" + this.getIdExclusions().sort().toString();

  // results will be different depending on the value exclusions
  key += ":" + this.getValueExclusions().sort().toString();

  return key;
};

/**
 * Holds the maximum number of search results to fetch per search.
 * @var int _maxNumSearchResults
 * @protected
 */
MultipleValuesFieldData.prototype._maxNumSearchResults;

/**
 * Holds the ID of the field from which to fetch values.
 * @var int _fieldId
 * @protected
 */
MultipleValuesFieldData.prototype._fieldId;

/**
* Holds the list of IDs of values to exclude.
* @var array _idExclusions
* @protected
*/
MultipleValuesFieldData.prototype._idExclusions;

/**
* Holds the list of values to exclude.
* @var array _valueExclusions
* @protected
*/
MultipleValuesFieldData.prototype._valueExclusions;

// -----------------------------------------------------------------------------

/**
 * Facilitates and caches remotely- and ansynchronously-fetched search results
 * for resource keyword searches.
 * @see RemoteData::getData()
 */
function UserData() {
  UserData.base.call(this);
} cw.extend(UserData, RemoteData);

/**
 * Determine if the data identified by the given key is already cached.
 * @param string key data identifier
 * @return true if the data is cached or false otherwise
 * @see UserData::getData()
 */
UserData.prototype.isDataCached = function(key) {
  key = this.getCacheKey(key);
  return UserData.base.prototype.isDataCached.call(this, key);
};

/**
 * Get data identified by the given key. This method will fetch the data from
 * the cache if possible. Otherwise, it will trigger a remote fetch call.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 * @return the data if it's already cached
 * @see RemoteData::fetchRemoteData()
 * @see RemoteData::isDataCached()
 */
UserData.prototype.getData = function(key, callback) {
  key = this.getCacheKey(key);
  return UserData.base.prototype.getData.call(this, key, callback);
};

/**
 * The method used to initiate the remote data fetching process.
 * @param string key data identifier
 * @param function callback the callback executed when the data is available
 */
UserData.prototype.fetchRemoteData = function(key, callback) {
  var searchString = key.replace(/:[^:]*:[^:]*:[^:]*$/, ""),
      data = {
        "P": "QuickSearchCallback",
        "Version": "1",
        "Context": "Users",
        "SearchString": searchString
      };

  // only add the MaxNumSearchResults parameter if the value appears to be valid
  if (this._maxNumSearchResults) {
    data["MaxNumSearchResults"] = this._maxNumSearchResults;
  }

  // only add the ID exclusions if there are any
  if (this._idExclusions.length) {
    data["IdExclusions"] = this._idExclusions;
  }

  // only add the value exclusions if there are any
  if (this._valueExclusions.length) {
    data["ValueExclusions"] = this._valueExclusions;
  }

  jQuery.ajax(RouterUrl, {
    "data": data,
    "success": cw.bindObjectMethodCallback(this, this.remoteDataCallback, key, callback),
    "error": cw.bindObjectMethodCallback(this, this.remoteDataError, key)
  });
};

/**
 * Get the maximum number of search results to fetch per search.
 * @return the maximum number of search results fetched per search
 */
UserData.prototype.getMaxNumSearchResults = function() {
  return this._maxNumSearchResults;
};

/**
 * Set the maximum number of search results to fetch per search.
 * @param int maxNumSearchResults maximum number of search results per search
 */
UserData.prototype.setMaxNumSearchResults = function(maxNumSearchResults) {
  this._maxNumSearchResults = maxNumSearchResults;
};

/**
* Get all of the currently-set ID exclusions.
* @return Returns the list of currently-set ID exclusions.
*/
UserData.prototype.getIdExclusions = function() {
  return this._idExclusions.slice(0);
};

/**
* Replace the current ID exclusions with the given ID exclusions.
* @param array idExclusions Array of IDs of values to exclude.
*/
UserData.prototype.setIdExclusions = function(idExclusions) {
  this._idExclusions = idExclusions.slice(0);
};

/**
* Add an ID exclusions.
* @param int idExclusion ID of a value to exclude.
*/
UserData.prototype.addIdExclusion = function(idExclusion) {
  this._idExclusions.push(idExclusion);
};

/**
* Remove an ID exclusion.
* @param int idExclusion ID exclusion to remove.
*/
UserData.prototype.removeIdExclusion = function(idExclusion) {
  delete this._idExclusions[this._idExclusions.indexOf(idExclusion)];
};

/**
* Clear the list of ID exclusions.
*/
UserData.prototype.clearIdExclusions = function() {
  this._idExclusions = [];
};

/**
* Get all of the currently-set value exclusions.
* @return Returns the list of currently-set value exclusions.
*/
UserData.prototype.getValueExclusions = function() {
  return this._valueExclusions.slice(0);
};

/**
* Replace the current value exclusions with the given value exclusions.
* @param array valueExclusions Array of values to exclude.
*/
UserData.prototype.setValueExclusions = function(valueExclusions) {
  this._valueExclusions = valueExclusions.slice(0);
};

/**
* Add a value exclusion.
* @param int valueExclusion Value to exclude.
*/
UserData.prototype.addValueExclusion = function(valueExclusion) {
  this._valueExclusions.push(valueExclusion);
};

/**
* Remove a value exclusion.
* @param int valueExclusion Value exclusion to remove.
*/
UserData.prototype.removeValueExclusion = function(valueExclusion) {
  delete this._valueExclusions[this._valueExclusions.indexOf(valueExclusion)];
};

/**
* Clear the list of value exclusions.
*/
UserData.prototype.clearValueExclusions = function() {
  this._valueExclusions = [];
};

/**
* Initialize default values for the object.
* @protected
*/
UserData.prototype.initDefaultValues = function() {
  UserData.base.prototype.initDefaultValues.apply(this, arguments);
  this.clearIdExclusions();
  this.clearValueExclusions();
};

/**
 * Get the key used internally for the cache for a data key.
 * @param string key data key
 * @protected
 */
UserData.prototype.getCacheKey = function(key) {
  // keyword searches are case-insensitive, so normalize the string so THE and
  // the are treated exactly the same
  key = key.toLowerCase();

  // results will be different depending on the number of results, so add it to
  // the key
  key += ":" + this.getMaxNumSearchResults();

  // results will be different depending on the ID exclusions
  key += ":" + this.getIdExclusions().sort().toString();

  // results will be different depending on the value exclusions
  key += ":" + this.getValueExclusions().sort().toString();

  return key;
};

/**
 * Holds the maximum number of search results to fetch per search.
 * @var int _maxNumSearchResults
 * @protected
 */
UserData.prototype._maxNumSearchResults;

/**
* Holds the list of IDs of users to exclude.
* @var array _idExclusions
* @protected
*/
UserData.prototype._idExclusions;

/**
* Holds the list of user names to exclude.
* @var array _valueExclusions
* @protected
*/
UserData.prototype._valueExclusions;

// -----------------------------------------------------------------------------

/**
 * Construct a remote data store. RemoteData should be used instead.
 */
function RemoteDataStore() {
    this.Cache = {};
}

/**
 * Get data from the data store.
 * @param id:mixed data id
 * @param callback:function function called when the data is available
 */
RemoteDataStore.prototype.get = function(id, callback) {
    if (this.Cache[id]) {
        callback(this.Cache[id]);
    } else {
        this.fetch(id, callback);
    }
};

/**
 * Fetch data remotely.
 * @param id:mixed data id
 * @param callback:function function called when the data is available
 */
RemoteDataStore.prototype.fetch = function(id, callback) {
    return undefined;
};

/**
 * @protected Cache:object data cache
 */
RemoteDataStore.prototype.Cache = null;

/**
 * Generate a random string of some arbitrary length.
 * @param int length the length of the random string
 * @return a random string
 */
function GenerateRandomString(length) {
  var text = "",
      possibleCharacters = "01234567890abcdefghijklmnopqrstuvwxyz";

  for (var i = 0; i < length; i++) {
    text += possibleCharacters.charAt(
      Math.floor(Math.random() * possibleCharacters.length));
  }

  return text;
}

/**
 * Make the given number user-friendly by returning its string representation in
 * English, when appropriate.
 * @param int num integer number
 * @return the string representation of the number in English
 */
function MakeNumberUserFriendly(num) {
  switch (num) {
    case 0: return "zero";
    case 1: return "one";
    case 2: return "two";
    case 3: return "three";
    case 4: return "four";
    case 5: return "five";
    case 6: return "six";
    case 7: return "seven";
    case 8: return "eight";
    case 9: return "nine";
    case 10: return "ten";
    default: return num+"";
  }
}

/**
 * Truncate a string to a given length and put a suffix on the end after it has
 * been truncated. The length of the suffix is taken into account.
 * @param string text string to truncate
 * @param int maxLength the maximum length of the string
 * @param string suffix string to add on to the end of the string
 * @return the truncated string
 */
function TruncateString(text, maxLength, suffix) {
  var truncatedText = text;

  // set the default value for maxLength if not given
  if (typeof maxLength == "undefined") {
    maxLength = 120;
  }

  // set the default value for suffix if not given
  if (typeof suffix == "undefined") {
    suffix = "...";
  }

  // return the string untouched if it is already short enough
  if (truncatedText <= maxLength) {
    return truncatedText;
  }

  // need to take the suffix into account
  maxLength -= suffix.length;

  // perform the truncation and add on the suffix
  truncatedText = truncatedText.substring(0, maxLength).split(" ");
  truncatedText = truncatedText.slice(0, -1).join(" ");

  // add the suffix there is something to add it to
  if (truncatedText.length) {
    truncatedText += suffix;
  }

  return truncatedText;
}

/**
 * Escape all HTML entities in a string.
 * @param string text text to escape
 * @return string with HTML entities escaped
 */
function EscapeHtmlEntitiesInString(text) {
  return new String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

/**
 * Strip HTML from a string.
 * @param string text text from which to strip HTML
 * @return string with HTML removed
 */
function StripHtmlFromString(text) {
  return new String(text).replace(/<("(\\"|[^"])*"|'(\\'|[^'])*'|[^>])*>/g, "");
}

/**
 * Reverse a string.
 * @param string text string to reverse
 * @return the reversed string
 */
function ReverseString(text) {
  return new String(text).split("").reverse().join("");
}

/**
 * Trim whitespace from the front and back of a string.
 * @param string text string to trim
 * @return the trimmed string
 */
function TrimString(text) {
  return new String(text).replace(/^\s+|\s+$/g, "");
}

/**
 * Format a number to include commas where necessary, using American English
 * placement.
 * @param number num number
 * @return the number with commas included where necessary
 */
function FormatNumber(num) {
  var partsOfNumber = (num+"").split("."),
      wholePart = partsOfNumber[0],
      decimalPart = partsOfNumber[1] || "";

  // add the commas to the whole part of the number
  if (wholePart > 999) {
    wholePart = ReverseString(ReverseString(wholePart).replace(/([0-9]{3})/g, "$1,"));
  }

  // tack on the decimal part, if necessary, and return
  return decimalPart ? wholePart + "." + decimalPart : wholePart;
}

/**
 * Highlight the terms in the given search string in the given text.
 * @param string searchString search string
 * @param string text text in which to highlight search string terms
 * @return the text with search string terms highlighted
 */
function HighlightSearchTermsInText(searchString, text) {
  var excludedTermsExp = /(^|\s+)-('(\\'|[^'])*('|$)|"(\\"|[^"])*("|$)|[^\s]*)/g,
      requiredTermsExp = /(^|\s+)[+]/g,
      quotesExp = /\\["']|["']/g,
      whitespaceExp = /^\s+|\s+$/g,
      shortWordsExp = /\b([^\s]{1,2})\b/g,
      escapeExp = /[-\/\\^$*+?.()|[\]{}]/g;

  // remove excluded terms because they shouldn't be highlighted
  searchString = searchString.replace(excludedTermsExp, "");

  // remove the required term(s) operator
  searchString = searchString.replace(requiredTermsExp, " ");

  // remove quotes and escaped quotes
  searchString = searchString.replace(quotesExp, "");

  // remove whitespace from the beginning and end
  searchString = searchString.replace(whitespaceExp, "");

  // escape remaining characters so they don't mess with the expression
  searchString = searchString.replace(escapeExp, "\\$&");

  // very short terms should only match the beginning of words
  searchString = searchString.replace(shortWordsExp, '\\b$1');

  // replace whitespace with | for the expression
  searchString = searchString.replace(/\s+/g, "|");

  return text.replace(new RegExp("("+searchString+")", "gi"), '<b class="highlighted-term">$1</b>');
}

/**
* Select a random option element from a select element, optionally filtering
* which elements may be selected. This adds the "selected" attribute to a random
* option and removes any "selected" attributes on options within the select
* element.
* @param jQuery select The select element in a jQuery object.
* @param string filter Optional filter for specifying which options may be
*      selected.
*/
function SelectRandomOption(select, filter) {
  var selectableOptions = jQuery("option", select).filter(filter),
  randomIndex = Math.floor(Math.random() * selectableOptions.length);

  // remove any existing selection
  jQuery(":selected", select).removeAttr("selected");

  // no options to select
  if (selectableOptions.length < 1) {
    return;
  }

  // select the random option
  selectableOptions.eq(randomIndex).attr("selected", "selected");
}

// exports
this.RemoteData = RemoteData;
this.ResourceKeywordSearchData = ResourceKeywordSearchData;
this.MultipleValuesFieldData = MultipleValuesFieldData;
this.UserData = UserData;
this.RemoteDataStore = RemoteDataStore;
this.GenerateRandomString = GenerateRandomString;
this.MakeNumberUserFriendly = MakeNumberUserFriendly;
this.TruncateString = TruncateString;
this.EscapeHtmlEntitiesInString = EscapeHtmlEntitiesInString;
this.StripHtmlFromString = StripHtmlFromString;
this.ReverseString = ReverseString;
this.TrimString = TrimString;
this.FormatNumber = FormatNumber;
this.HighlightSearchTermsInText = HighlightSearchTermsInText;
this.SelectRandomOption = SelectRandomOption;

});
