/*
 * @(#)TelnetSession.java
 *
 * Copyright (c) 2003 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 java.io.IOException;
import java.io.PrintStream;

/**
 * Convenience class for automating communications with a Telnet server.
 * <p/>
 * Example:<P>
 * <pre>
 * String hostname = "10.0.0.2";
 * String loginPrompt = "login:";
 * String passwordPrompt = "Password:"
 * String shellPrompt = "$";
 *
 * // create new TelnetSession instance providing hostname as argument
 * TelnetSession session = new TelnetSession(hostname);
 *
 * // set the expected login prompt
 * session.setLoginPrompt(loginPrompt);
 *
 * // set the expected password prompt
 * session.setPasswordPrompt(passwordPrompt);
 *
 * // set the expected shell prompt
 * session.setShellPrompt(shellPrompt); *
 *
 * // connect and login using supplied username and password
 * session.connect(username,password);
 *
 * // send command to telnet server and wait for shell prompt
 * session.send("cd /user/logs");
 *
 * // send command to telnet server and wait for shell prompt
 * session.send("rm *.log");
 *
 * // send command to telnet server and DO NOT wait for shell prompt
 * session.sendNoWait("exit");
 *
 * // close connection with telnet server
 * session.disconnect();
 * </pre>
 */
public class TelnetSession {

    private String loginPrompt = "login:";
    private String passwordPrompt = "Password:";
    private String shellPrompt = "$";
    private String commandTerminator = "\n";
    private Telnet telnet;
    private TelnetScript script;
    private boolean loginPromptRegex = false;
    private boolean passwordPromptRegex = false;
    private boolean shellPromptRegex = false;
    private int pollingInterval = 100;
    private int regexMatchLength = 20;
    private boolean interrupted = false;

    private String characterSet = null;

    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname the hostname of telnet server
     */
    public TelnetSession(String hostname) {
        this(hostname, 23, null, true);
    }

    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname the hostname of telnet server
     * @param listener automatically register a telnet listener and reject any option negotiation
     */
    public TelnetSession(String hostname, boolean listener) {
        this(hostname, 23, null, listener);
    }

    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname     the hostname of telnet server
     * @param characterSet the character set to use when reading data from telnet server
     */
    public TelnetSession(String hostname, String characterSet) {
        this(hostname, 23, characterSet, true);
    }


    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname     the hostname of telnet server
     * @param characterSet the character set to use when reading data from telnet server
     * @param listener     automatically register a telnet listener and reject any option negotiation
     */
    public TelnetSession(String hostname, String characterSet, boolean listener) {
        this(hostname, 23, characterSet, listener);
    }

    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname the hostname of telnet server
     * @param port     the port of telnet server
     * @param listener automatically register a telnet listener and reject any option negotiation
     */
    public TelnetSession(String hostname, int port, boolean listener) {
        this(hostname, port, null, listener);
    }


    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname the hostname of telnet server
     * @param port     the port of telnet server
     */
    public TelnetSession(String hostname, int port) {
        this(hostname, port, null, true);
    }


    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname     the hostname of the telnet server
     * @param port         the port of the telnet server
     * @param characterSet the character set
     */
    public TelnetSession(String hostname, int port, String characterSet) {
        this(hostname, port, characterSet, true);
    }


    /**
     * Creates a new TelnetSession instance.
     *
     * @param hostname     the hostname of the telnet server
     * @param port         the port of the telnet server
     * @param characterSet the character set
     * @param listener     automatically register a telnet listener and reject any option negotiation
     */
    public TelnetSession(String hostname, int port, String characterSet, boolean listener) {
        telnet = new Telnet(hostname, port);
        if (characterSet != null) {
            this.characterSet = characterSet;
            telnet.setCharacterSet(characterSet);
        }
        if (listener) {
            telnet.addTelnetListener(new TelnetSessionListener(this));
        }
        script = new TelnetScript(telnet);
    }

