/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000,2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Ant" and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.tools.ant.taskdefs.optional.net;

import com.oroinc.net.telnet.TelnetClient;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.Vector;

/**
 * Automates the telnet protocol.
 *
 * @author <a href="mailto:ScottCarlson@email.com">ScottCarlson@email.com</a>
 * @version $Revision: 1.12.2.4 $
 */

public class TelnetTask extends Task {
    /**
     *  The userid to login with, if automated login is used
     */
    private String userid  = null;

    /**
     *  The password to login with, if automated login is used
     */
    private String password = null;

    /**
     *  The server to connect to. 
     */
    private String server  = null;

    /**
     *  The tcp port to connect to. 
     */
    private int port = 23;

    /**
     *  The Object which handles the telnet session.
     */
    private AntTelnetClient telnet = null;

    /**
     *  The list of read/write commands for this session
     */
    private Vector telnetTasks = new Vector();

    /** 
     *  If true, adds a CR to beginning of login script
     */
    private boolean addCarriageReturn = false;

    /**
     *  Default time allowed for waiting for a valid response
     *  for all child reads.  A value of 0 means no limit.
     */
    private Integer defaultTimeout = null;

    /** 
     *  Verify that all parameters are included. 
     *  Connect and possibly login
     *  Iterate through the list of Reads and writes 
     */
    public void execute() throws BuildException {
       /**  A server name is required to continue */
       if (server == null) {
           throw new BuildException("No Server Specified");
       }
       /**  A userid and password must appear together 
        *   if they appear.  They are not required.
        */
       if (userid == null && password != null) {
           throw new BuildException("No Userid Specified");
       }
       if (password == null && userid != null) {
           throw new BuildException("No Password Specified");
       }

       /**  Create the telnet client object */
       telnet = new AntTelnetClient();
       try {
           telnet.connect(server, port);
       } catch (IOException e) {
           throw new BuildException("Can't connect to " + server);
       }
       /**  Login if userid and password were specified */
       if (userid != null && password != null) {
          login();
       }
       /**  Process each sub command */
       Enumeration tasksToRun = telnetTasks.elements();
       while (tasksToRun != null && tasksToRun.hasMoreElements()) {
           TelnetSubTask task = (TelnetSubTask) tasksToRun.nextElement();
           if (task instanceof TelnetRead && defaultTimeout != null) {
               ((TelnetRead) task).setDefaultTimeout(defaultTimeout);
           }
           task.execute(telnet);
       }
    }

    /**  
     *  Process a 'typical' login.  If it differs, use the read 
     *  and write tasks explicitely
     */
    private void login() {
       if (addCarriageReturn) {
          telnet.sendString("\n", true);
       }
       telnet.waitForString("ogin:");
       telnet.sendString(userid, true);
       telnet.waitForString("assword:");
       telnet.sendString(password, false);
    }

    /**
     * Set the the login id to use on the server; 
     * required if <tt>password</tt> is set. 
     */
    public void setUserid(String u) { this.userid = u; }

    /**
     *  Set the the login password to use
     * required if <tt>userid</tt> is set. 
     */
    public void setPassword(String p) { this.password = p; }

    /**
     *  Set the hostname or address of the remote server.
     */
    public void setServer(String m) { this.server = m; }

    /**
     *  Set the tcp port to connect to; default is 23.
     */
    public void setPort(int p) { this.port = p; }

    /**
     *  send a carriage return after connecting; optional, defaults to false.
     */
    public void setInitialCR(boolean b) {
       this.addCarriageReturn = b;
    }

    /**
     * set a default timeout in seconds to wait for a response, 
     * zero means forever (the default) 
     */
    public void setTimeout(Integer i) {
       this.defaultTimeout = i;
    }

    /**
     *  A string to wait for from the server. 
     *  A subTask &lt;read&gt; tag was found.  Create the object, 
     *  Save it in our list, and return it.
     */
   
    public TelnetSubTask createRead() {
        TelnetSubTask task = (TelnetSubTask) new TelnetRead();
        telnetTasks.addElement(task);
        return task;
    }

