/*
 * @(#)Rexec.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.bsd;

import com.jscape.inet.util.TimedSocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Vector;


/**
 * Implements the functionality of a BSD rexec client.
 * <p/>
 * Example Usage:
 * <p/>
 * <pre>
 * Rexec rexec = new Rexec("remote.host.com", "johndoe", "SeCrEt","ls -la", false);
 * try
 * {
 *    rexec.connect();
 *    rexec.execute();
 *    InputStream input = rexec.getInputStream();
 *    int in = input.read();
 *    StringBuffer buffer = new StringBuffer();
 *    if(in == 0)
 *    {
 *       buffer.append("Success! Remote host returned: \n");
 *    }
 *    else
 *    {
 *       buffer.append("Failure! Remote host returned: \n");
 *    }
 *       while((in = input.read()) != -1)
 *    {
 *       buffer.append((char)in);
 *    }
 *    System.out.println(buffer.toString());
 * }
 * catch(Exception e)
 * {
 *    System.out.println(e);
 * }
 * </pre>
 */
public class Rexec
        implements Serializable {
    /**
     * The set of event listeners for this class.
     */
    protected transient Vector bsdListeners = new Vector();

    /**
     * The Rexec remote port.
     */
    protected int port = 512;

    /**
     * The timeout in milliseconds.
     */
    protected int timeout;

    /**
     * Flag to determine if an error stream has been set.
     */
    protected boolean hasErrorStream = false;

    /**
     * The InputStream used for reading data.
     */
    protected transient InputStream input;

    /**
     * The OutputStream used for writing data.
     */
    protected transient OutputStream output;

    /**
     * The InputStream used for reading error data.
     */
    protected transient InputStream error;

    /**
     * The Socket used for reading and writing data.
     */
    protected transient Socket rexecSocket;

    /**
     * The Socket used for reading error data.
     */
    protected transient Socket errSocket;

    /**
     * The hostname or ip address of the remote host.
     */
    protected String remoteHost = "";
    private String username = "";
    private String password = "";
    private String command = "";

    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;

    /**
     * Initializes the Rexec class with or without the error stream.
     * That is, whether the stderr  from remote host will be returned
     * via dedicated connection - <code>Rexec(true)</code> or together
     * with stdout - <code>Rexec(false)</code>.
     * All other connection parameters are initialized to an empty <code> String </code>
     * <p/>
     *
     * @param hasErrorStream If <code>true</code>, initialize separate error stream
     */
    public Rexec(boolean hasErrorStream) {
        
        this.hasErrorStream = hasErrorStream;
    }

    /**
     * Constructs the Rexec class and sets all the connection parameters.
     * <p/>
     *
     * @param remoteHost     the Internet name of the remote host.
     * @param username       the username of the user, whose credentials are to be used to execute the <code>command</code>.
     * @param password       the password associated with <code> username</code>.
     * @param command        the command with arguments, to execute.
     * @param hasErrorStream if true, remote host's stderr will be outputted through
     *                       separate error stream. If false, stderr will be outputted
     *                       along with stdout stream.
     */
    public Rexec(String remoteHost, String username, String password,
                 String command, boolean hasErrorStream) {
        this(hasErrorStream);
        setRemoteHost(remoteHost);
        setUsername(username);
        setPassword(password);
        setCommand(command);
    }

    /**
     * Initializes the InputStream that will be used to receive stderr.
     *
     * @return InputStream from which the stderr can be read.
     *
     * @throws IOException if an I/O related error occurs
     */
    private InputStream initErrorStream()
            throws IOException {
        ServerSocket err = new ServerSocket(0);

        //Send ASCII encoded port number through existing connection
        output.write(Integer.toString(err.getLocalPort()).getBytes());
        output.write('\0');
        output.flush();

        errSocket = err.accept();
        err.close();

        return errSocket.getInputStream();
    }

    /**
     * Returns the <code>InputStream</code> from which the standard output of the remote
     * process can be read.  The input stream will be set after <code>connect()</code>
     * method invocation.
     * <p/>
     *
     * @return <code>InputStream</code> from which the standard output of the remote
     *         process can be read. Until the <code>connect()</code> method has been invoked,
     *         <code>null</code> will be returned.
     */
    public InputStream getInputStream() {
        return input;
    }

    /**
     * Returns the <code>OutputStream</code> to which the standard input of the remote
     * process can be written to.  The output stream will be set after <code>connect()</code>
     * method invocation.
     * <p/>
     *
     * @return <code>OutputStream</code> from which the standard input of the remote
     *         process can be written. Until the <code>connect()</code> method has been invoked,
     *         <code>null</code> will be returned.
     */
    public OutputStream getOutputStream() {
        return output;
    }

    /**
     * Returns the <code>InputStream</code> from which the standard error of the remote
     * process can be read if a separate error stream is requested from
     * the remote host.  The error stream will only be set after a successful
     * <code>execute()</code> invocation.
     * <p/>
     *
     * @return <code>InputStream</code> from which the standard error of the remote
     *         process can be read if a separate error stream is requested from
     *         the server.  Otherwise, <code>null</code> will be returned.
     */
    public InputStream getErrorStream() {
        return error;
    }

    /**
     * Executes command on the remote host. After calling this method,
     * command is sent to remote host, and any response can be parsed
     * using remote host's standard output. This output can be read
     * through an <code>InputStream</code>, returned by
     * <code><a href="#getInputStream"> getInputStream() </a></code>
     * <p/>
     * If Rexec object was constructed with separate error stream, remote
     * host will return all stderr output through that stream. Separate
     * error stream is available through an <code>InputStream</code>, returned by
     * <code>getInputStream()</code>
     *
     * @throws BsdException if the <code>execute()</code> fails.
     * @see #getInputStream
     */
    public void execute()
            throws BsdException {
        synchronized (this) {
            try {
                if (hasErrorStream) {
                    error = initErrorStream();
                } else {
                    output.write('\0');
                }

                output.write(username.getBytes());
                output.write('\0');
                output.write(password.getBytes());
                output.write('\0');
                output.write(command.getBytes());
                output.write('\0');
                output.flush();
            } catch (IOException e) {
                throw new BsdException(e.toString());
            }
        }
    }

    /**
     * Connects to the remote host, initializing streams that handle stdin and stdout.
     *
     * @throws BsdException if an error occurs while connecting.
     */
    public void connect()
            throws BsdException {
        synchronized (this) {
            try {
                if (this.proxyHostname == null) {
                    rexecSocket = TimedSocket.getSocket(remoteHost, port, timeout);
                } else {
                    rexecSocket = TimedSocket.getSocket(remoteHost, port, timeout,
                                                        this.proxyHostname, this.proxyPort, this.proxyUsername,
                                                        this.proxyPassword, this.proxyType);
                }
                input = rexecSocket.getInputStream();
                output = rexecSocket.getOutputStream();
                fireBsdConnected(getRemoteHost());
            } catch (UnknownHostException e) {
                throw new BsdException(e.toString());
            } catch (IOException e) {
                throw new BsdException(e.toString());
            }
        }
    }

    /**
     * Disconnects from the server, and cleans up all associated connections.
     *
     * @throws BsdException if an error occurs while disconnecting.
     */
    public void disconnect()
            throws BsdException {
        synchronized (this) {
            try {
                if (error != null) {
                    error.close();
                    errSocket.close();
                }

                output.close();
                input.close();
                rexecSocket.close();
                error = null;
                fireBsdDisconnected(getRemoteHost());
            } catch (IOException e) {
                throw new BsdException(e.toString());
            }
        }
    }

    /**
     * Adds Bsd event listener.
     *
     * @param listener the listener to add
     *
     * @see BsdListener
     */
    public synchronized void addBsdListener(BsdListener listener) {
        bsdListeners.addElement(listener);
    }

    /**
     * Removes specified Bsd event listener.
     *
     * @param listener the listener to remove
     *
     * @see BsdListener
     */
    public synchronized void removeBsdListener(BsdListener listener) {
        bsdListeners.removeElement(listener);
    }

    /**
     * Gets all BsdListener currently bound to this <code>Rexec</code> instance.
     *
     * @return a <code>Vector</code> of <code>BsdListener</code>
     *
     * @see BsdListener
     */
    public synchronized Vector getBsdListeners() {
        return bsdListeners;
    }

    /**
     * Fires <code>BsdDisconnectedEvent</code>.
     * This event is fired upon disconnecting from Bsd r* service server.
     *
     * @param hostname the hostname
     *
     * @see BsdDisconnectedEvent
     */
    protected void fireBsdDisconnected(String hostname) {
        Vector thisList = new Vector();
        BsdDisconnectedEvent thisEvent = new BsdDisconnectedEvent(this, hostname);

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

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

    /**
     * Fires <code>BsdConnectedEvent</code>.
     * This event is fired upon connecting to Bsd r* service server.
     *
     * @param hostname the hostname
     *
     * @see BsdConnectedEvent
     */
    protected void fireBsdConnected(String hostname) {
        Vector thisList = new Vector();
        BsdConnectedEvent thisEvent = new BsdConnectedEvent(this, hostname);

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

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

    /**
     * Sets the name of the remote host to cennect to.
     *
     * @param remoteHost the remote host to connect to
     */
    public void setRemoteHost(String remoteHost) {
        this.remoteHost = remoteHost;
    }

    /**
     * Gets the name of the remote host.
     *
     * @return the remote host to connect to
     */
    public String getRemoteHost() {
        return remoteHost;
    }

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

    /**
     * Gets the timeout for opening connection to server.
     *
     * @return timeout in milliseconds
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Sets the port to be used to make the connection.
     * TCP/IP restrictions apply - only valid port numbers are allowed. Otherwise,
     * a <code>BsdException</code> is thrown.
     *
     * @param port the port to be used to make the connection.
     *
     * @throws BsdException if port is invalid
     */
    public void setPort(int port)
            throws BsdException {
        if ((port < 1) || (port > 65534)) {
            throw new BsdException("Invalid port");
        }

        this.port = port;
    }

    /**
     * Gets the port to be used to make the connection.
     *
     * @return the port used to make the connection.
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the username to be sent to the remote host.
     *
     * @param username the username to be sent to the remote host.
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Gets the username to be sent to the remote host.
     *
     * @return the username to be sent to the remote host
     */
    public String getUsername() {
        return username;
    }

    /**
     * Sets the password to be sent to the remote host.
     *
     * @param password the password to be sent to the remote host.
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Gets the password to be sent to the remote host.
     *
     * @return the password to be sent to the remote host
     */
    public String getPassword() {
        return password;
    }

    /**
     * Sets the command to be sent to the remote host.
     *
     * @param command the command to be sent to the remote host.
     */
    public void setCommand(String command) {
        synchronized (this) {
            this.command = command;
        }
    }

    /**
     * Gets the command to be sent to the remote host.
     *
     * @return the command to be sent to the remote host.
     */
    public String getCommand() {
        return command;
    }

    /**
     * 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);
    }

    /**
     * Returns weather the Rexec object was initialized with or without
     * separate error stream.
     *
     * @return <code>true</code> if there is a separate error stream <code>false</code> otherwise
     */
    public boolean hasErrorStream() {
        return hasErrorStream;
    }
}
