package com.sptci.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;
import java.util.zip.GZIPInputStream;

/**
 * Provides utility methods to return <code>String</code> objects
 * from commonly used resources.
 *
 * <p>Copyright 2005 Sans Pareil Technologies, Inc.</p>
 * @author Rakesh Vidyadharan 2005 August 27
 * @version $Id: StringUtilities.java 3010 2007-02-26 12:08:30Z rakesh $
 */
public final class StringUtilities extends Object
{
  /**
   * The <code>end of line</code> characters to use in the returned
   * String objects.
   */
  public static final String endOfLine = 
    System.getProperty( "line.separator" );

  /**
   * Default constructor.  Cannot be instantiated.
   */
  private StringUtilities()
  {
    super();
  }

  /**
   * Return the response body from the specified <code>HTTP URL</code>
   * as a <code>String</code> using the encoding specified by the
   * <code>web server</code>.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String result = StringUtilities.fromUrl( "http://www.w3.org/" );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @see #readResponseFromUrl( HttpURLConnection )
   * @param url The <code>URL</code> from which the contents are to be
   *   retrieved.
   * @throws MalformedURLException If the specified <code>url</code>
   *   is improperly formatted.
   * @throws IOException If errors are encountered while reading the
   *   data.  An IOException is thrown with the error response from the
   *   server if a HTTP error is encountered.
   */
  public static final String fromUrl( String url )
    throws MalformedURLException, IOException
  {
    String result = null;
    HttpURLConnection httpConnection = null;

    try
    {
      httpConnection =
        (HttpURLConnection) ( new URL( url ) ).openConnection();
      httpConnection.setRequestProperty( "Accept-Encoding", "gzip" ); 
      result = readResponseFromUrl( httpConnection );
    }
    finally
    {
      httpConnection.disconnect();
    }

    return result;
  }

  /**
   * Return the response body from the specified <code>HTTP URL</code>
   * as a <code>String</code> using the encoding specified by the 
   * <code>web server</code>.  The response is obtained by 
   * <code>POST</code>'ing the data specified in the 
   * <code>Properties</code> object.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import java.util.Properties;
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        Properties properties = new Properties();
   *        properties.setProperty( "page", "2" );
   *        properties.setProperty( "user", "scott" );
   *        properties.setProperty( "password", "tiger" );
   *        try
   *        {
   *          String result = StringUtilities.fromUrl( 
   *            "http://www.w3.org/", properties );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @see #readResponseFromUrl( HttpURLConnection )
   * @param url The <code>URL</code> from which the contents are to be
   *   retrieved.
   * @param properties A <code>Properties</code> object with the
   *   name-value pairs that are to be submitted to the server.  A
   *   Properties object is used to ensure that the keys and values
   *   are <code>String</code> objects.
   * @throws MalformedURLException If the specified <code>url</code>
   *   is improperly formatted.
   * @throws IOException If errors are encountered while reading the
   *   data.  An IOException is thrown with the error response from the
   *   server if a HTTP error is encountered.
   */
  public static final String fromUrl( String url, 
      Properties properties ) throws MalformedURLException, IOException
  {
    String result = null;
    HttpURLConnection httpConnection = null;
    StringBuilder body = new StringBuilder();

    if ( properties != null )
    {
      for ( Enumeration enumeration = properties.propertyNames();
          enumeration.hasMoreElements(); )
      {
        String name = (String) enumeration.nextElement();
        body.append( name );
        body.append( "=" );
        body.append( properties.getProperty( name ) );
        body.append( "&" );
      }
    }

    try
    {
      httpConnection =
        (HttpURLConnection) ( new URL( url ) ).openConnection();
      httpConnection.setRequestMethod( "POST" );
      httpConnection.setAllowUserInteraction( false );
      httpConnection.setDoOutput( true );
      httpConnection.setRequestProperty( "Accept-Encoding", "gzip" ); 
      httpConnection.setRequestProperty( 
          "Content-type", "application/x-www-form-urlencoded" );
      httpConnection.setRequestProperty( "Content-length", 
          String.valueOf( body.length() - 1 ) );

      if ( body.length() > 0 )
      {
        PrintWriter writer = 
          new PrintWriter( httpConnection.getOutputStream() );
        writer.print( body.substring( 0, body.length() - 1 ) );
        writer.flush();
        writer.close();
      }

      result = readResponseFromUrl( httpConnection );
    }
    finally
    {
      httpConnection.disconnect();
    }

    return result;
  }

