/*
 * @(#)Telnet.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.telnet;

import com.jscape.inet.util.TimedSocket;

import java.io.*;
import java.net.Socket;
import java.util.Vector;

/**
 * Implements the basic functionality of a Telnet client as defined in RFC 854.  Upon establishing
 * a connection the Telnet server may attempt to perform option negotiation (see RFC 855). Any option
 * negotiation options exchanged must be handled prior to receiving a login or shell prompt.
 * <p/>
 * To handle option negotiation you must add a TelnetListener to the Telnet instance and overload the
 * doOption, dontOption, willOption and wontOption methods.
 * <p/>
 * Once option negotiation has completed sucessfully a separate thread is spawned that reads data sent by the
 * Telnet server. You can capture this data by overloading the dataReceived method in the TelnetListener
 * interface or TelnetAdapter class.
 * <p/>
 * Example:<P>
 * <pre>
 * public class TelnetExample extends TelnetAdapter {
 *
 *      private Telnet telnet = null;
 *      private OutputStream output = null;
 *      private static BufferedReader reader = null;
 *      private boolean connected = false;
 *      private String hostname = "10.0.0.1";
 *
 *      public TelnetExample() throws IOException, TelnetException {
 *
 *              // create new Telnet instance
 *              telnet = new Telnet(hostname);
 *
 *              // register this class as TelnetListener
 *              telnet.addTelnetListener(this);
 *
 *              // establish Telnet connection
 *              telnet.connect();
 *              connected = true;
 *
 *              // get output stream
 *              output = telnet.getOutputStream();
 *
 *              // sends all data entered at console to Telnet server
 *              String input = null;
 *              while ((input = reader.readLine()) != null) {
 *                      if (connected) {
 *                              ((TelnetOutputStream) output).println(input);
 *             } else {
 *                              break;
 *             }
 *         }
 *     }
 *
 *      // Invoked when Telnet socked is connected.
 *      public void connected(TelnetConnectedEvent event) {
 *              System.out.println("Connected");
 *     }
 *
 *      // Invoked when Telnet socket is disconnected. Disconnect can
 *      public void disconnected(TelnetDisconnectedEvent event) {
 *              connected = false;
 *              System.out.print("Disconnected.  Press enter key to quit.");
 *     }
 *
 *      // Invoked when Telnet server requests that the Telnet client begin performing specified <code>TelnetOption</code>.
 *      public void doOption(DoOptionEvent event) {
 *              // refuse any options requested by Telnet server
 *              telnet.sendWontOption(event.getOption());
 *     }
 *
 *      // Invoked when Telnet server offers to begin performing specified <code>TelnetOption</code>.
 *      public void willOption(WillOptionEvent event) {
 *              // refuse any options offered by Telnet server
 *              telnet.sendDontOption(event.getOption());
 *     }
 *
 *      // Invoked when data is received from Telnet server.
 *      public void dataReceived(TelnetDataReceivedEvent event) {
 *              // print data recevied from Telnet server to console
 *              System.out.print(event.getData());
 *     }
 *
 *      // starts console program
 *      public static void main(String[] args) {
 *              try {
 *                      // create BufferedReader to read data from console
 *                      reader = new BufferedReader(new InputStreamReader(System.in));
 *
 *                      // create new TelnetExample instance
 *                      TelnetExample example = new TelnetExample();
 *
 *         } catch (Exception e) {
 *                      e.printStackTrace(System.out);
 *         }
 *     }
 *
 * }
 *
 * </pre>
 */
