/*
 * @(#)EscapedCodec.java    1.0.1  2 July 2001
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.codec;

import java.io.Serializable;
import java.text.ParseException;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * <P><CODE>{@link Codec CODEC}</CODE> implementation 
 * whose encoding scheme uses separator sequences
 * to separate distinct data elements
 * and uses escape sequences to mask separator sequences
 * and escape sequences at each level of recursion.
 *
 * This CODEC does not make use of data compression,
 * and does not result in any data loss.
 *
 * This CODEC produces encoded data 
 * that is extremely readable at low levels of recursion,
 * but is very difficult to read at high levels
 * of recursion.</P>
 *
 * <P>The unique three letter identifier 
 * for this CODEC is "<CODE>ESC</CODE>".
 *
 * This CODEC is automatically installed by the JPT.</P>
 *
 * @author  Jeff Raab
 * @version 2.2
 * @since   1.0
 * @see CodecUtilities
 */
public final class EscapedCodec 
    implements Codec, Serializable 
{
    
    /** 
     * The sequence of characters that separates elements
     * of an encoded <CODE>String</CODE>.
     */
    private static final String SEPARATOR = ",";
    
    /** 
     * The sequence of characters that escapes 
     * the following character or sequence 
     * from being considered an escape or separator sequence. 
     */
    private static final String ESCAPE = "\\";
    
    /** 
     * The sequence of characters that represents <CODE>null</CODE>,
     * or "no data".
     */
    private static final String NULL = "" + (char)0;
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs an object capable of encoding and decoding data 
     * using the "Escaped-separated" encoding scheme.
     */
    public EscapedCodec() {}
    
    ///////////
    // Codec //
    ///////////

    /**
     * Encodes the given array of data <CODE>String</CODE>s
     * into a single compound data <CODE>String</CODE> 
     * using the "Escaped-separated" encoding scheme.
     *
     * If the given array is <CODE>null</CODE>,
     * a representation of <CODE>null</CODE> is returned.
     *
     * @param data an array of data <CODE>String</CODE>s
     * @return the resulting encoded <CODE>String</CODE>
     * @see #decode(String)
     * @see CodecUtilities#encode(String[])
     * @see CodecUtilities#encode(Stringable[])
	 * @see Codec
     */
    public String encode(String[] data) {
    
    	// encode null array as stored sequence
        if (data == null)
            return NULL;
            
        // encode non-null array a 
        // as an escaped separated sequence
        else {
            String s = "";
            for (int i = 0; i < data.length; i++)
                s += escape(data[i]) + SEPARATOR;
            return s;
        }
    }

    /**
     * Decodes the given compound data <CODE>String</CODE> 
     * into an array of data <CODE>String</CODE>s 
     * using the "Escaped-separated" encoding scheme.
     *
     * If the given data is <CODE>null</CODE>
     * or the representation of <CODE>null</CODE>,
     * this method returns <CODE>null</CODE>.
     *
     * @param data an encoded data <CODE>String</CODE>
     * @return the resulting array of data <CODE>String</CODE>s
     * @throws ParseException if the data was not encoded 
     *		using this scheme
     * @see #encode(String[])
     * @see CodecUtilities#decode(String)
	 * @see Codec
     */
    public String[] decode(String data) throws ParseException {
    
        // return null for null data
        if (data == null)
            return null;
            
        // return null for representation of null data
        if (data.equals(NULL))
            return null;

        // create a vector to hold the individual elements, 
        // a flag to note escape sequences, 
        // and a buffer to hold the current element
        Vector       elements = new Vector();
        boolean      esc   = false;
        StringBuffer current  = new StringBuffer();
        
        // for each position in the input
        for (int i = 0; i < data.length();) {
            
            // if escape sequence starts at this position
            if (data.indexOf(ESCAPE, i) == i) {
            
                // if the last token was an escape sequence
                if (esc) {
                    
                    // note that this token does not escape the next
                    esc = false;
                    
                    // add the escape character to the current element
                    current.append(ESCAPE);
                }
                
                // otherwise,
                else {

                    // note the need to escape the next token
                    esc = true;
                }
                
                // always skip ahead the appropriate number of positions
                i += ESCAPE.length();
            }                    

            // if separator sequence starts at this position
            else if (data.indexOf(SEPARATOR, i) == i) {
            
                // if the last token was an escape sequence
                if (esc) {
                    
                    // add the separator sequence to the current element
                    current.append(SEPARATOR);
                }
                
                // otherwise,
                else {
                
                    // check for representation of null
                    if (current.toString().equals(NULL))
                        elements.add(null);

                    // otherwise add the current element to the collection
                    else
                        elements.add(current.toString());
                        
                    // empty out the current element buffer
                    current.delete(0, current.length());
                }
                    
                // note that this token does not escape the next
                esc = false;
                    
                // skip ahead the appropriate number of positions
                i += SEPARATOR.length();
            }

            // if other sequence starts at this position
            else {
                
                // check if escape sequence was skipped inappropriately
                if (esc) {
                    
                    // and add it back
                    current.append(ESCAPE);
                }
                
                // add the character at this position
                current.append(data.charAt(i++));
                
                // note that this token does not escape the next
                esc = false;
            }
        }
        
        // return an array of the appropriate type and length
        return (String[])elements.toArray(new String[0]);
    }
    
    /**
     * Returns the unique identifier for this encoding scheme:
     * the <CODE>String</CODE> "<CODE>ESC</CODE>".
     *
     * @see CodecUtilities#installCodec(Codec)
     * @see CodecUtilities#getDefaultCodec()
     * @see Codec
     */
    public String getPrefix() {
        return "ESC";
    }
    
    /////////////////////
    // Private methods //
    /////////////////////
    
    /**
     * Prepares an individual element 
     * from the array of <CODE>String</CODE> data 
     * for encoding by preceding each separator 
     * and escape sequence with an escape sequence.
     *
     * If the given data is <CODE>null</CODE>,
     * the representation for null data is returned.
     *
     * @param data the data <CODE>String</CODE> to escape
     * @return the escaped data <CODE>String</CODE>
     * @see #encode(String[])
     */
    private String escape(String data) {
    
        // return null representation for null data
        if (data == null)
            return NULL;

        // create a buffer to hold the output
        StringBuffer buffer = new StringBuffer(data.length());
        
        // for each position in the input,
        for (int i = 0; i < data.length();) {

            // if separator sequence starts at this position
            if (data.indexOf(SEPARATOR, i) == i) {
                
                // add an escape character to the output
                buffer.append(ESCAPE);
                
                // add the found separator to the output
                buffer.append(SEPARATOR);

                // skip ahead the appropriate number of positions
                i += SEPARATOR.length();
            }
                    
            // if escape sequence starts at this position
            else if (data.indexOf(ESCAPE, i) == i) {
                
                // add an escape character to the output
                buffer.append(ESCAPE);
                
                // add the found escape character to the output
                buffer.append(ESCAPE);

                // skip ahead the appropriate number of positions
                i += ESCAPE.length();
            }
            
            // otherwise add character at this position to the output
            else buffer.append(data.charAt(i++));
        }
        
        // return the output as a String
        return buffer.toString();
    }
}
