/* 
 * <file> 
 *  Name:    KeyValueMap.js
 *  Purpose: Creates a Key-Value map which maps string keys
 *           to arrays of string values.
 *  
 *  Created: 1999-09-01
 *  $Id: KeyValueMap.js,v 1.5 2005/09/16 13:44:15 sweber Exp $
 * </file> 
*/ 
initPackage ( "com.hyperwave.util" );

//------------------------------------------------------------
/** 
 * A class for managing maps from string keys to arrays
 * of values for parameter definitions.
 * 
 * @author Christian Fessl
 * @author Simon Zwoelfer
**/ 
//------------------------------------------------------------
// <JSClass Name="KeyValueMap"> 

  //----------------------------------------------------------
  /** 
   * Constructor for this class.
   * @examplecall my_obj = new KeyValueMap()
  **/ 
  com.hyperwave.util.KeyValueMap = function ( theParams )
  {
    if ( theParams == "__proto__" && typeof theParams == "string" )
      return;
 
    this.map_ = new Object(); // hold the key-value pairs
    this.serialize_ = false; // do not serialize values by default
    this.valueSort_ = true; // sort values of each key before sending by default
    this.isCaseInsensitive_ = false;
  }
  class$ = doInherit ( com.hyperwave.util.KeyValueMap );
	
  /**
   * Solves some problems occuring by bad firewalls.
   * ";" --> "%3B" and "&" --> "%26" were converted by some firewalls.
   * Now they are converted back to their original values.
   * 
   * @param  String: aParametersString: the parameters part of an URL
   * @return String: the converted parameters part of the URL
  **/
  class$._solveFirewallProblem = function (aParametersString)
  {
    var converted_par_str = "";
    var split = aParametersString.split("=");
    
    if (split.length > 0)
    {
      converted_par_str += split[0] + "=";
      for (var k=0; k<split.length; k++)
      {
        var and_position = split[k].lastIndexOf("%26");            
        if (and_position != -1)
        {
          var prev_value = split[k].substring(0,and_position);
          var next_key   = split[k].substr(and_position+3);
          converted_par_str += prev_value + "&" +  next_key + "=";
        }
      }
      converted_par_str += split[split.length-1];
    }

    return converted_par_str;
  }

  //-----------------------------------------------------------
  /**
   * Returns only the parameter part of an URL.
   * @param  String: anUrl: the URL called by the browser
   * @return String: the parameter string of the URL
   * @examplecall 
   *   this._getKeyValueMapFromUrl("http://www.server.com;p1=1&p2=2&p3=3")
   *   results in "p1=1&p2=2&p3=3"
  **/
  class$._getParameterStringFromUrl = function (anUrl)
  {
    var par_str = "";

    var found_position_of_questionmark = anUrl.indexOf("?");
    var found_position_of_semicolon = anUrl.indexOf(";");
    var found_position_of_unescaped_semicolon = anUrl.indexOf("%3B");
    
    if (found_position_of_questionmark != -1) {
      if (found_position_of_semicolon != -1) {
        var pos = found_position_of_semicolon;
        if (found_position_of_questionmark < found_position_of_semicolon) {
          pos = found_position_of_questionmark;
        }
        par_str = anUrl.substr(pos + 1);
        par_str = par_str.replace(";", "&");
        par_str = par_str.replace("?", "&");
      }
      else {
        par_str = anUrl.substr(found_position_of_questionmark + 1);
      }
    }
    else {
      if (found_position_of_semicolon != -1) {
        par_str = anUrl.substr(found_position_of_semicolon + 1);
      }
      else {
        if (found_position_of_unescaped_semicolon != -1) {
          // firewall problem by ";" --> "%3B" and "&" --> "%26"
          par_str = anUrl.substr(found_position_of_unescaped_semicolon + 3);
          par_str = this._solveFirewallProblem(par_str);
        } // firewall problem solved
      }
    }

    return par_str;
  }

  //-----------------------------------------------------------
  /**
   * Adds a key value pair to the map. Note that all other
   * values remain in the map and are not changed. Regardless
   * to the existence of a same value the given value will
   * simply be added to the array of values for this key.
   * @param  String: aKey: contains the name which should have
   *   at leas the given value.
   * @param  String: aValue: is the value which should be
   *   added for the key.
   * @examplecall
   *   var my_params = new KeyValueField();
   *   my_params.addKeyValue ( "firstPar", 1 );
   *   my_params.addKeyValue ( "firstPar", 2 );
  **/
  class$.addKeyValue = function (aKey, aValue)  
  {
    if (this.map_[aKey])
    {
      this.map_[aKey][this.map_[aKey].length] = aValue;
    }
    else 
    {
      this.map_[aKey] = new Array();
      this.map_[aKey][0] = aValue;
    }
  }
  
  //-----------------------------------------------------------
  /**
   * Deletes a key and ist associated values from the map.
   * @param  String: aKey: the key which should be deleted.
   * @examplecall 
   *   var my_params = new KeyValueField();
   *   my_params.addUrlParameters ( window.location.href );
   *   
   *   // assure that there is no Key: "line":
   *   my_params.deleteKey ( "line" );
  **/
  class$.deleteKey = function (aKey)  
  {
    if (this.map_[aKey])
      delete this.map_[aKey];
  }
  
  //-----------------------------------------------------------
  /**
   * Assures that the map contains exactly one key (with the name
   * aKey) with one corresponsing value
   * @param  String: aKey: contains the name which should have 
   *   the given value.
   * @param  String: aValue: defines the value.
   * @examplecall
   *   var my_params = new KeyValueField();
   *   my_params.addKeyValue ( "firstPar", 1 );
   *   my_params.addKeyValue ( "firstPar", 2 );
   *   
   *   // ... some processing with my_params ...
   *   
   *   // now assure that firstPar has exactly one value:
   *   my_params.setKeyValue ( "firstPar", "only_this_value" );
      
  **/
  class$.setKeyValue = function (aKey, aValue)  
  {
    this.map_[aKey] = new Array();
    this.map_[aKey][0] = aValue;
  }
  
  //-----------------------------------------------------------
  /** 
   * Splits the parameter string into its keys and values
   * and adds them to the map as this is done in addUrlParameters.
   * Note: the values are added regardless to their existence within 
   * the map!
   * @param  String: theParams: the Parameters in String represenation
   * @return Object of type Parameters
   * @examplecall
   *   var my_par = new KeyValueMap ();
   *   my_par.addParameters ( "key1=value1&key1=value2&key2=value3" );
   *   
  **/ 
  class$.addParameterString = function ( theParams )
  {
    var theUrl = ";" + theParams;
    this.addUrlParameters ( theUrl );
  }

  //-----------------------------------------------------------
  /** 
   * Fetches all availabe parameters from the given URL string
   * and puts them into this KeyValueMap. The parameters are
   * detected with the following algorithm:
   * <ol>
   * <li>Detect the parameter string:
   *  <ul>
   *  <li>If there is a question mark, take the string after
   *      the question mark and process the parameters.
   *  <li>Otherwise: if there is a semicolon, treat all characters
   *      after the semicolon as parameter string and 
   *      process them.
   *  <li>Otherwise: if there is an escaped semicolon (which
   *      is in fact the string "%3B" then take the characters
   *      which follow this string. In this case take care
   *      of escaped "=" and "&" characters too.
   *  </ul>
   * <li>Extract the parameters by splitting the whole
   *     string by its "&" characters. Subsequently use
   *     the first "=" character within the resulting
   *     string fragments as end for the key string
   *     and beginning for the value string. The
   *     general form of parameter strings is then:
   *     <pre>
   *      key1=value11&key1=value12&key...
   *     </pre>
   * </ul>
   * Note: the values are added regardless to their existence within 
   * the map!   
   *     
   * @param  String: theUrl: the URL containing the parameters.
   *   which should be added.
   * @examplecall 
   *   // Client Example: get URL of window, add
   *   //   a parameter and make a link ...
   *   var my_par = new KeyValueMap ();
   *   my_par.addUrlParameters ( window.location.href );
   *   my_par.addKeyValue ( "key10", "value10" );
   *   document.writeln ( "<a href='" +
   *                      window.loaction.pathname + ";" +
   *                      my_par.getParameterString () );
  **/ 
  class$.addUrlParameters = function ( theUrl )
  {
             
    var my_get_params_string = this._getParameterStringFromUrl(theUrl);   
    if (my_get_params_string != "") { 
      var my_key_values_array = my_get_params_string.split("&");
      for (var i=0; i<my_key_values_array.length; i++) {
        var key_value_pair = my_key_values_array [i].split("=");
        if (key_value_pair.length == 2) {
          var temp_value = decodeURIComponent(key_value_pair[1].replace (/\+/g, ' '));
          if (this.serialize_ == true) {
            eval("var evaluated_value = " + temp_value);
          }
          else { 
            var evaluated_value = temp_value;
          }
          if (this.isCaseInsensitive_) {
            this.addKeyValue(decodeURIComponent(key_value_pair[0]).toLowerCase(), 
                             evaluated_value );
          }
          else {
            this.addKeyValue(decodeURIComponent(key_value_pair[0]), 
                             evaluated_value );
          }
        }
      }
    } 
  }

  //-----------------------------------------------------------
  /** 
   * Checks if a given key exists which has at least the 
   * desired value.
   * @param String: aKey: contains the name of the parameter
   * @param Object: aValue: contains the desired value
   * @return Boolean: true, if the key aKey exists and one of 
   *   its values is equal to aValue. Otherwise false will be 
   *   returned.
   * @examplecall .containsKeyValue("my_key","my_value")
  **/ 
  class$.containsKeyValue = function (aKey, aValue)
  {
    var ret = false;
    if (this.map_[aKey])
    {
      for (var i=0; i<this.map_[aKey].length; i++)
      {
        if (this.map_[aKey][i] == aValue)
        {
          ret = true;
          break;
        }
      }
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Returns an array of strings containing all keys with at 
   * least one value (domain set).
   * @return Array: an array of strings containing the keys 
   *   which have at least one value.
   * @examplecall
   *   URL: http://www.server.com/module;p1=1&p1=2&p2=5&p3=10
   *   
   *   var my_params = new KeyValueMap();
   *   my_params.addUrlParameters ( location.href ); 
   *   var my_keys = my_params.getKeys(); 
   *   
   *   Result: my_keys = ["p1", "p2", "p3"]   
   **/ 
  class$.getKeys = function ()
  {
    var ret = new Array();
    for (var x in this.map_)
    {
      ret[ret.length] = x;
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Returns the number of keys with at least one value which
   * are present.
   * @return Integer: number of keys with at least one value 
   * @examplecall
   *   var my_params = new KeyValueMap();
   *   my_params.addUrlParameters ( location.href ); 
   *   var number_of_keys = my_params.getNumberOfKeys();
   *   writeln("Number of Keys sent: " + number_of_keys);   
   **/ 
  class$.getNumberOfKeys = function ()
  {
    var ret = 0;
    for (var x in this.map_)
    {
      ret = ret + 1;
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Checks if a given key with at least one value is present.
   * @param  String: aKey: contains the name of the parameter 
   * @return Boolean: true, if <i>aKey</i> has at least one
   *   value within the map.
   * @examplecall 
   *   var my_params = new KeyValueMap();
   *   if ( my_params.containsKey("param1") ) 
   *   { 
   *     writeln("param1 exists!"); 
   *   } 
  **/ 
  class$.containsKey = function (aKey)
  {
    var ret = false;
    if (this.map_[aKey])
    {
      ret = true;
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Returns the number of values of a given key. 
   * @param String: aKey: contains the name of the parameter 
   * @return Integer: representing the number of values to that key 
   * @examplecall
   *   var my_params = new KeyValueMap();
   *   my_params.addUrlParameters ( location.href ); 
   *   var number_of_values = my_params.getNumberOfValues("param1");
   *   writeln("Number of values to key: " + number_of_values);   
   **/ 
  class$.getNumberOfValues = function (aKey)
  {
    var ret = 0;
    if (this.map_[aKey])
    {
      ret = this.map_[aKey].length;
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Returns all values of a given key. 
   * @param  String: aKey: contains the name of the parameter 
   * @return Array: an array of strings containing all values 
   *   of the given key. 
   *   <b>Note</b> that this array can contain multiple 
   *   entries with the same value.
   * @examplecall 
   *   URL: http://www.server.com/module;p1=1&p1=2&p2=5&p3=10
   *   
   *   var my_params = new KeyValueMap();
   *   my_params.addUrlParameters ( location.href ); 
   *   my_values = my_params.getAllValues("p1"); 
   *   
   *   Result: my_values = ["1", "2"]
  **/ 
  class$.getAllValues = function (aKey)
  {
    var ret = new Array();
    if (this.map_[aKey])
    {
      ret = this.map_[aKey];
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * If a key has exactly one value it will be returned, if there
   * are more than one values only one value will be selected in a
   * nondeterministic way.
   * @param String: aKey: contains the name of the parameter 
   * @return Object: contains one value of the given key 
   * @examplecall 
   *   var my_params = new KeyValueMap();
   *   my_params.addUrlParameters ( location.href ); 
   *   writeln( "param1: one value = " + my_params.getOneValue("param1") ); 
  **/ 
  class$.getOneValue = function (aKey)
  {
    var ret = null;
    if (this.map_[aKey])
    {
      if (this.map_[aKey].length > 0)
      {
        ret = this.map_[aKey][0];
      }
    }
    return ret;
  }

  //-----------------------------------------------------------
  /** 
   * Clones a KeyValueMap at the level of keys. This means, that
   * a new KeyValueMap will be returned with new keys BUT
   * references to the values. If the values are strings
   * or only key-manipulations are done this is sufficient.
   *
   * @param Array: theExclList: array of string keys which 
   *   should not be contained in the resulting map.
   * @return KeyValueMap: a new KeyValueMap containing all
   *   keys and their corresponding values but the keys in 
   *   the exclusion list
  **/
  class$.clone = function ( theExclList )
  {
    var out = new com.hyperwave.util.KeyValueMap;
    var look_up = {};
    if (theExclList) {
      for ( var i = 0; i < theExclList.length; ++i )
        look_up [theExclList [i]] = true;
    }

    var key_array = this.getKeys ();

    for ( var i = 0; i < key_array.length; ++i )
    {
      var key = key_array [i];
      if ( look_up [key] )
        continue;

      var values = this.getAllValues (key);
      for ( var j = 0; j < values.length; ++j )
        out.addKeyValue ( key, values [j] );
    }
    return out;    
  }

  //-----------------------------------------------------------
  /** 
   * Create a parameter-string which is unique for this 
   * set of parameters (sorted by keys and values). This
   * string can be appended to an URL for transmitting ...
   * Note: This function is the inverse function of
   *   addParameterString. It <i>can</i> be used
   *   to &quote;serialize&quote; the state of the
   *   object which can be later de-serialized with
   *   addParameterString of an empty object!
   * @param aPrefix: string | "?": the string prefix for the returned parameter string
   * @return String: the key-value pairs for an URL
   * @examplecall
   *   // Client Example: get URL of window, add
   *   //   a parameter and make a link ...
   *   var my_par = new KeyValueMap ();
   *   
   *   my_par.addUrlParameters ( "key2=2&key2=1&key1=2" );
   *   writeln ( my_par.getParameterString () )
   *   // would give: "key1=2&key2=1&key2=2" !
  **/ 
  class$.getParameterString = function (aPrefix)
  {
    var key_array = this.getKeys ();
    if (key_array.length==0) return "";
    var out_arr = [];
    key_array.sort ();

    for ( var i = 0; i < key_array.length; ++i )
    {
      var key = key_array [i];
      var values = this.getAllValues (key);
      if (this.valueSort_)
        values.sort ();
      for ( var j = 0; j < values.length; ++j ) {
        if (this.serialize_ == true)
          var temp_value = com.hyperwave.util.ObjectExt.static_.toSource(values [j]);
        else 
          var temp_value = values[j];
        var encoded_value = (temp_value != null) ? encodeURIComponent(temp_value).replace ( /\+/g, "%2B" ): "null";
        out_arr [out_arr.length] = key + "=" + encoded_value;
      }
    }
    return (typeof aPrefix == "undefined" ? "?" : aPrefix) + out_arr.join ( "&" );
    
  }

  //-----------------------------------------------------------
  /** 
   * Returns a string containing a description of the status of that object.
   * @param  void
   * @return String: containing the status of the Parameter-object.
   * @examplecall .debugGetObjectStatus()
  **/ 
  class$.debugGetObjectStatus = function ()
  {
    var out_arr = [];
    var key_array = this.getKeys ();

    for ( var i = 0; i < key_array.length; ++i )
    {
      var key = key_array [i];
      out_arr [out_arr.length] = 
        "'" + key + "' => '" +
        this.getAllValues (key).join ( "', '" ) + "'";
    }
    return out_arr.join ( "\n" );
  }
    
  //------------------------------------------------------------
  /**
   * after calling this method serializing is activated.
   * all values of KeyValuePairs are serialized using com.hyperwave.util.ObjectExt.static_.toSource() while calling 
   * the method getParameterString. with this behaviour whole objects are represented as string
   * and can be sent to the serevr. When calling addUrlParameters those string representations 
   * are tried to be evaluated using "eval" before assigning as value to a specified key.
   *
   * @examplecall:
   * //CLIENT
   * var a1 = {p1:"hallo", p2:[1,"3"]};
   * var map1 = new KeyValueMap();
   * map1.enableSerializing();
   * map1.addKeyValue( "a", a1 );
   * var url = "test?" + map1.getParameterString();
   * // SERVER
   * var map2 = new KeyValueMap();
   * map2.enableSerializing();
   * map2.addUrlParameters( url );
   * var a2 = map2.getOneValue( "a" );
   * // results:
   * // a2 = {p1:"hallo", p2:[1,"3"]}
   * // but (a1 == a2) returns false!
  **/
  class$.enableSerializing = function ()
  {
    this.serialize_ = true;
  }
  
  //------------------------------------------------------------
  /**
   * after calling this method serializing is deactivated.
   * @see KeyValueMap_enableSerializing() for further details.
  **/
  class$.disableSerializing = function ()
  {
    this.serialize_ = false;
  }
  
  //------------------------------------------------------------
  /**
   * after calling this method keys are handled case insensitive
   * when added by method 'addUrlParameters'. 
   * (converted to lower case) 
  **/
  class$.isCaseInsensitive = function ()
  {
    this.isCaseInsensitive_ = true;
  }

  //------------------------------------------------------------
  /**
   * after calling this method keys are handled case sensitive
   * when added by method 'addUrlParameters'. 
   * (no longer converted to lower case) 
  **/
  class$.isCaseSensitive = function ()
  {
    this.isCaseInsensitive_ = false;
  }

  //------------------------------------------------------------
  /**
   * after calling this method all values of a given key are inserted
   * in sorted order when creating the parameter string (getParameterString()).
   * this is also the default behaviour.
  **/
  class$.enableSort = function ()
  {
    this.valueSort_ = true;
  }
  
  //------------------------------------------------------------
  /**
   * after calling this method all values of a given key are inserted
   * in the order they appear when creating the parameter string (getParameterString()).
   * this is NOT the default behaviour (default: insert in sorted order).
  **/
  class$.disableSort = function ()
  {
    this.valueSort_ = false;
  }
    

  class$.cutParametersFromUrl = function (anUrl)
  {
    var ind = anUrl.indexOf(";");
    var ind2 = anUrl.indexOf("?");
    if ((ind==-1) || ((ind2!=-1) && (ind2<ind))) ind=ind2;
    if (ind > -1){
      anUrl = anUrl.substring(0,ind);
    }
    return anUrl;
  }

  //--------------------------------------------------------------------
  /**
   * Keep Compatibility: make alias "KeyValueMap" for "com.hyperwave.util.KeyValueMap"
   */
  KeyValueMap = com.hyperwave.util.KeyValueMap;

// </JSClass>
//------------------------------------------------------------

/* End of KeyValueMap.js */

