/* 
 * <copyright> 
 *  Copyright (c) 2001 by Hyperwave AG
 * </copyright> 
 * 
 * <file> 
 *  Name:        ScopedMap.js
 *  Created:     2001-01-14
 *  $Id: ScopedMap.js,v 1.2.2.1 2004/10/12 08:29:25 mmair Exp $
 * </file>
 */

initPackage ( "com.hyperwave.res" );

//----------------------------------------------------------------------
/**
 * This class provides a <i>scope chain</i> of maps from string keys
 * to any objects. If a key is searched in the scoped map firstly
 * the top map is searched. If nothing is found the next map
 * of the chain is searched and so forth.
 * This structure simulates inheritance and overloading mechanisms
 * used in JavaScript.
 * @author Simon Zwoelfer
 */
// <JSClass Name="com.hyperwave.res.ScopedMap">

  //--------------------------------------------------------------------
  /**
    * Constructs a new ScopedMap by means of a given one
    * or, if none is given, an empty ScopedMap.
    * @param aParam: ScopedMap | void: this value can either
    *   be <code>null</code> or an existing stack can be
    *   used to construct a new one by copying all references
    *   of the old one. Hence the new stack will not influence
    *   the old stack but has the same conent!
    * @param theTop: Object | void: a Map with already defined properties
    *   which should reside on the top of the ScopedMap
    */
  com.hyperwave.res.ScopedMap = function ( aParam, theTop )
  {
    if ( aParam == "__proto__" )
      return;

    if ( !theTop )
      theTop = {};
    this.top_ = theTop;

//  Use JavaScript internals:
//    if ( aParam != null && aParam.top_ != null )
//      this.top_.__proto__ = aParam.top_;
//    
//    return;


    // Array implementation ...
    if ( aParam != null && aParam.stack_ != null )
      this.stack_ = [this.top_].concat ( aParam.stack_ );
    else
      this.stack_ = [this.top_];

  }
  class$ = doInherit ( com.hyperwave.res.ScopedMap );

  //--------------------------------------------------------------------
  /**
   * Get a value by means of a given key, or null if the key is not
   * present in any map of the stack. The most upper key is found.
   * The key parameter may contain modifiers, but the returned value is not transformed.
   * @param aKey: String: the key which is to be searched (including modifiers) using the syntax:
   *    modifier1:modifier2:...:key
   * @return Object: the value of the given key or <CODE>null</CODE>
   *                 if the key was not found
   */
  class$.get = function ( aKey )
  {
//  Use JavaScript internals:
//    return this.top_ [aKey];
    var key = aKey;
    
    // parse placeholder modifiers
    if (aKey.indexOf(":") != -1) {
      var key_items = aKey.split(":");
      key = key_items[key_items.length - 1];
    }
    
    for (var map_ctr = 0; map_ctr < this.stack_.length; ++map_ctr) {
      var cur_map = this.stack_ [map_ctr];
      var value = cur_map [ key ];
      
      // key does not exist, lookup using original key (including modifiers)
      if (value == null)
        value = cur_map[aKey];
        
      if (value != null) {
        return value;
      }
    }
    return null;
  }
  
  //--------------------------------------------------------------------
  /**
   * Get a value by means of a given key, or null if the key is not
   * present in any map of the stack. The most upper key is found.
   * The key parameter may contain modifiers, which cause a set of 
   * transformations to the given value, if it is a string. 
   * The transformations are applied in reverse order.
   * Currently, the following modifiers are supported:
   * html:    escape string in HTML context
   * single:  escape string in single-quoted string context
   * double:  escape string in double-quoted string context
   * @param aKey: String: the key which is to be searched (including modifiers) using the syntax:
   *    modifier1:modifier2:...:key
   * @return Object: the value of the given key or <CODE>null</CODE>
   *                 if the key was not found
   */
  class$.getModified = function ( aKey )
  {
    try {
      var modifiers = this.static_.getModifiers(aKey);
      var value = this.get(aKey);
      return this.static_.applyModifiers(modifiers, value); 
    }
    catch (ex) {
      return "($UNKNOWN_MODIFIER:" + aKey + ")";
    }
  }
  //--------------------------------------------------------------------
  /**
   * Set a value of the map stack. This always influences the top
   * map!
   * @param aKey: String: the key which should be set
   * @param aValue: Object: the associated value
   */
  class$.set = function ( aKey, aValue )
  {
    this.top_ [aKey] = aValue;
  }

  //--------------------------------------------------------------------
  /**
   * Returns an array of modifiers specified by the given key
   * Currently, the following modifiers are supported:
   * html:    escape string in HTML context
   * single:  escape string in single-quoted string context
   * double:  escape string in double-quoted string context
   * @param aKey: String: the key, also containing the modifiers
   * @return String[]: an array of modifiers, or null if no modifiers specified
   */
  class$.static_.getModifiers = function ( aKey )
  {
    // parse placeholder modifiers
    if (aKey.indexOf(":") != -1) {
      var modifiers = new Array();
      var key_items = aKey.split(":");
      for (var i = 0; i < key_items.length - 1; i++) {
        modifiers[i] = key_items[i];
        if (typeof(this.modifierRegistry[modifiers[i]]) != "function")
          throw new com.hyperwave.res.ResourceException (
        "com.hyperwave.res.ScopedMap: modifier " + modifiers[i] + " is not defined!" ); 
      }
      return modifiers;
    }
    return null;
  }
  //--------------------------------------------------------------------
  /**
   * Applies a set of transformations to the given string value. 
   * The transformations are specified by an array of string modifiers 
   * and are applied in reverse order.
   * Currently, the following modifiers are supported:
   * html:    escape string in HTML context
   * single:  escape string in single-quoted string context
   * double:  escape string in double-quoted string context
   * @param modifiers: String[]: an array of modifiers, or null if no modifiers specified
   * @param value: String: the string which needs to be transformed
   * @return String: the transformed value
   */
  class$.static_.applyModifiers = function ( modifiers, value )
  {
    if (modifiers == null)
      return value;
    if (typeof(modifiers.length) != "undefined" && modifiers.length > 0) {
      for (var j = modifiers.length - 1; j >= 0; j--) {
        var handler = this.modifierRegistry[modifiers[j]];
        if (typeof(handler) == "function" && typeof(value) == "string") {
          value = handler(value);
        } 
      }
      return value;
    }
    else {
      return value;
    }
  }

  //--------------------------------------------------------------------
  /**
   * Primitive test for the class.
   * @return String[]: an array with string errors for the class
   */
  class$.static_.test = function ()
  {
    var errors = [];
    var map_s1 = new com.hyperwave.res.ScopedMap ();
    if ( map_s1.get ( "a" ) != null )
      errors [errors.length] = "(1) Empty Stack gives values";

    // Test Basic Map:
    map_s1.set ( "a", "a" );
    map_s1.set ( "b", "b" );
    if ( map_s1.get ( "a" ) != "a" ||
         map_s1.get ( "b" ) != "b" ||
         map_s1.get ( "c" ) != null )
      errors [errors.length] = "(2) Basic access does not work";

    map_s1.set ( "c", "x" );
    if ( map_s1.get ( "a" ) != "a" ||
         map_s1.get ( "b" ) != "b" ||
         map_s1.get ( "c" ) != "x" )
      errors [errors.length] = "(3) Basic access after put does not work";
    
    // Create new Stack by means of Stack above and put
    // overriding values in it ...
    var map_s2 = new com.hyperwave.res.ScopedMap ( map_s1 );
    map_s2.set ( "c", "c" );
    map_s2.set ( "a", "a2" );
    if ( map_s2.get ( "a" ) != "a2" ||
         map_s2.get ( "b" ) != "b" ||
         map_s2.get ( "c" ) != "c" )
      errors [errors.length] = "(4) Overridden access does not work";

    map_s1.set ( "x", "s1x" );
    map_s1.set ( "y", "s1y" );
    map_s2.set ( "x", "s2x" );
    map_s2.set ( "z", "s2z" );
    if ( map_s1.get ( "z" ) != null ||
         map_s2.get ( "x" ) != "s2x" ||
         map_s2.get ( "z" ) != "s2z" ||
         map_s2.get ( "y" ) != "s1y" )
      errors [errors.length] = "(5) Overridden access after put does not work";

    var map_s3 = new com.hyperwave.res.ScopedMap ( map_s2, { "x": "xx" } );    
    if ( map_s3.get ( "xx" != "xx" ) )
      errors [errors.length] = "(6) Overridden access after put does not work";
         
    return errors;
  }
  
  //--------------------------------------------------------------------
  /**
   * Tests the class.
   */
  class$.static_.main = function ()
  {
    var errors = this.test ();
    if ( errors.length == 0 )
      writeln ( "Test of ScopedMap ok" );
    else
      writeln ( "Test of ScopedMap gives the following Errors: \n  " + 
                errors.join ( "\n  " ) + "\n" );
  }
  
  //--------------------------------------------------------------------
  /**
   * Escapes a string in an HTML context.
   * @param string: String: the string to transform
   * @return String: the escaped string
   */
  class$.static_.escapeHtml = function (string)
  {
    if (typeof string != "string") 
      return string;
    var outstr = string;
    outstr = outstr.replace(/&/g, '&amp;');
    outstr = outstr.replace(/</g, "&lt;");
    outstr = outstr.replace(/>/g, "&gt;");
    outstr = outstr.replace(/\"/g, '&quot;');
    outstr = outstr.replace(/\'/g, '&#39;');
    return outstr;
  }
  
  //--------------------------------------------------------------------
  /**
   * Escapes a string in a single quoted string context.
   * Additionally, splits occurences of <string> and </string> tags.
   * @param string: String: the string to transform
   * @return String: the escaped string
   */
  class$.static_.escapeSingleString = function(string)
  {
    if (typeof string != "string") 
      return string;
    var outstr = string;
    outstr = outstr.replace(/\\/g, "\\\\");
    outstr = outstr.replace(/\n/g, "\\n");
    outstr = outstr.replace(/\r/g, '\\r');
    outstr = outstr.replace(/\t/g, '\\t');
    
    outstr = outstr.replace(/\'/g, "\\'");
    outstr = outstr.replace(/(<\/?scr)(ipt[^>]*>)/gi, "$1' + '$2");
  
    return outstr;
  }  
  
  //--------------------------------------------------------------------
  /**
   * Escapes a string in a double quoted string context.
   * Additionally, splits occurences of <string> and </string> tags.
   * @param string: String: the string to transform
   * @return String: the escaped string
   */
  class$.static_.escapeDoubleString = function(string)
  {
    if (typeof string != "string") 
      return string;
    var outstr = string;
    outstr = outstr.replace(/\\/g, "\\\\");
    outstr = outstr.replace(/\n/g, "\\n");
    outstr = outstr.replace(/\r/g, '\\r');
    outstr = outstr.replace(/\t/g, '\\t');

    outstr = outstr.replace(/\"/g, '\\"');
    outstr = outstr.replace(/(<\/?scr)(ipt[^>]*>)/gi, '$1" + "$2');

    return outstr;
  }
  
  /**
   * The registry that maps placeholder modifiers to their handlers 
   */
  class$.static_.modifierRegistry = {
          "html"  : class$.static_.escapeHtml, // Handler for HTML modifier
          "single": class$.static_.escapeSingleString, // Handler for single-quoted string modifier
          "double": class$.static_.escapeDoubleString // Handler for double-quoted string modifier
  };

// </JSClass>
//----------------------------------------------------------------------

/* End of "com.hyperwave.res.ScopedMap.js" */

