package edu.northwestern.at.utils;

/*  Please see the license information at the end of this file. */

import java.io.*;
import java.util.*;

/** UTF8Properties -- A UTF8 compatible version of Java Properties.
 *
 *  <p>
 *  Regular Java Properties must be read and written using the
 *  ISO8859-1 character encoding.  UTF8Properties reads and writes
 *  the properties using the utf-8 character encoding instead.
 *  </p>
 */

public class UTF8Properties
    extends Properties
    implements TaggedStrings, Serializable
{
    /** Allowed separators between key and value of a property.
     */

    protected static final String keyValueSeparators = "=: \t\r\n\f";

    /** Create empty property list with no default values.
     */

    public UTF8Properties()
    {
        this( null );
    }

    /** Create empty property list with specified defaults.
     *
     * @param   defaults    Default property values.
     */

    public UTF8Properties( Properties defaults )
    {
        this.defaults   = defaults;
    }

    /** Reads a property list (key and element pairs) from a utf-8 encoded
     *  input stream.
     *
     *  @param      inputStream     The input stream.
     *  @param      defaultValue    The default value when none specified.
     *
     *  @throws     IOException     When an error occurs while reading
     *                              from the input stream.
     *
     *  @throws     IllegalArgumentException
     *
     *              When the input stream contains an invalid Unicode
     *              escape sequence.
     *
     *  <p>
     *  The default value is assigned to any key which does not
     *  have a value.  Assume the default value is the string "missing".
     *  Then the input line
     *  </p>
     *
     *  <p><code>x=1</code></p>
     *
     *  <p>
     *  returns a key of "x" with value of "1", while the input line
     *  </p>
     *
     *  <p><code>x</code></p>
     *
     *  <p>
     *  returns a key of "x" with value of "missing" .
     *  </p>
     */

    public synchronized void load
    (
        InputStream inputStream ,
        String defaultValue
    )
        throws IOException
    {
                                //  Wrap provided input stream
                                //  with a buffered reader.
        BufferedReader in   =
            new BufferedReader
            (
                new UnicodeReader( inputStream , "utf-8" )
            );
                                //  Read first input line.

        String line = in.readLine();

                                //  While there are still input lines ...

        while ( line != null )
        {
                                //  Remove leading whitespace from
                                //  this line.

            line    = removeLeadingWhiteSpace( line );

                                //  Ignore empty lines and comments,

            if ( !line.equals( "" ) && ( line.charAt( 0 ) != '#' ) )
            {
                                //  First or only line of property.

                String property = line;

                                //  Pick up all continuation lines and
                                //  merge them together into one big
                                //  string.

                while ( isContinued( line ) )
                {
                    property    =
                        property.substring( 0 , property.length() - 1 );

                    line        = in.readLine();
                    property    += line;
                }
                                //  If the property line isn't empty,
                                //  split it into the key and the value.

                if ( !property.equals( "" ) )
                {
                    int endOfKey    = 0;

                                //  Find end of key.

                    while   (   ( endOfKey < property.length() ) &&
                                ( keyValueSeparators.indexOf(
                                    property.charAt( endOfKey ) ) == -1 ) )
                    {
                        endOfKey++;
                    }
                                //  Extract key and value.

                    String key      = property.substring( 0 , endOfKey );
                    String value    = defaultValue;

                                //  Assign default value if none
                                //  found in the property line.

                    if ( ( endOfKey + 1 ) <= property.length() )
                    {
                        value   =
                            property.substring(
                                endOfKey + 1 , property.length() );
                    }
                                //  Convert any escaped characters to
                                //  regular characters in key and value.

                    key     = convertInputString( key );
                    value   =
                        convertInputString(
                            removeLeadingWhiteSpace( value ) );

                                //  Store the processed key and value.

                    put( key , value );
                }
            }
                                //  Read next input line if any.

            line    = in.readLine();
        }
    }

    /** Reads a property list (key and element pairs) from a utf-8 encoded
     *  input stream.
     *
     *  @param      inputStream     The input stream.
     *
     *  @throws     IOException     When an error occurs while reading
     *                              from the input stream.
     *
     *  @throws     IllegalArgumentException
     *
     *              When the input stream contains an invalid Unicode
     *              escape sequence.
     *
     *  <p>
     *  Null is used as the default value for any key without a
     *  specified value.
     *  </p>
     */

    public synchronized void load( InputStream inputStream )
        throws IOException
    {
        load( inputStream , null );
    }

    /** Remove leading white space from a string.
     *
     *  @param  line        The string from which to remove leading
     *                      white space.
     *
     *  @return         String with leading while space removed.
     */

    public static String removeLeadingWhiteSpace( String line )
    {
        int index = 0;

        while   (   ( index < line.length() ) &&
                    ( keyValueSeparators.indexOf(
                        line.charAt( index ) ) != -1 )
                )
        {
            index++;
        }

        return line.substring( index , line.length() );
    }

    /** Replace all characters preceded by a '\' with the corresponding
     *  special character and convert unicode escape sequences to
     *  normal characterS.
     *
     *  @param  line        String for which to process escape sequences.
     *
     *  @return             The processed string.
     */

    public static String convertInputString( String line )
    {
                                //  Accumulates decoded input characters.

        StringBuffer sb = new StringBuffer( line.length() );

                                //  Loop over each character in the
                                //  input string.

        for ( int index = 0 ; index < line.length() ; index++ )
        {
                                //  Get next character in input string.

            char currentChar    = line.charAt( index );

                                //  Decode escaped characters.  These
                                //  start with a "\".

            if ( currentChar == '\\' )
            {
                                //  Skip past leading "\".
                index++;
                                //  Get the escaped character.

                currentChar = line.charAt( index );

                                //  Convert escaped character to
                                //  its internal representation.

                switch ( currentChar )
                {
                                //  Form feed.

                    case 'f':
                        currentChar = '\f';
                        break;

                                //  Line feed.

                    case 'n':
                        currentChar = '\n';
                        break;

                                //  Carriage return
                    case 'r':
                        currentChar = '\r';
                        break;

                                //  Tab.
                    case 't':
                        currentChar = '\t';
                        break;

                                //  Unicode value of form udddd where
                                //  "dddd" is a four-digit hexadecimal
                                //  value.

                    case 'u':
                                //  Skip past initial "u" character.

                        index++;

                                //  Accumulates Unicode character value.

                        int unicodeValue = 0;

                                //  Pick up each of the four characters
                                //  of the unicode value.

                        for ( int i = 0 ; i < 4 ; i++ )
                        {
                                //  Get next unicode character,
                                //  converting letters to lower case.

                            currentChar =
                                Character.toLowerCase
                                (
                                    line.charAt( index++ )
                                );

                                //  Handle digit ('0' through '9')

                            if  (   ( currentChar >= '0' ) &&
                                    ( currentChar <= '9' ) )
                            {
                                unicodeValue    =
                                    ( unicodeValue << 4 ) +
                                    currentChar - '0';
                            }

                                //  Handle letters 'a' through 'f'.

                            else if (   ( currentChar >= 'a' ) &&
                                        ( currentChar <= 'f' ) )
                            {
                                unicodeValue    =
                                    ( unicodeValue << 4 ) + 10 +
                                        currentChar - 'a';
                            }

                                //  Anything other than a letter or
                                //  a digit is invalid in the
                                //  unicode value.

                            else
                            {
                                throw new IllegalArgumentException
                                (
                                    "Invalid \\uxxxx encoding."
                                );
                            }
                        }
                                //  Set current character to accumulated
                                //  Unicode value.

                        currentChar = (char)unicodeValue;

                                //  Back up one character in input string
                                //  to continue parsing.

                        index--;
                        break;

                                //  Accept any other character as-is.
                    default :
                        break;
                }
            }
                                //  Append character to output string.

            sb.append( currentChar );
        }
                                //  Return output string.

        return sb.toString();
    }

    /** Format string for output.
     *
     *  @param  line    String to convert to output format.
     *
     *  @return         Output formatted string .
     */

    protected String formatStringForOutput( String line )
    {
        int length              = line.length();

        StringBuffer outBuffer  = new StringBuffer( length * 2 );

        for ( int i = 0 ; i < length ; i++ )
        {
            char currentChar    = line.charAt( i );

            switch ( currentChar )
            {
                case '\\':
                    outBuffer.append( '\\' );
                    outBuffer.append( '\\' );
                    break;

                case '\f':
                    outBuffer.append( '\\' );
                    outBuffer.append( 'f' );
                    break;

                case '\n':
                    outBuffer.append( '\\' );
                    outBuffer.append( 'n' );
                    break;

                case '\r':
                    outBuffer.append( '\\' );
                    outBuffer.append( 'r' );
                    break;

                case '\t':
                    outBuffer.append( '\\' );
                    outBuffer.append( 't' );
                    break;

                default:
                    outBuffer.append( currentChar );
                    break;
            }
        }

        return outBuffer.toString();
    }

    /** Check if property continues on the next line.
     *
     *  @param  line    Beginning of the property that might be continued
     *                  on the next line.
     *
     *  @return         true if property continues on the following line.
     *
     *  <p>
     *  A trailing "\" character indicates a property value is continued
     *  on the next line.
     *  </p>
     */

    protected boolean isContinued( String line )
    {
        if ( ( line != null ) && !line.equals( "" ) )
        {
            return line.charAt( line.length() - 1 ) == '\\';
        }

        return false;
    }

    /** Store properties to a file ignoring any I/O errors.
     *
     *  @param  outputStream    Stream to which to save properties.
     *  @param  header          Optionl description of the property list.
     *
     *  @throws ClassCastException
     *                          If any key or value is not a String.
     *
     *  @deprecated             Use {@link #store( OutputStream , String ) }
     *                          instead.
     */

    public void save( OutputStream outputStream , String header )
    {
        try
        {
            store( outputStream , header );
        }
        catch ( IOException e )
        {
        }
    }

    /** Store properties to a file.
     *
     *  @param  outputStream    Output stream.
     *  @param  header          Optional description of property list.
     *  @param  outputOnlyKeys  True to output only the keys,
     *                          false to output both the keys
     *                          and the values.
     *
     *  @throws IOException     When an error occurs while writing
     *                          to the output stream.
     */

    public void store
    (
        OutputStream outputStream ,
        String header ,
        boolean outputOnlyKeys
    )
        throws IOException
    {
        BufferedWriter output;

        output  =
            new BufferedWriter
            (
                new OutputStreamWriter( outputStream , "utf-8" )
            );
                                //  Disallow modification by other
                                //  threads while we are writing the
                                //  properties to the output stream.
        synchronized( this )
        {
                                //  Write header if provided.

            if ( header != null )
            {
                output.write( "#" + header );
                output.newLine();
            }
                                //  Output date/time stamp.

            output.write( "#" + Calendar.getInstance().getTime() );
            output.newLine();

                                //  Enumerate all properties and
                                //  write each (key,value)pair
                                //  to the output stream.

            Enumeration enumeration = keys();

            while ( enumeration.hasMoreElements() )
            {
                                //  Next key.

                String key      =
                    formatStringForOutput(
                        (String)enumeration.nextElement() );

                                //  Write key to output stream.

                output.write( key );

                                //  Output value if unless we're
                                //  only writing keys.

                if ( !outputOnlyKeys )
                {
                                //  Value for key.

                    String value    =
                        formatStringForOutput( (String)get( key ) );

                    output.write( "=" + value );
                }

                output.newLine();
            }
        }
                                //  Make sure all output is flushed
                                //  from the output buffer.
        output.flush();
    }

    /** Store properties to a file.
     *
     *  @param  outputStream    Output stream.
     *  @param  header          Optional description of the property list.
     *
     *  @throws IOException     When an error occurs while writing
     *                          to the output stream.
     */

    public void store( OutputStream outputStream , String header )
        throws IOException
    {
        store( outputStream , header , false );
    }

    /** See if list contains specified string.
     *
     *  @param  string  The string.
     *
     *  @return         True if list contains specified string.
     */

    public boolean containsString( String string )
    {
        return ( getProperty( string ) != null );
    }

    /** Get the tag value associated with a string.
     *
     *  @param  string  The string.
     *
     *  @return         The tag value associated with the string.
     *                  May be null.
     */

    public String getTag( String string )
    {
        return getProperty( string );
    }

    /** Set the tag value associated with a string.
     *
     *  @param  string  The string.
     *  @param  tag     The tag.
     */

    public void putTag( String string , String tag )
    {
        setProperty( string , tag );
    }

    /** Get number of strings.
     *
     *  @return     Number of strings.
     */

    public int getStringCount()
    {
        return size();
    }

    /** Get set of all unique strings.
     *
     *  @return     Set of all unique strings.
     */

    public Set<String> getAllStrings()
    {
        Set<String> result  = SetFactory.createNewSet();

        for (   Enumeration enumeration = propertyNames() ;
                enumeration.hasMoreElements() ;
            )
        {
            result.add( enumeration.nextElement().toString() );
        }

        return result;
    }

    /** Get set of all unique tags.
     *
     *  @return     Set of all unique tags
     */

    public Set<String> getAllTags()
    {
        Set<String> result  = SetFactory.createNewSet();

        for (   Enumeration enumeration = propertyNames() ;
                enumeration.hasMoreElements() ;
            )
        {
            String key  = (String)enumeration.nextElement();
            result.add( getProperty( key ) );
        }

        return result;
    }
}

/*
Copyright (c) 2008, 2013 by Northwestern University.
All rights reserved.

Developed by:
   Academic and Research Technologies
   Northwestern University
   http://www.it.northwestern.edu/about/departments/at/

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal with the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimers.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimers in the documentation and/or other materials provided
      with the distribution.

    * Neither the names of Academic and Research Technologies,
      Northwestern University, nor the names of its contributors may be
      used to endorse or promote products derived from this Software
      without specific prior written permission.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
*/



