/*
 * Copyright (c) 2003 by Hyperwave AG. All rights reserved.
 * $Id: WebParameters.js,v 1.4.2.1 2004/11/05 12:48:52 ckoch Exp $
 */

/**
 * Represents an abstract form of key |=&gt; array maps that mostly
 * represent either URL parameters or parameters that should be
 * encoded into a URL or HTML form. It serves mostly as a convenience class
 * that helps to get rid of the problem that URL parameters can have
 * multiple values for on key but won't usually use this feature. So
 * we will use this classes' method to get rid of nasty accessors like
 * <code>if (params[key]) value == params[key][0]</code>.</p>
 *
 * <p>This class might be useful for all parameter modifications on
 * the client side that will finally result in a URL. One can use
 * this class to create right parameter strings for URLs that
 * are RFC-2396 conform.
 *
 * @author simon.zwoelfer
 */
defineClass ( "com.hyperwave.util.WebParameters", function (class$)
{
  /**
   * Default constructor of empty WebParameters.
   *
   * <p>This implementation will initialize internal structures hence
   * it has to be called by subclass constructors.</p>
   */
  class$.constructor = function ( theParams )
  {
    if ( theParams == "__proto__" )
      return;

    /** Private holder for the key-values */
    this.map_ = new Object();

    /** Indicator for sorted conversions */
    this.valueSort_ = true;
  }
	
  /**
   * Solves some problems that occur with bad firewalls. The problem is
   * that too much is escaped sometimes. Hence one has to convert
   * escaped &quot;&amp;&quot;" characters (%26) back to their
   * original value to split key value pairs later on.
   *
   * <p>Note: This implementation is somewhat questionable because
   * there might occur some problems with it:
   * <ul>
   *   <li>There exist firewalls that decode URLs and URL
   *     parameters
   *   <li>There exist firewalls that ENCODE URLs and URL
   *     parameters
   *   <li>It depends on the assumption that question mark
   *     characters are not allowed within the path or
   *     host information.
   * </ul>
   * @param aParametersString: string: the parameters part of a 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 a URL as a string.
   *
   * <p>This implementation extracts the parameter part of the URL by
   * means of the first occurrence of a parameter separator. A
   * separator might be a question mark "?" or an escaped question
   * mark "%3F". From the point of view of RFC 2396 this is not
   * considered a problem because (a) question marks are reserved
   * characters and (b) escaped question marks will normally not be
   * present within the path information (because the Windows
   * file-system does not allow question marks within Filenames). If
   * this is the case a special handling of key-value pairs has to be
   * done.
   *
   * @param  anUrl: string: the URL to be analyzed.
   * @return string: the parameter string of the URL
   */
  class$._getParameterStringFromUrl = function (anUrl)
  {
    var par_str = "";

    var found_position_of_questionmark = anUrl.indexOf("?");
    var found_position_of_escaped_qmark = anUrl.indexOf ("%3F");

    if (found_position_of_questionmark != -1)
      par_str = anUrl.substr(found_position_of_questionmark + 1);
    else if (found_position_of_escaped_qmark != -1)
    {
      par_str = anUrl.substr(found_position_of_unescaped_semicolon + 3);
      par_str = this._solveFirewallProblem(par_str);
    }

    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 aKey: String: contains the name which should map
   *   at least to the given value.
   * @param aValue: Object: is the value which should be
   *   added to the key's values.
   */
  class$.addKeyValue = function (aKey, aValue)
  {
    if ( !aValue )
      return;

    if (this.map_[aKey])
      this.map_[aKey][this.map_[aKey].length] = aValue;
    else
      this.map_[aKey] = ([ aValue ]);
  }

  /**
   * Deletes a key and its associated values from the map.
   * @param aKey: string: the key that should be deleted.
   */
  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 corresponding value. Note that previous values
   * will simply be discarded.
   *
   * @param aKey: string: contains the name which should have
   *   the given value.
   * @param aValue: Object: defines the value.
   */
  class$.setKeyValue = function (aKey, aValue)
  {
    if ( !aValue )
      return;

    this.map_[aKey] = ([ aValue ]);
  }

  /**
   * Splits an arbitrary <i>parameter string</i> into its keys and
   * values and adds them to the map.
   *
   * <p>This implementation uses the @see #addUrlParameters method as a
   * helper.
   *
   * @param theParams: string: the Parameters in String representation
   * @see #addUrlParameters
   */
  class$.addParameterString = function ( theParams )
  {
    var theUrl = "?" + theParams;
    this.addUrlParameters ( theUrl );
  }

  /**
   * Fetches all available parameters from the given URL string and
   * adds them to the key-values. The parameters are extracted from
   * the URL with the following algorithm:
   * <ul>
   * <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 an escaped question mark (which is in
   *    fact the string "%3F" 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>
   *
   * <p>This implementation will add key-values regardless to existing
   * key-values.</p>
   *
   * @param theUrl: string: 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 com.hyperwave.util.WebParameters ();
   *   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 == "" )
      return;

    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)
        continue;

      var temp_key = unescape(key_value_pair[0].replace (/\+/g, ' '));
      if ( testUtf8 ( temp_key ) )
        temp_key = decodeUtf8 ( temp_key );

      var temp_value = unescape(key_value_pair[1].replace (/\+/g, ' '));
      if ( testUtf8 ( temp_value ) )
        temp_value = decodeUtf8 ( temp_value );

      this.addKeyValue(temp_key, temp_value );
    }
  }

  /**
   * Checks if a given key exists which has at least the
   * desired value. Note that the "desired value" will be matched by
   * the JavaScript equality.
   *
   * @param aKey: string: contains the name of the parameter
   * @param aValue: Object: 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.
   */
  class$.containsKeyValue = function (aKey, aValue)
  {
    var values = this.map_[aKey];
    if ( !values )
      return false;

    for (var i=0; i<values.length; i++)
    {
      if (values[i] == aValue)
        return true;
    }
    return false;
  }

  /**
   * Returns an array of strings containing all keys with at
   * least one value. (This array is often called <i>the map's domain
   * set</i>.)
   *
   * @return String[]: 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 com.hyperwave.util.WebParameters();
   *   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_)
      if ( this.map_.hasOwnProperty ( x ) )
        ret[ret.length] = x;

    return ret;
  }

  /**
   * Returns the number of keys with at least one value.
   *
   * @return int: number of keys.
   */
  class$.getNumberOfKeys = function ()
  {
    var ret = 0;
    for (var x in this.map_)
      if ( this.map_.hasOwnProperty ( x ) )
        ++ret;

    return ret;
  }

  /**
   * Checks if a given key with at least one value is present.
   *
   * @param aKey: string: the key to be checked.
   * @return boolean: true, if <i>aKey</i> has at least one
   *   value within the map.
   */
  class$.containsKey = function (aKey)
  {
    return (this.map_[aKey] ? true : false);
  }

  /**
   * Returns the number of values of a given key.
   *
   * @param aKey: string: the key.
   * @return int: representing the number of values to that key.
   *   If the key is not present in the map the value 0 will
   *   returned.
   */
  class$.getNumberOfValues = function (aKey)
  {
    if (this.map_[aKey])
      return this.map_[aKey].length;
    return 0;
  }

  /**
   * Returns all values of a given key.
   *
   * @param  aKey: string: the key.
   * @return String[]: an array of strings containing all values
   *   of the given key or <code>null</code> if the key is not
   *   present in the map.
   *   Note that the returned array can contain multiple
   *   entries with the same value.
   * @examplecall
   *   var url = "http://www.server.com/module;p1=1&p1=2&p2=5&p3=10";
   *   var my_params = new com.hyperwave.util.WebParameters();
   *   my_params.addUrlParameters ( url );
   *   my_values = my_params.getAllValues("p1");
   *
   *   Result: my_values = ["1", "2"]
   */
  class$.getAllValues = function (aKey)
  {
    return this.map_[aKey];
  }

  /**
   * 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 aKey: string: the key.
   * @return Object: will be <code>null</code> if the key is not
   *   present within the map or one of the objects (strings)
   *   referenced by the key.
   */
  class$.getOneValue = function (aKey)
  {
    if (this.map_[aKey] && this.map_[aKey].length > 0)
      return this.map_[aKey][0];

    return null;
  }

  /**
   * Clones a WebParameters at the level of keys. This means, that
   * a new WebParameters 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.</p>
   *
   * This implementations will use the constructor to create
   * a new object.
   *
   * @param theExclList: String[]: array of string keys which
   *   should not be contained in the resulting map.
   * @return com.hyperwave.util.WebParameters: a new WebParameters
   *   containing all keys and their corresponding values but the keys
   *   in the exclusion list
   */
  class$.clone = function (theExclList)
  {
    var out = new com.hyperwave.util.WebParameters;
    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.
   *
   * @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 com.hyperwave.util.WebParameters ();
   *
   *   my_par.addUrlParameters ( "key2=2&key2=1&key1=2" );
   *   writeln ( my_par.getParameterString () )
   *   // would give: "key1=2&key2=1&key2=2" !
   */
  class$.getParameterString = function ()
  {
    var out_arr = [];
    var key_array = this.getKeys ();
    key_array.sort ();

    for ( var i = 0; i < key_array.length; ++i )
    {
      var key = key_array [i];
      if ( !this.map_.hasOwnProperty ( key ) )
        continue;
      var values = this.map_ [key];
      if (this.valueSort_)
        values.sort ();
      for ( var j = 0; j < values.length; ++j )
      {
        out_arr [out_arr.length] = encodeURIComponent (key) + "="
          + encodeURIComponent (values[j]);
      }
    }
    return out_arr.join ( "&" );
  }

  /**
   * Returns a list of all key-values.
   * @return string: containing the status of the map.
   */
  class$.toString = function ()
  {
    var out_arr = [ "@" + this.static_.getClassName() + ":" ];

    for ( var key in this.map_ )
    {
      if ( !this.map_.hasOwnProperty ( key ) )
	continue;
      var values = this.map_ [key];
      out_arr [out_arr.length] =
        "'" + key + "' => [" +
        values.join ( "," ) + "]";
    }
    return out_arr.join ( "\n" );
  }

  /**
   * After calling this method all values of a given key are inserted
   * in sorted order when creating the parameter string
   * (getParameterString()).
   *
   * <p>This is also the default behavior to make a comparision of
   * maps possible on the level of strings.</p>
   */
  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;
  }
});

/* End of WebParameters.js */