  /**
   * Return the response body from the specified <code>HTTP URL</code>
   * as a <code>String</code> using the encoding specified by the
   * <code>web server</code>.  Uses the specified credentials to
   * authenticate the request with the server.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String url = "http://www.w3.org/";
   *          String userName = "scott";
   *          String password = "tiger";
   *          String result = StringUtilities.fromUrl( url, userName, password );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @see #setAuthenticator( String, String )
   * @see #fromUrl( String )
   * @param url The <code>URL</code> from which the contents are to be
   *   retrieved.
   * @param userName The user name to use for authentication with the
   *   server.
   * @param password The password to use for authentication with the
   *   server.
   * @throws MalformedURLException If the specified <code>url</code>
   *   is improperly formatted.
   * @throws IOException If errors are encountered while reading the
   *   data.  An IOException is thrown with the error response from the
   *   server if a HTTP error is encountered.
   */
  public static final String fromUrl( String url, String userName,
      String password ) throws MalformedURLException, IOException
  {
    setAuthenticator( userName, password );
    return fromUrl( url );
  }

  /**
   * Return the response body from the specified <code>HTTP URL</code>
   * as a <code>String</code> using the encoding specified by the 
   * <code>web server</code>.  The response is obtained by 
   * <code>POST</code>'ing the data specified in the 
   * <code>Properties</code> object.  The <code>userName</code> and
   * <code>password</code> values specified are used to authenticate
   * the request with the server.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import java.util.Properties;
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        String url = "http://www.w3.org/";
   *        String user = "scott";
   *        String password = "tiger";
   *        Properties properties = new Properties();
   *        properties.setProperty( "page", "2" );
   *        properties.setProperty( "content", "xml" );
   *        properties.setProperty( "details", "heavy" );
   *        try
   *        {
   *          String result = StringUtilities.fromUrl( 
   *            url, properties, user, password );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @see #setAuthenticator( String, String )
   * @see #fromUrl( String, Properties )
   * @param url The <code>URL</code> from which the contents are to be
   *   retrieved.
   * @param properties A <code>Properties</code> object with the
   *   name-value pairs that are to be submitted to the server.  A
   *   Properties object is used to ensure that the keys and values
   *   are <code>String</code> objects.
   * @param userName The user name to use for authentication with the
   *   server.
   * @param password The password to use for authentication with the
   *   server.
   * @throws MalformedURLException If the specified <code>url</code>
   *   is improperly formatted.
   * @throws IOException If errors are encountered while reading the
   *   data.  An IOException is thrown with the error response from the
   *   server if a HTTP error is encountered.
   */
  public static final String fromUrl( String url, 
      Properties properties, String userName, String password ) 
    throws MalformedURLException, IOException
  {
    setAuthenticator( userName, password );
    return fromUrl( url, properties );
  }

  /**
   * Parse the <code>charset</code> from the <code>content-type</code>
   * HTTP header if it was specified.
   *
   * @param type The <code>content-type</code> header value.
   * @return String - The encoding value if specified.  If it was not
   *   specified, this method returns <code>null</code>.
   */
  public static final String parseEncoding( String type )
  {
    if ( type == null ) return type;

    StringBuilder builder = new StringBuilder();
    int length = type.length();
    int index = type.indexOf( "charset" );
    if ( index != -1 )
    {
      index += 8; // Advance to after charset=
      char currentChar = type.charAt( index );
      
      // Skip leading quotes if any
      if ( currentChar == '"' || currentChar == '\'' )
      {
        ++index;
      }

      currentChar = type.charAt( index );
      while ( currentChar != '\r' && 
          currentChar != '\n' && 
          currentChar != ' ' && 
          currentChar != ';' && 
          currentChar != '\'' && 
          currentChar != '"' && 
          ++index < length )
      {
        builder.append( currentChar );
        currentChar = type.charAt( index );
      }

      // Add last character if there were no quotes or such like
      if ( index == length )
      {
        builder.append( currentChar );
      }
    }

    String result = null;
    if ( builder.length() > 0 )
    {
      result = builder.toString().toUpperCase();
    }

    return result;
  }

  /**
   * Return the contents of the specified <code>file</code> as a 
   * <code>String</code> using the system default encoding.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String result = StringUtilities.fromFile( "/tmp/test.java" );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @param file The <code>file</code> from which the contents are to 
   *   be read.
   * @throws FileNotFoundException If the specified <code>file</code>
   *   could not be found.
   * @throws IOException If errors are encountered while reading the
   *   data.
   */
  public static final String fromFile( String file )
    throws FileNotFoundException, IOException
  {
    BufferedReader reader = new BufferedReader(
        new FileReader( file ) );
    String result = readData( reader );
    reader.close();

    return result;
  }