    /**
     * Establishes connection with telnet server and performs login using specified username and password.
     *
     * @param username the username or null if no login prompt is displayed
     * @param password the password or null if no password prompt is displayed
     * @param timeout  the timeout in milliseconds for opening connection to telnet server.
     *
     * @throws TelnetException if a telnet or I/O related error occurs
     */
    public synchronized void connect(String username, String password, int timeout)
            throws TelnetException {
        if (username != null && password != null) {
            // define login task
            TelnetTask loginTask =
                    new TelnetTask("Login task", loginPrompt, username, passwordPrompt);
            loginTask.setTerminator(commandTerminator);
            loginTask.setStartPromptRegex(loginPromptRegex);
            loginTask.setEndPromptRegex(passwordPromptRegex);
            loginTask.setRegexMatchLength(regexMatchLength);
            if (timeout > 0) {
                loginTask.setStartPromptTimeout(timeout);
                loginTask.setEndPromptTimeout(timeout);
            }

            // define password task
            TelnetTask passwordTask =
                    new TelnetTask("Password task", passwordPrompt, password, shellPrompt);
            passwordTask.setTerminator(commandTerminator);
            passwordTask.setStartPromptRegex(passwordPromptRegex);
            passwordTask.setEndPromptRegex(shellPromptRegex);
            passwordTask.setRegexMatchLength(regexMatchLength);
            if (timeout > 0) {
                passwordTask.setEndPromptTimeout(timeout);
            }

            // enable debugging
            loginTask.setDebug(telnet.getDebug());
            loginTask.setDebugStream(telnet.getDebugStream());
            passwordTask.setDebug(telnet.getDebug());
            passwordTask.setDebugStream(telnet.getDebugStream());

            // add tasks
            script.addTask(loginTask);
            script.addTask(passwordTask);
        } else if (username == null && password != null) {
            // define password task
            TelnetTask passwordTask =
                    new TelnetTask("Password task", passwordPrompt, password, shellPrompt);
            passwordTask.setTerminator(commandTerminator);
            passwordTask.setStartPromptRegex(passwordPromptRegex);
            passwordTask.setEndPromptRegex(shellPromptRegex);
            passwordTask.setRegexMatchLength(regexMatchLength);
            if (timeout > 0) {
                passwordTask.setStartPromptTimeout(timeout);
                passwordTask.setEndPromptTimeout(timeout);
            }

            // enable debugging
            passwordTask.setDebug(telnet.getDebug());
            passwordTask.setDebugStream(telnet.getDebugStream());

            // add task
            script.addTask(passwordTask);
        } else if (username != null && password == null) {
            // define login task
            TelnetTask loginTask =
                    new TelnetTask("Login task", loginPrompt, username, shellPrompt);
            loginTask.setTerminator(commandTerminator);
            loginTask.setStartPromptRegex(loginPromptRegex);
            loginTask.setEndPromptRegex(shellPromptRegex);
            loginTask.setRegexMatchLength(regexMatchLength);
            if (timeout > 0) {
                loginTask.setStartPromptTimeout(timeout);
                loginTask.setEndPromptTimeout(timeout);
            }

            // enable debugging
            loginTask.setDebug(telnet.getDebug());
            loginTask.setDebugStream(telnet.getDebugStream());

            // add task
            script.addTask(loginTask);
        }
        if (timeout > 0) {
            telnet.setTimeout(timeout);
        }
        // get current task
        TelnetTask current = script.head;

        // establish connection
        telnet.connect();

        // check that all tasks completed without error
        if (current != null) {
            do {
                while (current != null && !current.isComplete() && !interrupted) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        // todo: ignore?
                    }
                }

                // check if task timed out
                if (current != null && current.isTimedOut()) {
                    throw new TelnetTaskTimeoutException(current);
                }
                current = current.nextTask;
            } while (current != null && current.nextTask != null && !interrupted);