public class Telnet
        implements Serializable {

    // Telnet commands and special charaters

    /**
     * Telnet special character : No operation
     */
    public static final int TSC_NULL = 0;

    /**
     * Telnet special character : BELL.<br>
     * Produces an audible or visible signal (which does NOT move the print
     * head).
     */
    public static final int TSC_BEL = 7;

    /**
     * Telnet special character : Back Space.<br>
     * Moves the print head one character position towards the left margin.
     */
    public static final int TSC_BS = 8;

    /**
     * Telnet special character : Horizontal Tab.<br>
     * Moves the printer to the next horizontal tab stop. It remains
     * unspecified how either party determines or establishes where such tab
     * stops are located.
     */
    public static final int TSC_HT = 9;

    /**
     * Telnet special character : Line Feed.<br>
     * Moves the printer to the next print line, keeping the same horizontal
     * position.
     */
    public static final int TSC_LF = 10;

    /**
     * Telnet special character : Vertical Tab.<br>
     * Moves the printer to the next vertical tab stop.  It remains
     * unspecified how either party determines or establishes where such tab
     * stops are located.
     */
    public static final int TSC_VT = 11;

    /**
     * Telnet special character : Form Feed.<br>
     * Moves the printer to the top of the next page, keeping the same
     * horizontal position.
     */
    public static final int TSC_FF = 12;

    /**
     * Telnet special character : Carriage Return.<br>
     * Moves the printer to the left margin of the current line.
     */
    public static final int TSC_CR = 13;

    /**
     * Telnet command : End of subnegotiation parameters.
     */
    public static final int TC_SE = 240;

    /**
     * Telnet command : No operation.
     */
    public static final int TC_NOP = 241;

    /**
     * Telnet command : Data Mark.<br>
     * The data stream portion of a Synch. This should always be accompanied
     * by a TCP Urgent notification.
     */
    public static final int TC_DM = 242;

    /**
     * Telnet command : Break.<br> NVT character BRK.
     */
    public static final int TC_BRK = 243;

    /**
     * Telnet command : Interrupt Process.
     */
    public static final int TC_IP = 244;

    /**
     * Telnet command : Abort output
     */
    public static final int TC_AO = 245;

    /**
     * Telnet command : Are You There
     */
    public static final int TC_AYT = 246;

    /**
     * Telnet special character : Erase character
     */
    public static final int TSC_EC = 247;

    /**
     * Telnet special character : Erase line
     */
    public static final int TSC_EL = 248;

    /**
     * Telnet command : Go ahead
     */
    public static final int TC_GA = 249;

    /**
     * Telnet command : Subnegotiation.<br>
     * Indicates that what follows is subnegotiation of the indicated option.
     */
    public static final int TC_SB = 250;

    /**
     * Telnet command : WILL.<br>
     * Indicates the desire to begin performing, or confirmation that you are
     * now performing, the indicated option.
     */
    public static final int TC_WILL = 251;

    /**
     * Telnet command : WON'T.<br>
     * Indicates the refusal to perform, or continue performing, the indicated
     * option.
     */
    public static final int TC_WONT = 252;

    /**
     * Telnet command : DO.<br>
     * Indicates the request that the other party perform, or confirmation
     * that you are expecting the other party to perform, the indicated
     * option.
     */
    public static final int TC_DO = 253;

    /**
     * Telnet command : DON'T.<br>
     * Indicates the demand that the other party stop performing, or
     * confirmation that you are no longer expecting the other party to
     * perform, the indicated option.
     */
    public static final int TC_DONT = 254;

    /**
     * Telnet escape charater : Interpret as Command
     */
    public static final int TSC_IAC = 255;

    /**
     * This field can be used to map command codes to their String
     * representation.
     */
    public static final String[] COMMAND_MAP = new String[256];

    static {
        COMMAND_MAP[TC_NOP] = new String("NOP");
        COMMAND_MAP[TC_DM] = new String("DM");
        COMMAND_MAP[TC_GA] = new String("GA");
        COMMAND_MAP[TC_AYT] = new String("AYT");
        COMMAND_MAP[TC_SE] = new String("SE");
        COMMAND_MAP[TC_SB] = new String("SB");
        COMMAND_MAP[TC_WILL] = new String("WILL");
        COMMAND_MAP[TC_WONT] = new String("WONT");
        COMMAND_MAP[TC_DO] = new String("DO");
        COMMAND_MAP[TC_DONT] = new String("DONT");
    }

    protected transient Thread reader = null;
    private transient PrintStream debugStream = System.out;
    protected int timeout = 30000;
    protected int readingTimeout = 30000;
    private String characterSet;

    /**
     * Controls whether debugging output is printed to debug stream.
     *
     * @see #getDebug
     * @see #setDebug
     * @see #setDebugStream
     */
    protected boolean debug = false;

    /**
     * The TCP socket.
     */
    protected transient Socket socket = null;

    /**
     * Filtered input stream for telnet.
     */
    protected transient TelnetInputStream input = null;

    /**
     * Filtered output stream for telnet.
     */
    protected transient TelnetOutputStream output = null;

    /**
     * Remote hostname or IP address.
     */
    protected String hostname;

    /**
     * Remote port.
     */
    protected int port = 23;

    /**
     * Event listeners.
     */
    private transient Vector listeners = new Vector();

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";
    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyUsername = null;
    private String proxyPassword = null;
    private String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Constructs Telnet object for connection to specified host.
     *
     * @param hostname the hostname or IP address of the Telnet server
     */
    public Telnet(String hostname) {
        
        this.hostname = hostname;
    }

    /**
     * Constructs Telnet object.
     *
     * @param host the hostname or IP address of the Telnet server
     * @param port the port of the Telnet server
     */
    public Telnet(String host, int port) {
        this(host);
        this.port = port;
    }

    /**
     * Connect the client socket to host and port specified by contructor or
     * {@link #setHostname}, {@link #setPort} methods.
     */
    public void connect()
            throws TelnetException {
        synchronized (this) {
            try {
                if (this.proxyHostname == null) {
                    socket = TimedSocket.getSocket(hostname, port, timeout);
                } else {
                    socket = TimedSocket.getSocket(hostname, port, timeout,
                                                   this.proxyHostname, this.proxyPort, this.proxyUsername,
                                                   this.proxyPassword, this.proxyType);
                }
                socket.setTcpNoDelay(true);
                socket.setSoTimeout(this.readingTimeout);

                input = new TelnetInputStream(this, socket.getInputStream());
                output = new TelnetOutputStream(socket.getOutputStream(), characterSet);

                // start reader in new thread
                reader = new Thread(new TelnetReader(input));
                reader.start();

                // fire connection event and negotiate options
                fireTelnetConnected(hostname, port);
            } catch (Exception e) {
                throw new TelnetException(e.getMessage(), e);
            }
        }
    }

    /**
     * Disconnects and releases all associated resources.
     */
    public void disconnect() {
        // wait for any extra incoming data to be receive
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            // todo: ignore?
        }

        try {
            input.close();
        } catch (Exception e) {
            // todo: ignore?
        }

        try {
            output.close();
        } catch (Exception e) {
            // todo: ignore?
        }

        try {
            socket.close();
        } catch (Exception e) {
            // todo: ignore?
        }

        fireTelnetDisconnected(hostname, port);
    }

    /**
     * Set server hostname.
     *
     * @param hostname the hostname or IP address of the Telnet server
     */
    public void setHostname(String hostname) {
        synchronized (this) {
            this.hostname = hostname;
        }
    }

    /**
     * Return server hostname.
     *
     * @return the hostname or IP address of the Telnet server
     */
    public String getHostname() {
        return hostname;
    }

    /**
     * Set server port.
     *
     * @param port the port of the Telnet server
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Return server port.
     *
     * @return the port of the Telnet server
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }

    /**
     * Add telnet event listener.
     *
     * @param listener a TelnetListener
     *
     * @see TelnetListener
     */
    public void addTelnetListener(TelnetListener listener) {
        synchronized (listeners) {
            listeners.addElement(listener);
        }
    }

    /**
     * Remove telnet event listener.
     *
     * @param listener a TelnetListener
     *
     * @see TelnetListener
     */
    public void removeTelnetListener(TelnetListener listener) {
        synchronized (listeners) {
            if (listeners.contains(listener)) {
                listeners.removeElement(listener);
            }
        }
    }

    /**
     * Sends telnet command.
     *
     * @param command the command to send
     */
    public void sendCommand(int command) {
        synchronized (this) {
            try {
                byte[] buf = new byte[2];
                buf[0] = (byte) TSC_IAC;
                buf[1] = (byte) command;
                output.twrite(buf);
            } catch (Exception e) {
                disconnect();
            }
        }
    }

    /**
     * Sends telnet command with specified parameter.
     *
     * @param command   the command to send
     * @param parameter the parameter to send
     */
    public void sendCommand(int command, int parameter) {
        synchronized (this) {
            try {
                byte[] buf = new byte[3];
                buf[0] = (byte) TSC_IAC;
                buf[1] = (byte) command;
                buf[2] = (byte) parameter;
                output.twrite(buf);
            } catch (Exception e) {
                disconnect();
            }
        }
    }

    /**
     * Returns input stream associated with current Telnet connection. Telnet
     * commands, special and control characters are filtered out of this
     * stream.
     *
     * @return an InputStream
     */
    public InputStream getInputStream() {
        return input;
    }

    /**
     * Returns output stream associated with current telnet connection.
     * Commands, special and control characters should be written only using
     * Telnet class methods, rather than directly writing to this stream.
     *
     * @return an OutputStream
     */
    public OutputStream getOutputStream() {
        return output;
    }

    /**
     * Returns Reader object for reading from the server.
     *
     * @return a Reader
     */
    public Reader getReader() {
        return new InputStreamReader(input);
    }

    /**
     * Returns Writer object for writing to server.
     *
     * @return a Writer
     */
    public Writer getWriter() {
        return new OutputStreamWriter(output);
    }

    /**
     * Handles all command requests from server.
     *
     * @param code the option code to read
     *
     * @throws IOException if an I/O related error occurs
     */
    protected void commandRead(int code)
            throws IOException {
        synchronized (this) {
            int optionCode = input.tread();
            TelnetOption opt = new TelnetOption(optionCode);

            if (code == TC_WILL) {
                fireWillOption(opt);
            } else if (code == TC_WONT) {
                fireWontOption(opt);
            } else if (code == TC_DO) {
                fireDoOption(opt);
            } else if (code == TC_DONT) {
                fireDontOption(opt);
            } else if (code == TC_SB) {
                // storage for subnegotation parameters
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                int a = input.tread();

                while (a != -1) {
                    if (a == Telnet.TSC_IAC) {
                        int b = input.read();

                        if (b == Telnet.TSC_IAC) {
                            // descape IAC for option parser
                            bout.write(Telnet.TSC_IAC);
                        } else if (b == Telnet.TC_SE) {
                            // subnegotiation end
                            break;
                        } else {
                            // funny, server has embedded a command in subnegotiation
                            // synch?
                            commandRead(b);
                        }
                    } else {
                        bout.write(a);
                    }
                    a = input.tread();
                }
                opt.readSubnegotiation(
                        new ByteArrayInputStream(bout.toByteArray()));
                fireDoSubOption(opt);
            }
        }
    }

    /**
     * Initiate subnegotiation for option.
     *
     * @param opt a TelnetOption
     *
     * @see TelnetOption
     */
    public void sendOptionSubnegotiation(TelnetOption opt) {
        synchronized (this) {
            try {
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                bout.write(Telnet.TSC_IAC);
                bout.write(Telnet.TC_SB);
                bout.write(opt.getOptionCode());
                opt.writeSubnegotiation(bout);
                bout.write(Telnet.TSC_IAC);
                bout.write(Telnet.TC_SE);
                output.twrite(bout.toByteArray());
            } catch (Exception e) {
                disconnect();
            }
        }
    }

    /**
     * Gets name of command based on corresponding command.
     *
     * @param command the command code
     *
     * @return the name of command code or null if not found
     */
    public String getCommandName(int command) {
        return COMMAND_MAP[command];
    }

    /**
     * Sends Telnet option to server with Telnet WILL OPTION (accept offer to use option)
     * command.
     *
     * @param opt the option to send
     */
    public void sendWillOption(TelnetOption opt) {
        sendCommand(Telnet.TC_WILL, opt.getOptionCode());
    }

    /**
     * Sends Telnet option to server with Telnet WONT OPTION (refuse offer to use option)
     * command.
     *
     * @param opt the option to send
     */
    public void sendWontOption(TelnetOption opt) {
        sendCommand(Telnet.TC_WONT, opt.getOptionCode());
    }

    /**
     * Sends Telnet option to server with Telnet DO OPTION (request to use option)
     * command
     *
     * @param opt the option to send
     */
    public void sendDoOption(TelnetOption opt) {
        sendCommand(Telnet.TC_DO, opt.getOptionCode());
    }

    /**
     * Sends Telnet option to server with Telnet DONT OPTION (refuse request to use option)
     * command
     *
     * @param opt the option to send
     */
    public void sendDontOption(TelnetOption opt) {
        sendCommand(Telnet.TC_DONT, opt.getOptionCode());
    }

    /**
     * Fires <code>TelnetConnectedEvent</code>. This event is fired upon
     * establishing a connection to Telnet server.
     *
     * @param hostname the hostname of Telnet server
     * @param port     the port of Telnet server
     *
     * @see TelnetConnectedEvent
     */
    protected void fireTelnetConnected(String hostname, int port) {
        Vector thisList = new Vector();
        TelnetConnectedEvent thisEvent =
                new TelnetConnectedEvent(this, hostname, port);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetConnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).connected(thisEvent);
        }
    }

    /**
     * Fires <code>TelnetDisconnectedEvent</code>. This event is fired upon
     * disconnecting from Telnet server.
     *
     * @param hostname the hostname of Telnet server
     * @param port     the port of Telnet server
     *
     * @see TelnetDisconnectedEvent
     */
    protected void fireTelnetDisconnected(String hostname, int port) {
        Vector thisList = new Vector();
        TelnetDisconnectedEvent thisEvent =
                new TelnetDisconnectedEvent(this, hostname, port);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDisconnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).disconnected(thisEvent);
        }
    }

    /**
     * Fires <code>TelnetDataReceivedEvent</code>. This event is fired upon
     * receiving data from Telnet server.
     *
     * @param data the data received
     *
     * @see TelnetDataReceivedEvent
     */
    protected void fireDataReceived(String data) {
        Vector thisList = new Vector();
        TelnetDataReceivedEvent thisEvent = TelnetDataReceivedFactory.getDataReceivedEvent(this, data);
        debug(data);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDataReceivedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).dataReceived(thisEvent);
        }
    }

    /**
     * Fires <code>WillOptionEvent</code>.
     * This event is fired when Telnet server sends WILL OPTION command.
     *
     * @param option the <code>TelnetOption</code>
     *
     * @see TelnetOption
     * @see WillOptionEvent
     */
    protected void fireWillOption(TelnetOption option) {
        Vector thisList = new Vector();
        WillOptionEvent thisEvent = new WillOptionEvent(this, option);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDisconnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).willOption(thisEvent);
        }
    }

    /**
     * Fires <code>WontOptionEvent</code>.
     * This event is fired when Telnet server sends WONT OPTION command.
     *
     * @param option a <code>TelnetOption</code>
     *
     * @see TelnetOption
     * @see WontOptionEvent
     */
    protected void fireWontOption(TelnetOption option) {
        Vector thisList = new Vector();
        WontOptionEvent thisEvent = new WontOptionEvent(this, option);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDisconnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).wontOption(thisEvent);
        }
    }

    /**
     * Fires <code>DoOptionEvent</code>.
     * This event is fired when Telnet server sends DO OPTION (request client to use option)
     * command.
     *
     * @param option a <code>TelnetOption</code>
     *
     * @see DoOptionEvent
     */
    protected void fireDoOption(TelnetOption option) {
        Vector thisList = new Vector();
        DoOptionEvent thisEvent = new DoOptionEvent(this, option);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDisconnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).doOption(thisEvent);
        }
    }

    /**
     * Fires <code>DontOptionEvent</code>.
     * This event is fired when Telnet server sends DONT OPTION (refuse clients request to use option)
     * command.
     *
     * @param option a <code>TelnetOption</code>
     *
     * @see DontOptionEvent
     */
    protected void fireDontOption(TelnetOption option) {
        Vector thisList = new Vector();
        DontOptionEvent thisEvent = new DontOptionEvent(this, option);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a TelnetDisconnectedEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).dontOption(thisEvent);
        }
    }

    /**
     * Fires <code>DoSubOptionEvent</code>.
     * This event is fired when Telnet server sends SB OPTION (subnegotiation) command.
     *
     * @param option a <code>TelnetOption</code>
     *
     * @see DoSubOptionEvent
     */
    protected void fireDoSubOption(TelnetOption option) {
        Vector thisList = new Vector();
        DoSubOptionEvent thisEvent = new DoSubOptionEvent(this, option);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }
        // Send a DoSubnegotiationEvent to every listener.

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((TelnetListener) thisList.elementAt(elem)).doSubOption(thisEvent);
        }
    }

    /**
     * Enable debugging output to debugging stream.
     *
     * @param debug true if debugging enabled, false otherwise
     *
     * @see #setDebugStream
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Gets debugging state.
     *
     * @return <code>true</code> if debugging is enabled
     */
    public boolean getDebug() {
        return debug;
    }

    /**
     * Sends debug message to debug stream.
     *
     * @param message the message to debug
     */
    protected void debug(String message) {
        if (debug) {
            debugStream.print(message);
        }
    }

    /**
     * Gets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @return the PrintStream used in reporting debug statements
     *
     * @see java.io.PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }

    /**
     * Sets the timeout for opening connection to Telnet server.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * Gets the timeout for opening connection to Telnet server.
     *
     * @return timeout in milliseconds
     */
    public int getTimeout() {
        return timeout;
    }
    
    /**
     * Returns the socket reading timeout value.
     *
     * @return reading timeout in millis
     */
    public int getReadingTimeout() {
        return readingTimeout;
    }
    
    /**
     * Sets the socket reading timeout value.
     *
     * @param value timeout value is millis
     */
    public void setReadingTimeout(int value) {
        this.readingTimeout = value;
    }
    
    /**
     * Sets character set used when reading data from telnet server
     *
     * @param characterSet the character set
     */
    public void setCharacterSet(String characterSet) {
        this.characterSet = characterSet;
    }

    /**
     * Gets the character set used when reading data from Telnet server.
     * Default is null, using the default system character set.
     *
     * @return the character set or null
     */
    public String getCharacterSet() {
        return characterSet;
    }

    /**
     * Sets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @param debugStream the PrintStream to send debug statements to
     *
     * @see java.io.PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        this.debugStream = debugStream;
    }

    /**
     * Byte Decoder
     *
     * @author mjawwad
     */
    public abstract class ByteDecoder {
        public abstract String getDecodedString(byte[] buffer, InputStream in);

        public byte[] getBytes(int size, InputStream in)
                throws IOException {
            byte[] bytes = new byte[size + 1];
            for (int i = 0; i < size; i++) {
                bytes[i + 1] = (byte) in.read();
            }
            return bytes;
        }
    }

    /**
     * Default Decoder
     *
     * @author mjawwad
     */
    public class DefaultDecoder
            extends ByteDecoder {
        public String getDecodedString(byte[] buffer, InputStream in) {
            try {
                String charset = Telnet.this.getCharacterSet();
                if (charset == null) {
                    return new String(buffer);
                } else {
                    return new String(buffer, charset);
                }
            } catch (Exception e) {
                return new String(buffer);
            }
        }
    }

    /**
     * UTF8 Decoder
     *
     * @author mjawwad
     */
    public class UTF8Decoder
            extends ByteDecoder {
        public String getDecodedString(byte[] buffer, InputStream in) {
            byte[] bytes = null;
            String ret = null;
            try {
                if ((buffer[0] & 0xF0) == 0xF0) {
                    bytes = getBytes(3, in);
                } else if ((buffer[0] & 0xE0) == 0xE0) {
                    bytes = getBytes(2, in);
                } else if ((buffer[0] & 0xC0) == 0xC0) {
                    bytes = getBytes(1, in);
                }

                if (bytes != null) {
                    bytes[0] = buffer[0];
                    ret = new String(bytes, "UTF-8");
                } else {
                    ret = new String(buffer, "UTF-8");
                }
            } catch (Exception e) {
                ret = new String(buffer);
            }
            return ret;
        }
    }

    /**
     * EUC Korean Decoder
     *
     * @author mjawwad
     */
    public class EUCKoreanDecoder
            extends ByteDecoder {
        public String getDecodedString(byte[] buffer, InputStream in) {
            byte[] bytes = null;
            String ret = null;
            try {
                int val = (int) buffer[0] & 0xFF;
                if (val >= 0xA1 && val <= 0xFE) {
                    bytes = getBytes(1, in);
                }

                if (bytes != null) {
                    bytes[0] = buffer[0];
                    ret = new String(bytes, "EUC-KR");
                } else {
                    ret = new String(buffer, "EUC-KR");
                }
            } catch (Exception e) {
                ret = new String(buffer);
            }
            return ret;
        }
    }

    /**
     * Reads data from Telnet server in separate thread
     */
    protected class TelnetReader
            implements Runnable {
        InputStream in = null;
        private ByteDecoder decoder = null;

        /**
         * Creates a new TelnetReader instance.
         *
         * @param in the InputStream to read from
         */
        public TelnetReader(InputStream in) {
            this.in = in;
            String charset = Telnet.this.getCharacterSet();
            if (charset != null) {
                charset = charset.trim().toLowerCase();
                if (charset.equals("utf-8") || charset.equals("utf8")) {
                    this.decoder = new UTF8Decoder();
                } else if (charset.equals("euc-kr")) {
                    this.decoder = new EUCKoreanDecoder();
                } else {
                    this.decoder = new DefaultDecoder();
                }
            } else {
                this.decoder = new DefaultDecoder();
            }
        }

        /**
         * Read data
         */
        public void run() {
            int b = 0;
            byte[] buffer = new byte[1];

            try {
                while ((b = in.read(buffer)) != -1) {
                    if (b != 0) {
                        fireDataReceived(decoder.getDecodedString(buffer, in));
                    } else {
                        break;
                    }
                } // end while

            } catch (IOException ioe) {
                // ignore
            } finally {
                disconnect();
            }
        }
    }


}