  /**
   * Return the contents of the specified <code>file</code> as a 
   * <code>String</code> using the character encoding specified.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String result = StringUtilities.fromFile( "/tmp/test.java", "UTF-8" );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @param file The <code>file</code> from which the contents are to 
   *   be read.
   * @param encoding The <code>character encoding</code> scheme to use
   *   to read the file contents.
   * @throws FileNotFoundException If the specified <code>file</code>
   *   could not be found.
   * @throws UnsupportedEncodingException If the specified character
   *   encoding is invalid.
   * @throws IOException If errors are encountered while reading the
   *   data.
   */
  public static final String fromFile( String file, String encoding )
    throws FileNotFoundException, IOException
  {
    BufferedReader reader = new BufferedReader( 
        new InputStreamReader( 
          new FileInputStream( file ), encoding ) );
    String result = readData( reader );
    reader.close();

    return result;
  }

  /**
   * Write the contents of the specified <code>String</code> to the
   * <code>file</code> specified using the system default encoding.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String content = "blah .... blah";
   *          StringUtilities.toFile( content, "/tmp/test.txt" );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @since version 1.16
   * @param content The content that is to be written to file.
   * @param file The <code>fully qualified file name</code> to which 
   *   the contents are to be written.
   * @throws IOException If errors are encountered while writing the
   *   data.
   */
  public static final void toFile( String content, String file )
    throws IOException
  {
    BufferedWriter writer = new BufferedWriter(
        new FileWriter( file ) );
    writer.write( content, 0, content.length() );
    if ( ! content.endsWith( "\n" ) )
    {
      writer.newLine();
    }
    writer.close();
  }

  /**
   * Write the contents of the specified <code>String</code> to the
   * <code>file</code> specified using the character encoding specified.
   *
   * <p>The following code snippet shows how to use this method:</p>
   * <pre>
   *    import org.rakeshv.utils.StringUtilities;
   *
   *        try
   *        {
   *          String content = "blah ... blah";
   *          StringUtilities.toFile( content, "/tmp/test.txt", "UTF-8" );
   *        }
   *        catch ( Throwable t )
   *        {
   *          // Exception handling
   *        }
   * </pre>
   *
   * @since version 1.16
   * @param content The content that is to be written to file.
   * @param file The <code>file</code> from which the contents are to 
   *   be read.
   * @param encoding The <code>character encoding</code> scheme to use
   *   to read the file contents.
   * @throws UnsupportedEncodingException If the specified character
   *   encoding is invalid.
   * @throws IOException If errors are encountered while reading the
   *   data.
   */
  public static final void toFile( String content, String file, 
      String encoding )
    throws UnsupportedEncodingException, IOException
  {
    BufferedWriter writer = new BufferedWriter( 
        new OutputStreamWriter( 
          new FileOutputStream( file ), encoding ) );
    writer.write( content, 0, content.length() );
    if ( ! content.endsWith( "\n" ) )
    {
      writer.newLine();
    }
    writer.close();
  }

  /**
   * Returns the <code>StackTrace</code> for the specified instance
   * of <code>Throwable</code>.
   *
   * @param throwable The instance whose stack trace is desired.
   * @return String - The stack trace for the throwable instance.
   */
  public static final String stackTrace( Throwable throwable )
  {
    StringBuilder builder = new StringBuilder( 1024 );

    builder.append( throwable.getMessage() );
    builder.append( endOfLine );
    printTrace( throwable, builder );

    // Loop through the causes if they exist
    Throwable throwable1 = throwable.getCause();
    while ( throwable1 != null )
    {
      builder.append( "Caused by: " );
      builder.append( throwable1.toString() );
      builder.append( endOfLine );
      printTrace( throwable1, builder );
      throwable1 = throwable1.getCause();
    }

    return builder.toString();
  }

