/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002-03 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.util;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import org.apache.log4j.Logger;

/**
 * Common string utilities.  Inherits from the <CODE>org.apache.commons.lang.StringUtils</CODE>
 * class, so it contains everything that contains.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class StringUtils extends org.apache.commons.lang.StringUtils
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Logger logger = Logger.getLogger(StringUtils.class);

  private static final char[] HTML_ENCODE_CHARS = { '"', '&', '<', '>' };

  private static final String VAR_START = "${";
  private static final String VAR_END = "}";

  private static final Set TRUE_STRINGS;
  private static final Set FALSE_STRINGS;

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * StringUtils instances should NOT be constructed in standard programming.
   * Instead, the class should be used as <code>StringUtils.trim(" foo ");</code>.
   * This constructor is public to permit tools that require a JavaBean instance
   * to operate.
   */
  public StringUtils()
  {
    super();

  } // end constructor

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Performs HTML encoding of an arbitrary string.  When a string is HTML encoded, the double-quote (&quot;),
   * ampersand (&amp;), less-than (&lt;), and greater-than (&gt;) characters are transformed to their HTML
   * entity equivalents; all other characters are left untouched.
   *
   * @param str The string to be HTML-encoded.  May be <B><CODE>null</CODE></B>.
   * @return The HTML-encoded equivalent of <CODE>str</CODE>.  If <CODE>str</CODE> is
   *         <B><CODE>null</CODE></B>, returns <B><CODE>null</CODE></B>.
   */
  public static final String encodeHTML(String str)
  {
    if (str==null)
      return null;  // safety feature
    AnyCharMatcher nhc = new AnyCharMatcher(HTML_ENCODE_CHARS);
    int ndx = nhc.get(str);
    if (ndx<0)
      return str;   // trivial short-circuit case
    StringBuffer buf = new StringBuffer();
    while (ndx>=0)
    { // append the matched "head" and then the encoded character
      if (ndx>0)
	buf.append(str.substring(0,ndx));
      switch (str.charAt(ndx++))
      {
	case '"':
	  buf.append("&quot;");
	  break;

	case '&':
	  buf.append("&amp;");
	  break;

	case '<':
	  buf.append("&lt;");
	  break;

	case '>':
	  buf.append("&gt;");
	  break;

      } // end switch

      if (ndx==str.length())
	return buf.toString();  // munched the entire string - all done!
      str = str.substring(ndx);
      ndx = nhc.get(str);

    } // end while

    buf.append(str);  // append the unmatched tail
    return buf.toString();

  } // end encodeHTML

  /**
   * Translates a string into <CODE>application/x-www-form-urlencoded</CODE> format, using a UTF-8 encoding.
   *
   * @param s The string to be encoded.  May be <CODE>null</CODE>.
   * @return The translated string value, or <CODE>null</CODE> if <CODE>null</CODE> was passed in.
   */
  public static final String encodeURL(String s)
  {
    try
    { // the old URLEncoder.encode(str) method is deprecated as of JDK1.4, use the new one
      return ((s==null) ? null : URLEncoder.encode(s,"UTF-8"));

    } // end try
    catch (UnsupportedEncodingException e)
    { // this should never happen, but we gotta catch it anyway
      logger.fatal("WTF?  encodeURL doesn't support UTF-8?  You're crazy!");
      return null;

    } // end catch

  } // end encodeURL

  /**
   * Returns <CODE>true</CODE> if the given string is a representation of a Boolean <CODE>true</CODE> value.
   * Values that represent a valid Boolean <CODE>true</CODE> value include "true", "yes", "on", and "1".
   *
   * @param test The string to be tested.
   * @return <CODE>true</CODE> if the string represents a Boolean <CODE>true</CODE> value, <CODE>false</CODE> if not.
   */
  public static final boolean isBooleanTrue(String test)
  {
    if (test==null)
      return false;
    return TRUE_STRINGS.contains(test.trim().toLowerCase());

  } // end isBooleanTrue

  /**
   * Returns <CODE>true</CODE> if the given string is a representation of a Boolean <CODE>false</CODE> value.
   * Values that represent a valid Boolean <CODE>false</CODE> value include "false", "no", "off", and "0".
   *
   * @param test The string to be tested.
   * @return <CODE>true</CODE> if the string represents a Boolean <CODE>false</CODE> value, <CODE>false</CODE> if not.
   */
  public static final boolean isBooleanFalse(String test)
  {
    if (test==null)
      return false;
    return FALSE_STRINGS.contains(test.trim().toLowerCase());

  } // end isBooleanTrue

  /**
   * Replaces variable substitutions in a string.  Variable substitutions are strings of the form
   * <CODE>${<EM>varname</EM>}</CODE>.  The <EM>varname</EM> names are looked up in the supplied
   * <CODE>Map</CODE>, and the values of variables in that map are substituted.<P>
   * Only variable names that exist in the <CODE>Map</CODE> are replaced; other variable strings
   * in the supplied string are left untouched.  Variable substitution values may themselves contain
   * variables; those variables are recursively replaced.  (<B><EM>Caution:</EM></B> The code cannot
   * detect variable substitutions that contain themselves, or two variables that contain each other.
   * Avoid these situations.)
   *
   * @param base The string to be operated on.  If this parameter is <CODE>null</CODE>, the
   *             method will return <CODE>null</CODE>.
   * @param vars The mapping of variable name to value substitutions.  If this parameter is
   *             <CODE>null</CODE> or an empty map, no substitutions will be performed on
   *             <CODE>base</CODE>.
   * @return The <CODE>base</CODE> string with all variable substitutions made as detailed above.
   */
  public static final String replaceAllVariables(String base, Map vars)
  {
    if ((base==null) || (vars==null) || vars.isEmpty())
      return base;  // safety feature

    String work = base;
    boolean did_replace = false;
    boolean retest = true;
    do
    { // main loop for replacing all variables
      did_replace = false;
      Iterator it = vars.keySet().iterator();
      while (it.hasNext())
      { // variable start is there...
	if (retest)
	{ // only perform this test on the first iteration and after we know we've replaced a variable
	  if (work.indexOf(VAR_START)<0)
	    return work; // no more variables in text - all done!
	  retest = false;

	} // end if

	// get variable name and see if it's present
	String vname = it.next().toString();
	String var_full = VAR_START + vname + VAR_END;
	if (work.indexOf(var_full)>=0)
	{ // OK, this variable is in place
	  work = replace(work,var_full,vars.get(vname).toString());
	  did_replace = true;
	  retest = true;

	} // end if

      } // end while

    } while (did_replace);  // end do

    return work;  // all done!

  } // end replaceAllVariables

  /**
   * Splits the provided text into a list, based on a given separator. The separator is not included in the
   * returned String list. The maximum number of splits to perfom can be controlled.
   *
   * @param text The string to parse.
   * @param separator Character used as the delimiter.
   * @param limit The maximum number of elements to include in the list. A zero or negative value implies no limit.
   * @return A list of parsed Strings.
   */
  public static final List split1List(String text, char separator, int limit)
  {
    if (text==null)
      return Collections.EMPTY_LIST;
    if (limit<=0)
      limit = Integer.MAX_VALUE;
    ArrayList rc = new ArrayList();
    String work = text;
    int p = work.indexOf(separator);
    while ((work.length()>0) && (p>=0) && (--limit>0))
    { // add elements to the ArrayList
      if (p==0)
	rc.add("");
      else
	rc.add(work.substring(0,p));
      work = work.substring(p+1);
      p = work.indexOf(separator);

    } // end while

    rc.add(work);
    rc.trimToSize();
    return rc;

  } // end split1list

  /**
   * Splits the provided text into a list, based on a given separator. The separator is not included in the
   * returned String list.
   *
   * @param text The string to parse.
   * @param separator Character used as the delimiter.
   * @return A list of parsed Strings.
   */
  public static final List split1List(String text, char separator)
  {
    return split1List(text,separator,0);

  } // end split1list

  /**
   * Splits the provided text into a list, based on a given separator. The separator is not included in the
   * returned String array. The maximum number of splits to perfom can be controlled.
   *
   * @param text The string to parse.
   * @param separator Character used as the delimiter.
   * @param limit The maximum number of elements to include in the list. A zero or negative value implies no limit.
   * @return An array of parsed Strings.
   */
  public static final String[] split1(String text, char separator, int limit)
  {
    List tmp = split1List(text,separator,limit);
    String[] rc = new String[tmp.size()];
    tmp.toArray(rc);
    return rc;

  } // end split1

  /**
   * Splits the provided text into a list, based on a given separator. The separator is not included in the
   * returned String array.
   *
   * @param text The string to parse.
   * @param separator Character used as the delimiter.
   * @return An array of parsed Strings.
   */
  public static final String[] split1(String text, char separator)
  {
    return split1(text,separator,0);

  } // end split1

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  { // get the boolean values resource
    ResourceBundle bv = ResourceBundle.getBundle("com.silverwrist.util.BooleanValues");

    // Load the Boolean "true" values.
    String[] values = split(bv.getString("values.true"),"|");
    HashSet tmp = new HashSet();
    for (int i=0; i<values.length; i++)
      tmp.add(values[i].toLowerCase());
    tmp.add("1");     // always add the default English values, too
    tmp.add("true");
    tmp.add("yes");
    tmp.add("on");
    TRUE_STRINGS = Collections.unmodifiableSet(tmp);

    // Load the Boolean "false" values.
    values = split(bv.getString("values.false"),"|");
    tmp = new HashSet();
    for (int i=0; i<values.length; i++)
      tmp.add(values[i].toLowerCase());
    tmp.add("0");    // always add the default English values, too
    tmp.add("false");
    tmp.add("no");
    tmp.add("off");
    FALSE_STRINGS = Collections.unmodifiableSet(tmp);

  } // end static initializer

} // end class StringUtils