    /**
     *  Add text to send to the server
     *  A subTask &lt;write&gt; tag was found.  Create the object, 
     *  Save it in our list, and return it.
     */
    public TelnetSubTask createWrite() {
        TelnetSubTask task = (TelnetSubTask) new TelnetWrite();
        telnetTasks.addElement(task);
        return task;
    }

    /**  
     *  This class is the parent of the Read and Write tasks.
     *  It handles the common attributes for both.
     */
    public class TelnetSubTask {
        protected String taskString = "";
        public void execute(AntTelnetClient telnet) 
                throws BuildException {
            throw new BuildException("Shouldn't be able instantiate a SubTask directly");
        }
        
        /**
         *  the message as nested text
         */
        public void addText(String s) {
            setString(getProject().replaceProperties(s));
        }
        
        /**
         * the message as an attribute
         */
        public void setString(String s) {
           taskString += s; 
        }
    }
    
    /**
     *  Sends text to the connected server 
     */
    public class TelnetWrite extends TelnetSubTask {
        private boolean echoString = true;
        public void execute(AntTelnetClient telnet) 
               throws BuildException {
           telnet.sendString(taskString, echoString);
        }
        
        /**
         * should the message be echoed to the log? default=true.
         */
        public void setEcho(boolean b) {
           echoString = b;
        }
    }
    
    /**
     *  Reads the output from the connected server
     *  until the required string is found or we time out. 
     */
    public class TelnetRead extends TelnetSubTask {
        private Integer timeout = null;
        public void execute(AntTelnetClient telnet) 
               throws BuildException {
            telnet.waitForString(taskString, timeout);
        }
        /**
         *  a timeout value that overrides any task wide timeout. 
         */
        public void setTimeout(Integer i) {
           this.timeout = i;
        }
        
        /**
         * Sets the default timeout if none has been set already
         * @ant.attribute ignore="true"
         */
        public void setDefaultTimeout(Integer defaultTimeout) {
           if (timeout == null) {
              timeout = defaultTimeout;
           }
        }
    }
    
    /**
     *  This class handles the abstraction of the telnet protocol.
     *  Currently it is a wrapper around <a href="www.oroinc.com">ORO</a>'s 
     *  NetComponents
     */
    public class AntTelnetClient extends TelnetClient {
        /**
         * Read from the telnet session until the string we are 
         * waiting for is found 
         * @param s The string to wait on 
         */
        public void waitForString(String s) {
            waitForString(s, null);
        }

        /**
         * Read from the telnet session until the string we are 
         * waiting for is found or the timeout has been reached
         * @param s The string to wait on 
         * @param timeout The maximum number of seconds to wait
         */
        public void waitForString(String s, Integer timeout) {
            InputStream is = this.getInputStream();
            try {
                StringBuffer sb = new StringBuffer();
                if (timeout == null || timeout.intValue() == 0) {
                    while (sb.toString().indexOf(s) == -1){
                        sb.append((char) is.read());
                    }
                } else {
                    Calendar endTime = Calendar.getInstance(); 
                    endTime.add(Calendar.SECOND, timeout.intValue());
                    while (sb.toString().indexOf(s) == -1) {
                        while (Calendar.getInstance().before(endTime) &&
                        is.available() == 0) {
                            Thread.sleep(250);
                        }
                        if (is.available() == 0) {
                            throw new BuildException(
                                "Response timed-out waiting for \""+s+'\"', 
                                getLocation());
                        }
                        sb.append((char) is.read());
                    }
                }
                log(sb.toString(), Project.MSG_INFO);
            } catch (BuildException be) { 
                throw be;
            } catch (Exception e) { 
                throw new BuildException(e, getLocation());
            }
        }

        /**
        * Write this string to the telnet session.
        * @param echoString  Logs string sent
        */
        public void sendString(String s, boolean echoString) {
            OutputStream os = this.getOutputStream();
            try {
                os.write((s + "\n").getBytes());
                if (echoString) {
                    log(s, Project.MSG_INFO);
                }
                os.flush();
            } catch (Exception e) { 
                throw new BuildException(e, getLocation());
            }
        }
    }
}