  /**
   * Replace XML/HTML special characters (&amp;, &lt;, ...) in the 
   * String specified with their escaped equivalents.
   *
   * @param content - The <code>String</code> that
   *   is to be processed.
   * @return String - The modified <code>String</code> object.
   */
  public static final String toXML( String content )
  {
    if ( content == null ) return content;

    char contentChars[] = content.toCharArray();
    StringBuilder builder = new StringBuilder( contentChars.length + 256 );
    for ( int i = 0; i < contentChars.length; ++i )
    {
      switch( contentChars[i] )
      {
        case '&':
          // Check to see if the ampersand has already been escaped
          if ( i + 4 < contentChars.length )
          {
            if ( contentChars[i + 1] == 'a' &&
                contentChars[i + 2] == 'm' &&
                contentChars[i + 3] == 'p' &&
                contentChars[i + 4] == ';' )
            {
              builder.append( contentChars[i] );
            }
            else
            {
              builder.append( "&amp;" );
            }
          }
          else
          {
            builder.append( "&amp;" );
          }
          break;
        case '<':
          builder.append("&lt;");
          break;
        case '>':
          builder.append("&gt;");
          break;
        case '\'':
        case '`':
          builder.append("&apos;");
          break;
        case '"':
          builder.append("&quot;");
          break;
        case '\\':
          builder.append("&#92;");
          break;
        case (char)133:
          builder.append("&#133;");
          break;
        default:
          builder.append( contentChars[i] );
          break;
      }
    }
  
    return builder.toString();
  }

  /**
   * Replace escaped XML/HTML special characters (&amp;, &lt;, ...) in 
   * the String specified with their original equivalents.
   *
   * @since version 1.16
   * @param content - The <code>String</code> that
   *   is to be processed.
   * @return String - The modified <code>String</code> object.
   */
  public static final String fromXML( String content )
  {
    if ( content == null ) return content;

    char contentChars[] = content.toCharArray();
    StringBuilder builder = new StringBuilder( contentChars.length );
    for ( int i = 0; i < contentChars.length; ++i )
    {
      boolean processed = false;
      if ( contentChars[i] == '&' )
      {
        // Check for escaped ampersand and backslash
        if ( i + 5 < contentChars.length )
        {
          // Check for escaped apostrophe and quote and 133
          if ( contentChars[i + 1] == 'a' &&
              contentChars[i + 2] == 'p' &&
              contentChars[i + 3] == 'o' &&
              contentChars[i + 4] == 's' &&
              contentChars[i + 5] == ';' )
          {
            builder.append( '\'' );
            processed = true;
            i += 5;
          } 
          else if ( contentChars[i + 1] == 'q' &&
              contentChars[i + 2] == 'u' &&
              contentChars[i + 3] == 'o' &&
              contentChars[i + 4] == 't' &&
              contentChars[i + 5] == ';' )
          {
            builder.append( '"' );
            processed = true;
            i += 5;
          } 
          else if ( contentChars[i + 1] == '#' &&
              contentChars[i + 2] == '1' &&
              contentChars[i + 3] == '3' &&
              contentChars[i + 4] == '3' &&
              contentChars[i + 5] == ';' )
          {
            builder.append( (char) 133 );
            processed = true;
            i += 5;
          }
        }
        if ( i + 4 < contentChars.length && ! processed )
        {
          if ( contentChars[i + 1] == 'a' &&
              contentChars[i + 2] == 'm' &&
              contentChars[i + 3] == 'p' &&
              contentChars[i + 4] == ';' )
          {
            builder.append( contentChars[i] );
            processed = true;
            i += 4;
          } 
          else if ( contentChars[i + 1] == '#' &&
              contentChars[i + 2] == '9' &&
              contentChars[i + 3] == '2' &&
              contentChars[i + 4] == ';' )
          {
            builder.append( '\\' );
            processed = true;
            i += 4;
          }
        }
        if ( i + 3 < contentChars.length && ! processed )
        {
          // Check for escaped less than or greater than
          if ( i + 3 < contentChars.length )
          {
            if ( contentChars[i + 1] == 'l' &&
                contentChars[i + 2] == 't' &&
                contentChars[i + 3] == ';' )
            {
              builder.append( '<' );
              processed = true;
              i += 3;
            }
            else if ( contentChars[i + 1] == 'g' &&
                contentChars[i + 2] == 't' &&
                contentChars[i + 3] == ';' )
            {
              builder.append( '>' );
              processed = true;
              i += 3;
            }
          }
        }
        if ( ! processed )
        {
          builder.append( contentChars[i] );
        }
      }
      else
      {
        builder.append( contentChars[i] );
      }
    }
  
    return builder.toString();
  }