            // interrupt current task and reset flag
            if (interrupted) {
                if (current != null) {
                    debug("Interrupting current task.");
                    current.cancel();
                    debug("Current task interrupted.");
                }
                interrupted = false;
            }
        }

        // get last task and wait until finished
        TelnetTask last = script.tail;

        if (last != null) {
            while (last != null && !last.isComplete() && !interrupted) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    // todo: ignore?
                }
            }

            // interrupt current task and reset flag
            if (interrupted) {
                if (last != null) {
                    last.cancel();
                }
                interrupted = false;
            }

            // check if last task timed out
            if (last != null && last.isTimedOut()) {
                throw new TelnetTaskTimeoutException(last);
            }
        }
        // clean up resources
        debug("Attempting to cleanup TelnetSession.connect tasks.");
        script.clear();
        debug("Login tasks cleared.");
    }

    /**
     * Establishes connection with telnet server and performs login using specified username and password.
     *
     * @param username the username or null if no login prompt is displayed
     * @param password the password or null if no password prompt is displayed
     *
     * @throws TelnetException if a telnet or I/O related error occurs
     */
    public synchronized void connect(String username, String password)
            throws TelnetException {
        connect(username, password, -1);
    }


    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until the shell prompt is returned by telnet server buffering response data. If shell prompt
     * is not received within defined timeout then a TelnetTaskTimeoutException will be thrown.
     *
     * @param command the command to send
     * @param timeout the timeout to use when waiting for task end prompt
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setShellPrompt
     * @see #setCommandTerminator
     * @see TelnetTaskTimeoutException
     */
    public synchronized String send(String command, long timeout)
            throws TelnetException {
        TelnetTask task = new TelnetTask(null, command, shellPrompt);
        task.setTerminator(commandTerminator);
        task.setEndPromptRegex(shellPromptRegex);
        task.setRegexMatchLength(regexMatchLength);

        // send debugging information
        script.telnet.debug(command);


        if (timeout > 0) {
            task.setEndPromptTimeout(timeout);
        }
        script.addTask(task);
        while (!task.isComplete() && !interrupted) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ie) {
                // todo: ignore?
            }
        }

        // interrupt current task and reset flag
        if (interrupted) {
            task.cancel();
            interrupted = false;
        }

        // check if task completed due to a timeout
        if (task.isTimedOut()) {
            throw new TelnetTaskTimeoutException(task);
        }
        String response = task.getResponse();
        script.clear();
        return response;
    }

    /**
     * Interrupts any running command waiting for an expected prompt.
     * This method does not stop the command from running on the server but instead stops the <code>TelnetSession</code>
     * from waiting for a response.  You should only use this method in the event you wish to interrupt waiting for a response.
     */
    public void interrupt() {
        this.interrupted = true;
        debug("Session has been interrupted via TelnetSession.interrupt");
    }

    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until the shell prompt is returned by telnet server buffering response data.  If shell prompt
     * is not received within default timeout of 120 seconds then a TelnetTaskTimeoutException will be
     * thrown.
     *
     * @param command the command to send
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setShellPrompt
     * @see #setCommandTerminator
     * @see TelnetTaskTimeoutException
     */
    public synchronized String send(String command)
            throws TelnetException {
        return send(command, -1);
    }


    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until specified prompt is returned by telnet server buffering response data. If shell prompt
     * is not received within defined timeout then a TelnetTaskTimeoutException will be thrown.
     *
     * @param command the command to send
     * @param prompt  the prompt to wait for
     * @param timeout the timeout to use when waiting for task end prompt
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setCommandTerminator
     * @see TelnetTaskTimeoutException
     */
    public synchronized String sendWait(String command, String prompt, long timeout)
            throws TelnetException {
        return sendWait(command, prompt, false, timeout);
    }

    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until specified prompt is returned by telnet server buffering response data. If shell prompt
     * is not received within defined timeout then a TelnetTaskTimeoutException will be thrown.
     *
     * @param command the command to send
     * @param prompt  the prompt to wait for
     * @param regex   true if prompt to match is a regular expression, false otherwise
     * @param timeout the timeout to use when waiting for task end prompt
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setCommandTerminator
     * @see #setRegexMatchLength
     * @see TelnetTaskTimeoutException
     */
    public synchronized String sendWait(String command, String prompt, boolean regex, long timeout)
            throws TelnetException {
        TelnetTask task = new TelnetTask(null, command, prompt);
        task.setEndPromptRegex(regex);
        task.setRegexMatchLength(regexMatchLength);
        if (timeout > 0) {
            task.setEndPromptTimeout(timeout);
        }
        task.setTerminator(commandTerminator);
        script.addTask(task);

        while (!task.isComplete() && !interrupted) {
            try {
                Thread.sleep(pollingInterval);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
        }

        // interrupt current task and reset flag
        if (interrupted) {
            task.cancel();
            interrupted = false;
        }

        // check if task completed due to timeout
        if (task.isTimedOut()) {
            throw new TelnetTaskTimeoutException(task);
        }
        String response = task.getResponse();
        script.clear();
        return response;
    }


    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until specified prompt is returned by telnet server buffering response data. If expected prompt
     * is not received within default timeout of 120 seconds then a TelnetTaskTimeoutException will be
     * thrown.
     *
     * @param command the command to send
     * @param prompt  the prompt to wait for
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setCommandTerminator
     * @see TelnetTaskTimeoutException
     */
    public synchronized String sendWait(String command, String prompt)
            throws TelnetException {
        return sendWait(command, prompt, false, -1);
    }

    /**
     * Sends command to telnet host terminated by command terminator.
     * Waits until specified prompt is returned by telnet server buffering response data. If expected prompt
     * is not received within default timeout of 120 seconds then a TelnetTaskTimeoutException will be
     * thrown.
     *
     * @param command the command to send
     * @param prompt  the prompt to wait for
     * @param regex   true if prompt to match is a regular expression, false otherwise
     *
     * @return the response from telnet server
     *
     * @throws TelnetException if regular expression prompts are used and regular expression is invalid
     * @see #setCommandTerminator
     * @see #setRegexMatchLength
     * @see TelnetTaskTimeoutException
     */
    public synchronized String sendWait(String command, String prompt, boolean regex)
            throws TelnetException {
        return sendWait(command, prompt, regex, -1);
    }


    /**
     * Sends command to telnet host terminated by command terminator.
     * Does not wait for shell prompt to be returned.
     *
     * @param command the command to send
     *
     * @throws IOException if an I/O related error occurs
     * @see #setCommandTerminator
     */
    public synchronized void sendNoWait(String command)
            throws IOException {
        TelnetOutputStream out = (TelnetOutputStream) telnet.getOutputStream();
        out.println(command, commandTerminator);
    }

    /**
     * Sends data to telnet host without command terminator.
     * Does not wait for shell prompt to be returned.
     *
     * @param data the data to send
     *
     * @throws IOException if an I/O related error occurs
     * @see #setCommandTerminator
     */
    public synchronized void sendRaw(String data)
            throws IOException {
        TelnetOutputStream out = (TelnetOutputStream) telnet.getOutputStream();
        if (characterSet == null) {
            out.write(data.getBytes());
        } else {
            out.write(data.getBytes(characterSet));
        }
        out.flush();
    }

    /**
     * Disconnects from telnet server.
     *
     * @throws TelnetException if a telnet or I/O related error occurs
     */
    public synchronized void disconnect()
            throws TelnetException {
        telnet.disconnect();
    }

    /**
     * Gets login prompt to wait for before sending username.
     * Default value is "login:"
     *
     * @return the login prompt
     */
    public synchronized String getLoginPrompt() {
        return loginPrompt;
    }

    /**
     * Gets password prompt to wait for before sending password.
     * Default value is "Password:"
     *
     * @return the password prompt
     */
    public synchronized String getPasswordPrompt() {
        return passwordPrompt;
    }

    /**
     * Gets shell prompt to wait for before sending commands.
     * Default value is "$"
     *
     * @return the shell prompt
     */
    public synchronized String getShellPrompt() {
        return shellPrompt;
    }

    /**
     * Sets login prompt to wait for before sending username.
     * Default value is "login:"
     *
     * @param string the login prompt
     */
    public synchronized void setLoginPrompt(String string) {
        setLoginPrompt(string, false);
    }

    /**
     * Sets login prompt to wait for before sending username.
     * Default value is "login:"
     *
     * @param loginPrompt the login prompt
     * @param regex       true if prompt to match is a regular expression, false otherwise
     *
     * @see #setRegexMatchLength
     */
    public synchronized void setLoginPrompt(String loginPrompt, boolean regex) {
        this.loginPrompt = loginPrompt;
        this.loginPromptRegex = regex;
    }

    /**
     * Sets password prompt to wait for before sending password.
     * Default value is "Password:"
     *
     * @param string the password prompt
     */
    public synchronized void setPasswordPrompt(String string) {
        setPasswordPrompt(string, false);
    }

    /**
     * Sets password prompt to wait for before sending password.
     * Default value is "Password:"
     *
     * @param string the password prompt
     * @param regex  true if prompt to match is a regular expression, false otherwise
     *
     * @see #setRegexMatchLength
     */
    public synchronized void setPasswordPrompt(String string, boolean regex) {
        passwordPrompt = string;
        passwordPromptRegex = regex;
    }

    /**
     * Sets shell prompt to wait for before sending commands.
     * Default value is "$"
     *
     * @param string the shell prompt
     */
    public synchronized void setShellPrompt(String string) {
        setShellPrompt(string, false);
    }

    /**
     * Sets shell prompt to wait for before sending commands.
     * Default value is "$"
     *
     * @param string the shell prompt
     * @param regex  true if prompt to match is a regular expression, false otherwise
     *
     * @see #setRegexMatchLength
     */
    public synchronized void setShellPrompt(String string, boolean regex) {
        shellPrompt = string;
        shellPromptRegex = regex;
    }

    /**
     * Sets the number of characters to read from the end of the response buffer when using regular expressions
     * to detect start or end prompts.  Default value is 20.
     *
     * @param regexMatchLength
     */
    public synchronized void setRegexMatchLength(int regexMatchLength) {
        this.regexMatchLength = regexMatchLength;
    }

    /**
     * Gets the number of characters to read from the end of the response buffer when using regular expressions
     * to detect start or end prompts.  Default value is 20.
     *
     * @return match length
     */
    public synchronized int getRegexMatchLength() {
        return regexMatchLength;
    }


    /**
     * Sets the polling interval used when checking for completion of commands sent using send or sendWait methods.
     * Default interval is 100ms.
     *
     * @param pollingInterval the polling interval in milliseconds.
     *
     * @see #send
     * @see #sendWait
     */
    public synchronized void setPollingInterval(int pollingInterval) {
        this.pollingInterval = pollingInterval;
    }

    /**
     * Gets the polling interval used when checking for completion of commands sent using send or sendWait methods.
     * Default interval is 100ms.
     *
     * @return the polling interval in milliseconds.
     *
     * @see #send
     * @see #sendWait
     */
    public synchronized int getPollingInterval() {
        return pollingInterval;
    }


    /**
     * Handles option negotiation and debugging information.
     */
    class TelnetSessionListener
            extends TelnetAdapter {

        private TelnetSession session;

        TelnetSessionListener(TelnetSession session) {
            this.session = session;
        }

        /**
         * Invoked when Telnet server requests that the Telnet client begin performing specified <code>TelnetOption</code>.
         *
         * @param event a <code>DoOptionEvent</code>
         *
         * @see DoOptionEvent
         * @see TelnetOption
         */
        public void doOption(DoOptionEvent event) {
            telnet.sendWontOption(event.getOption());
        }

        /**
         * Invoked when Telnet server offers to begin performing specified <code>TelnetOption</code>.
         *
         * @param event a <code>WillOptionEvent</code>
         *
         * @see WillOptionEvent
         * @see TelnetOption
         */
        public void willOption(WillOptionEvent event) {
            telnet.sendDontOption(event.getOption());
        }
    }

    /**
     * Checks if debugging is enabled.
     * Default is disabled.
     *
     * @return true if debugging enabled, false otherwise
     */
    public synchronized boolean isDebug() {
        return telnet.getDebug();
    }

    /**
     * Sends debug message to debug stream.
     *
     * @param msg the debug message to send
     *
     * @see #setDebugStream
     */
    public synchronized void debug(String msg) {
        telnet.debug(msg);
    }

    /**
     * Enables or disables debugging information.
     * In debug enabled mode all data received from telnet server is output to
     * the debug stream.  Default is disabled.
     *
     * @param b true to enable debugging, false to disable
     *
     * @see #setDebugStream
     */
    public synchronized void setDebug(boolean b) {
        telnet.setDebug(b);
    }

    /**
     * Sets PrintStream used in outputting debug information.
     * Default is System.out
     *
     * @param stream a PrintStream
     */
    public synchronized void setDebugStream(PrintStream stream) {
        telnet.setDebugStream(stream);
    }

    /**
     * Gets PrintStream used in outputting debug information.
     * Default is Sytem.out
     *
     * @return a PrintStream
     *
     * @see #setDebug
     * @see #setDebugStream
     */
    public synchronized PrintStream getDebugStream() {
        return telnet.getDebugStream();
    }

    /**
     * Gets terminator used when sending data to telnet server.
     * Default value is LF (Line Feed).
     *
     * @return the command terminator
     */
    public synchronized String getCommandTerminator() {
        return commandTerminator;
    }

    /**
     * Sets terminator used when sending data to telnet server.
     * Default value is LF (Line Feed).
     *
     * @param string the command terminator
     */
    public synchronized void setCommandTerminator(String string) {
        commandTerminator = string;
    }

    /**
     * Gets the port of the telnet server.
     *
     * @return the port
     */
    public synchronized int getPort() {
        return telnet.getPort();
    }

    /**
     * Gets the hostname of telnet server.
     *
     * @return the hostname
     */
    public synchronized String getHostname() {
        return telnet.getHostname();
    }

    /**
     * Sets the hostname of telnet server.
     *
     * @param hostname the hostname
     */
    public synchronized void setHostname(String hostname) {
        telnet.setHostname(hostname);
    }

    /**
     * Sets the port of the telnet server.
     *
     * @param i the port
     */
    public synchronized void setPort(int i) {
        telnet.setPort(i);
    }

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

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


    /**
     * Pauses current session for specified timeout.
     *
     * @param timeout time in milliseconds
     */
    public synchronized void pause(long timeout) {
        try {
            Thread.sleep(timeout);
        } catch (Exception e) {
            // todo: ignore?
        }
    }

    /**
     * Gets underlying Telnet instance.
     *
     * @return underlying Telnet instance
     */
    public synchronized Telnet getTelnet() {
        return telnet;
    }
}
