/*
 * 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) 2001 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.util;

import java.util.*;

/**
 * A collection of string-related static methods which are useful in various contexts.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class StringUtil
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

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

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

  /**
   * Performs SQL encoding of an arbitrary string.  When a string is SQL-encoded, all single-quote
   * characters are doubled, and all other characters are left untouched.
   *
   * @param str The string to be SQL-encoded.  May be <B><CODE>null</CODE></B>.
   * @return The SQL-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 encodeStringSQL(String str)
  {
    if (str==null)
      return null;  // safety feature
    int ndx = str.indexOf('\'');
    if (ndx<0)
      return str;
    StringBuffer buf = new StringBuffer();
    while (ndx>=0)
    { // convert each single quote mark to a pair of them
      if (ndx>0)
	buf.append(str.substring(0,ndx));
      buf.append("''");
      str = str.substring(ndx+1);
      ndx = str.indexOf('\'');

    } // end while

    buf.append(str);
    return buf.toString();

  } // end encodeStringSQL

  /**
   * 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

  /**
   * Returns <B><CODE>true</CODE></B> if the given string is <B><CODE>null</CODE></B> or a string of
   * length 0, <B><CODE>false</CODE></B> otherwise.
   *
   * @param s The string to be tested.
   * @return <B><CODE>true</CODE></B> if the given string is <B><CODE>null</CODE></B> or a string of
   *         length 0, <B><CODE>false</CODE></B> otherwise.
   */
  public static final boolean isStringEmpty(String s)
  {
    return ((s==null) || (s.length()==0));

  } // end isStringEmpty

  /**
   * Replaces all instances of a particular substring within a string with another substring.
   * For example:<P>
   * <CODE>replaceAllInstances(&quot;blahdeeblah&quot;,&quot;blah&quot;,&quot;hoo&quot;)</CODE>
   * returns <CODE>&quot;hoodeehoo&quot;</CODE><BR>
   * <CODE>replaceAllInstances(&quot;blahdeebleh&quot;,&quot;blah&quot;,&quot;hoo&quot;)</CODE>
   * returns <CODE>&quot;hoodeebleh&quot;</CODE>
   *
   * @param base The string to be operated on.  If this parameter is <B><CODE>null</CODE></B>, the
   *             method will return <B><CODE>null</CODE></B>.
   * @param find The substring to find in the <CODE>base</CODE> string.  If this parameter is
   *             <B><CODE>null</CODE></B> or an empty string, no replacing will be done.
   * @param replace The substring to replace instances of the <CODE>find</CODE> string with in the
   *                <CODE>base</CODE> string.  If this parameter is <B><CODE>null</CODE></B>, instances
   *                of the <CODE>find</CODE> string will be deleted.
   * @return The <CODE>base</CODE> string with all replacements made as detailed above.
   */
  public static final String replaceAllInstances(String base, String find, String replace)
  {
    if ((base==null) || isStringEmpty(find))
      return base;  // safety feature
    int ndx = base.indexOf(find);
    if (ndx<0)
      return base;  // trivial case
    StringBuffer b = new StringBuffer();
    while (ndx>=0)
    { // break off the first part of the string, then insert the replacement
      if (ndx>0)
	b.append(base.substring(0,ndx));
      if (replace!=null)
	b.append(replace);

      // break off the tail end
      ndx += find.length();
      if (ndx==base.length())
	return b.toString();   // entire string munched - we're done!
      base = base.substring(ndx);
      ndx = base.indexOf(find);

    } // end while

    // append the unmatched "tail" of the string and then return result
    b.append(base);
    return b.toString();

  } // end replaceAllInstances

  /**
   * 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 <B><CODE>null</CODE></B>, the
   *             method will return <B><CODE>null</CODE></B>.
   * @param vars The mapping of variable name to value substitutions.  If this parameter is
   *             <B><CODE>null</CODE></B> 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.size()==0))
      return base;  // safety feature

    String work = base;
    boolean did_replace;
    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();
	if (work.indexOf(VAR_START + vname + VAR_END)>=0)
	{ // OK, this variable is in place
	  work = replaceAllInstances(work,VAR_START + vname + VAR_END,vars.get(vname).toString());
	  did_replace = true;
	  retest = true;

	} // end if

      } // end while

    } while (did_replace);  // end do

    return work;  // all done!

  } // end replaceAllVariables

  public static final String join(Object[] arr, String separator)
  {
    StringBuffer buf = null;
    for (int i=0; i<arr.length; i++)
    { // put it all together
      if (buf==null)
	buf = new StringBuffer(arr[i].toString());
      else
	buf.append(separator).append(arr[i].toString());

    } // end for

    return (buf==null) ? null : buf.toString();

  } // end join

  public static final String join(List l, String separator)
  {
    StringBuffer buf = null;
    Iterator it = l.iterator();
    while (it.hasNext())
    { // put it all together
      Object o = it.next();
      if (buf==null)
	buf = new StringBuffer(o.toString());
      else
	buf.append(separator).append(o.toString());

    } // end for

    return (buf==null) ? null : buf.toString();

  } // end join

  public static final List splitList(String data, String delims)
  {
    if ((data==null) || (delims==null))
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList();
    StringBuffer buf = new StringBuffer();
    boolean accumulate = false;
    for (int i=0; i<data.length(); i++)
    { // look at each character in turn
      char ch = data.charAt(i);
      if (delims.indexOf(ch)>=0)
      { // delimiter character - flush the string if we have one
        if (accumulate)
        { // flush the buffer
          rc.add(buf.toString());
          buf.setLength(0);
          accumulate = false;

        } // end if

      } // end if
      else
      { // ordinary character - accumulate it
        accumulate = true;
        buf.append(ch);

      } // end else

    } // end for

    if (rc.isEmpty())
      return Collections.EMPTY_LIST;
    else
      return Collections.unmodifiableList(rc);

  } // end splitList

  public static final String[] splitArray(String data, String delims)
  {
    List tmp = splitList(data,delims);
    return (String[])(tmp.toArray(new String[0]));

  } // end splitArray

} // end class StringUtil