  /**
   * Read the response from the <code>HttpURLConnection</code>.  Check
   * the <code>Content-encoding</code> response header to see if the
   * response is <code>gzip</code> encoded.  If it is, use a
   * <code>GZIPInputStream</code> to read the response.
   *
   * @param httpConnection The <code>HttpURLConnection</code> from
   *   which the response is to be read.
   * @throws IOException If errors are encountered while reading the
   *   response from the server.
   */
  private static final String readResponseFromUrl( 
      HttpURLConnection httpConnection ) throws IOException
  {
    String result = null;
    BufferedReader reader = null;
    String encoding = parseEncoding( httpConnection.getContentType() );

    boolean gzipped = false;
    String value = httpConnection.getHeaderField( "Content-Encoding" );
    if ( value == null )
    {
      value = httpConnection.getHeaderField( "Content-encoding" );
    }
    if ( value != null && value.equals( "gzip" ) )
    {
      gzipped = true;
    }

    if ( httpConnection.getResponseCode() == HttpURLConnection.HTTP_OK )
    {

      if ( encoding == null )
      {
        if ( gzipped )
        {
          reader = new BufferedReader( 
              new InputStreamReader( 
                new GZIPInputStream( 
                  httpConnection.getInputStream() ) ) );
        }
        else
        {
          reader = new BufferedReader( 
              new InputStreamReader( 
                httpConnection.getInputStream() ) );
        }
      }
      else
      {
        if ( gzipped )
        {
          reader = new BufferedReader( 
              new InputStreamReader( 
                new GZIPInputStream( httpConnection.getInputStream() ), 
                encoding.toUpperCase() ) );
        }
        else
        {
          reader = new BufferedReader( 
              new InputStreamReader( 
                httpConnection.getInputStream(), 
                encoding.toUpperCase() ) );
        }
      }

      result = readData( reader );
      reader.close();
    }
    else
    {
      InputStream inputStream = httpConnection.getErrorStream();
      if ( inputStream != null )
      {
        if ( gzipped )
        {
          reader = new BufferedReader( 
              new InputStreamReader( 
                new GZIPInputStream( inputStream ) ) );
        }
        else
        {
          reader = new BufferedReader( 
              new InputStreamReader( inputStream ) );
        }
        String error = readData( reader );
        reader.close();
        throw new IOException( error );
      }
      else
      {
        throw new IOException( "HTTP Error " + httpConnection.getResponseCode() );
      }
    }

    return result;
  }

  /**
   * Create a new instance of <code>Authenticator</code> using the
   * specified credentials.  Create an anonymous sub-class of
   * <code>Authenticator</code> that implements the <code>
   * getPasswordAuthentication</code> method by returning a new 
   * instance of <code>PasswordAuthentication</code> using the
   * <code>userName</code> and <code>password</code> specified.
   *
   * @param userName The user name to use for authentication.
   * @param password The password to use for authentication.
   */
  private static final void setAuthenticator( final String userName,
      final String password )
  {
    Authenticator.setDefault(
        new Authenticator()
        {
          protected PasswordAuthentication getPasswordAuthentication()
          {
            return new PasswordAuthentication( 
              userName, password.toCharArray() );
          }
        }
        );
  }

  /**
   * Returns all the data from the specified <code>BufferedReader</code>.
   *
   * @param reader The <code>BufferedReader</code> from which the
   *   data is to be read.
   * @return String - The data read from the specified BufferedReader.
   * @throws IOException If errors are encountered while reading the
   *   data.
   */
  private static String readData( BufferedReader reader )
    throws IOException
  {
    StringBuilder builder = new StringBuilder( 8192 );
    String line = null;
    while ( ( line = reader.readLine() ) != null )
    {
      builder.append( line );
      builder.append( endOfLine );
    }

    return builder.toString();
  }

  /**
   * Return the <code>stack trace</code> for the specified instance
   * of <code>Throwable</code>.
   *
   * @param throwable The instance of <code>Throwable</code>
   *   whose <code>stack trace</code> is to be retrieved.
   * @param builder The StringBuilder to which the stack trace is to
   *   be appended.
   * @return StringBuilder - The reference to the StringBuilder that 
   *   was passed in for convenience.
   */
  private static final StringBuilder printTrace( Throwable throwable, 
      StringBuilder builder )
  {
    StackTraceElement [] ste = throwable.getStackTrace();

    for ( int i = 0; i < ste.length; ++i )
    {
      builder.append( "\tat " );
      builder.append( ste[i].getClassName() );
      builder.append( "." );
      builder.append( ste[i].getMethodName() );
      builder.append( "(" );
      builder.append( ste[i].getFileName() );
      builder.append( ":" );
      builder.append( ste[i].getLineNumber() );
      builder.append( ")" );
      builder.append( endOfLine );
    }

    return builder;
  }
}
