/*
 * In current implementation, the destination path for 'get' and 'put'
 * commands must be a file, not a directory.
 *
 *** For windows ***
 *
 * javac -cp jsch-0.1.54.jar\;. Sftp.java
 * java -cp jsch-0.1.54.jar\;. Sftp
 *
 *
 *** For linux ***
 *
 * javac -cp jsch-0.1.54.jar:. Sftp.java
 * java -cp jsch-0.1.54.jar:. Sftp
 *
 * Copyright (C) 2017-8 Sidney Marshall (swm@cs.rit.edu)
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import com.jcraft.jsch.SftpStatVFS;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.ListDataListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;

/**
 * This class provides a window for interactions with various
 * channels. Currently shell and sftp channels are supported.
 */
public class Sftp extends JFrame {
  static final long serialVersionUID = 42L;

  static JSch jsch = new JSch();
  Thread thread; // the thread to interrupt on closing
  Channel channel; // the channel for this window

  /**
   * Create an interaction window and execute factored code common to
   * all types of windows.
   *
   * @param title The title of the window
   */
  Sftp(String title) {
    super(title);
    addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          //System.out.println("Closing...");
          dispose();
        }
        public void windowClosed(WindowEvent e) {
          closeChannel(channel);
          if(thread != null) thread.interrupt();
        }
      });
  }

  // Set the location of the known hosts file
  static {
    try {
      jsch.setKnownHosts("~/.ssh/known_hosts");
    } catch(JSchException e) {
      e.printStackTrace();
    }
  }

  // map of Sessions to list of channels
  static HashMap<Session, ArrayList<Channel>> sessions
    = new HashMap<Session, ArrayList<Channel>>();

  /**
   * Adds text to the window at the end.
   *
   * This method does not have to be called from the event dispatching
   * thread.
   *
   * @param text The JTextPane to add the text to
   * @param s The text to add
   */
  static void addText(final JTextPane text, final String s) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          try {
            Document doc = text.getDocument();
            if(s.charAt(0) == '\b') {
              int len = doc.getLength();
              if(len > 0) {
                doc.remove(len - 1, 1);
              }
            } else {
              doc.insertString(doc.getLength(), s, null);
            }
            text.setCaretPosition(doc.getLength());
          } catch (BadLocationException e) {
            System.out.println(e.getMessage());
          }
        }
      });
  }

  // A simple GUI for a sftp client.

  /**
   * Opens a new session to [host/user]. Add an entry to sessions with
   * a key of the session and the value an empty
   * ArrayList<Channel>. Returns null if unsuccessful.
   *
   * Must be invoked on the event dispatching thread.
   *
   * @param host The name@host of the host to connect to
   * @return The newly opened session or null if unsuccessful
   */
  static Session openSession(String host) {
    try {
      Session session;
      if(host == null) {
        host = JOptionPane.showInputDialog("Enter username@hostname",
                                           System.getProperty("user.name") +
                                           "@localhost");
      }
      if(host == null) return null;
      String user = host.substring(0, host.indexOf('@'));
      host = host.substring(host.indexOf('@') + 1);
      int port = 22;

      session = jsch.getSession(user, host, port);
      session.setConfig("HashKnownHosts",  "yes");
      // username and password will be given via UserInfo interface.
      UserInfo ui = new MyUserInfo();
      session.setUserInfo(ui);
      session.connect(); // argument (if any) is timeout in milliseconds
      sessions.put(session, new ArrayList<Channel>());
      return session;
    } catch(JSchException e) {
      System.out.println(e);
      return null;
    }
  }

  /**
   * Warn about all remaining open channels for this session. Close
   * and then close them. Then close the session. Remove the session
   * from the sessions map.
   *
   * Closing the session closes all channels on this session.
   *
   * @param session The session to close
   */
  static void closeSession(Session session) {
    ArrayList<Channel> channels = sessions.get(session);
    if(channels != null) {
      for(Channel channel : channels) {
        System.out.println("Unclosed channel: " + channel);
        channel.disconnect();
      }
    }
    session.disconnect();
    //////// remove session from map
  }

  /**
   * Open an sftp Channel on the given Session. If successful, add
   * the new Channel to the sessions map entry and return the new
   * channel. Otherwise return null. If session is not in the map
   * then add it and warn.
   *
   * @param session The session on which to open the sftp channel
   * @return The newly opened sftp channel or null if failure
   */
  static ChannelSftp openSftpChannel(Session session) {
    try {
      Channel channel = (ChannelSftp)session.openChannel("sftp");
      channel.connect(10*1000);
      ArrayList<Channel> channels = sessions.get(session);
      if(channels == null) {
        System.out.println("Can't find session in map: " + session);
        sessions.put(session, new ArrayList<Channel>());
        channels = sessions.get(session);
      }
      channels.add(channel);
      return (ChannelSftp)channel;
    } catch(JSchException e) {
      System.out.println(e);
      return null;
    }
  }

  /**
   * Open a shell Channel on the given Session. If successful, add
   * the new Channel to the sessions map entry and return the new
   * channel. Otherwise return null. If session is not in the map
   * then add it and warn.
   *
   * @param session The session on which to open the sftp channel
   * @param in The InputStream for the shell
   * @param out The OutputStream for the shell
   * @return The newly opened ssh channel or null if failure
   */
  Channel openSSHChannel(Session session, InputStream in, OutputStream out) {
    try {
      Channel channel = session.openChannel("shell");
      channel.setInputStream(in);
      channel.setOutputStream(out);
      channel.connect(10*1000);
      ArrayList<Channel> channels = sessions.get(session);
      if(channels == null) {
        System.out.println("Can't find session in map: " + session);
        sessions.put(session, new ArrayList<Channel>());
        channels = sessions.get(session);
      }
      channels.add(channel);
      return channel;
    } catch(JSchException e) {
      System.out.println(e);
      e.printStackTrace();
      return null;
    }
  }

  /*
   * Close this channel and remove this channel from the Channel
   * ArrayList. A search must be done to find the channel. Warn if
   * channel can't be found.
   *
   * If this is the last channel on the session then close the session
   * too.
   *
   * @param channel The channel to close
   */
  static void closeChannel(Channel channel) {
    //System.out.println(channel);
    if(channel == null) {
      new Exception().printStackTrace();
      return;
    }
    channel.disconnect();
    for(Map.Entry<Session, ArrayList<Channel>> pair : sessions.entrySet()) {
      ArrayList<Channel> channels = pair.getValue();
      if(channels.remove(channel)) {
        if(channels.size() == 0) {
          try {
            Session session = channel.getSession();
            sessions.remove(session);
            closeSession(session);
            System.out.println("Session closed");
          } catch(JSchException e) {
            System.out.println(e);
          }
        }
        System.out.println("Number of channels remaining: " + pair.getValue().size());
        return;
      }
    }
    System.out.println("Channel not in map: " + channel);
  }

  /**
   * This subclass manages an sftp window.
   */
  public static class SftpWindow extends Sftp {
    static final long serialVersionUID = 42L;

    JTextPane text;
    JScrollPane scroll;
    String[] cmds;
    ChannelSftp channelSftp;
    StringBuffer wbuf = new StringBuffer();
    boolean filling = true;

    /**
     * Make an sftp window with the given title
     *
     * @param title The title for the window
     * @param session The session to open the sftp channel on
     */
    SftpWindow(String title, Session session) {
      super(title);
      text = new JTextPane();
      scroll = new JScrollPane(text);
      text.setEditable(false);
      text.addFocusListener(new FocusListener() {

          public void focusLost(FocusEvent fe) {
          }

          public void focusGained(FocusEvent fe) {
            Document doc = text.getDocument();
            text.setCaretPosition(doc.getLength());
            text.getCaret().setVisible(true);
          }
        });
      text.setPreferredSize(new Dimension(300, 100));
      add(scroll);

      channel = openSftpChannel(session);
      channelSftp = (ChannelSftp)channel;
    }

    /**
     * Get a line from the sftp window that the user types in.
     *
     * @return The line the user typed in
     */
    String getLine() {
      synchronized(wbuf) {
        while(filling) {
          try{
            wbuf.wait();
          } catch(InterruptedException e) {
            //e.printStackTrace(); // when thread receives InterruptedException
            return null; // shutting down
          }
        }
        String s = wbuf.toString();
        wbuf.setLength(0);
        filling = true;
        wbuf.notifyAll();
        return s;
      }
    }

    /**
     * Starts the window operating. The while loop reads commands,
     * packages them up, and sends them down the channel. It shows the
     * response in the window. The loop exits when the command is null
     * caused by an interrupted exception or a quit or exit command is
     * executed.
     */
    void start() {
      thread = Thread.currentThread();
      try {
        byte[] buf = new byte[1024];
        int i;
        int level = 0; // compression level
        text.addKeyListener(new KeyListener() {
            public void keyPressed(KeyEvent e) {}
            public void keyReleased(KeyEvent e) {}
            public void keyTyped(KeyEvent e) {
              char c = e.getKeyChar();
              addText(text, Character.toString(c));
              synchronized(wbuf) {
                if(filling) {
                  if(c == '\n') {
                    filling = false;
                    wbuf.notifyAll();
                  } else if(c == '\b') {
                    if(wbuf.length() > 0) wbuf.deleteCharAt(wbuf.length() - 1);
                  } else {
                    wbuf.append(c);
                  }
                }
              }
            }
          });
        Session session = channelSftp.getSession();

        while(true) {
          addText(text, "sftp> ");
          String strng = getLine();
          if(strng == null) break;
          int p = 0;
          while(p < strng.length() && Character.isWhitespace(strng.charAt(p))) {
            ++p;
          }
          String[] cmds = strng.substring(p).split("\\s+");
          if(cmds.length == 0) continue;
          if(cmds[0].equals("")) continue;

          String cmd = cmds[0];
          if(cmd.equals("quit")) {
            channelSftp.quit();
            break;
          }
          if(cmd.equals("exit")) {
            channelSftp.exit();
            break;
          }
          if(cmd.equals("rekey")) {
            session.rekey();
            continue;
          }
          if(cmd.equals("compression")) {
            if(cmds.length < 2) {
              addText(text, "compression level: " + level + '\n');
              continue;
            }
            level = Integer.parseInt(cmds[1]);
            if(level == 0) {
              session.setConfig("compression.s2c", "none");
              session.setConfig("compression.c2s", "none");
            } else {
              session.setConfig("compression.s2c", "zlib@openssh.com,zlib,none");
              session.setConfig("compression.c2s", "zlib@openssh.com,zlib,none");
            }
            session.rekey();
            continue;
          }
          if(cmd.equals("cd") || cmd.equals("lcd")) {
            if(cmds.length < 2) continue;
            String path = cmds[1];
            try {
              if(cmd.equals("cd")) channelSftp.cd(path);
              else channelSftp.lcd(path);
            } catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("rm") || cmd.equals("rmdir") || cmd.equals("mkdir")) {
            if(cmds.length < 2) continue;
            String path = cmds[1];
            try{
              if(cmd.equals("rm")) channelSftp.rm(path);
              else if(cmd.equals("rmdir")) channelSftp.rmdir(path);
              else channelSftp.mkdir(path);
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("chgrp") || cmd.equals("chown") || cmd.equals("chmod")) {
            if(cmds.length !=3) continue;
            String path = cmds[2];
            int foo = 0;
            if(cmd.equals("chmod")) {
              byte[] bar = cmds[1].getBytes();
              int k;
              for(int j = 0; j < bar.length; j++) {
                k = bar[j];
                if(k < '0' ||k > '7') {
                  foo = -1;
                  break;
                }
                foo <<= 3;
                foo |= (k - '0');
              }
              if(foo == -1)continue;
            } else {
              try{
                foo = Integer.parseInt(cmds[1]);
              } catch(Exception e) {
                System.out.println(e.toString());
                continue;
              }
            }
            try {
              if(cmd.equals("chgrp")) {
                channelSftp.chgrp(foo, path);
              } else if(cmd.equals("chown")) {
                channelSftp.chown(foo, path);
              } else if(cmd.equals("chmod")) {
                channelSftp.chmod(foo, path);
              }
            } catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("pwd") || cmd.equals("lpwd")) {
            String str = (cmd.equals("pwd")?"Remote":"Local");
            str += " working directory: ";
            if(cmd.equals("pwd")) str += channelSftp.pwd();
            else str += channelSftp.lpwd();
            addText(text, str + '\n');
            continue;
          }
          if(cmd.equals("ls") || cmd.equals("dir")) {
            String path = ".";
            if(cmds.length == 2) path = cmds[1];
            try{
              java.util.Vector<?> vv = channelSftp.ls(path);
              if(vv!=null) {
                for(int ii = 0; ii < vv.size(); ii++) {
                  Object obj = vv.get(ii);
                  if(obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry) {
                    addText(text, ((com.jcraft.jsch.ChannelSftp.LsEntry)obj).getLongname() + '\n');
                  }
                }
              }
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("lls") || cmd.equals("ldir")) {
            String path = ".";
            if(cmds.length==2) path = cmds[1];
            java.io.File file = new java.io.File(path);
            if(!file.exists()) {
              addText(text, path + ": No such file or directory" + '\n');
              continue;
            }
            if(file.isDirectory()) {
              String[] list = file.list();
              for(int ii = 0; ii < list.length; ii++) {
                addText(text, list[ii] + '\n');
              }
              continue;
            }
            addText(text, path + '\n');
            continue;
          }
          if(cmd.equals("get") ||
             cmd.equals("get-resume") || cmd.equals("get-append") ||
             cmd.equals("put") ||
             cmd.equals("put-resume") || cmd.equals("put-append")) {
            if(cmds.length!=2 && cmds.length!=3) continue;
            String p1 = cmds[1];
            String p2 = ".";
            if(cmds.length == 3) p2 = cmds[2];
            try{
              SftpProgressMonitor monitor = new MyProgressMonitor();
              if(cmd.startsWith("get")) {
                int mode = ChannelSftp.OVERWRITE;
                if(cmd.equals("get-resume")) {
                  mode = ChannelSftp.RESUME;
                } else if(cmd.equals("get-append")) {
                  mode = ChannelSftp.APPEND;
                }
                channelSftp.get(p1, p2, monitor, mode);
              } else {
                int mode = ChannelSftp.OVERWRITE;
                if(cmd.equals("put-resume")) {
                  mode = ChannelSftp.RESUME;
                } else if(cmd.equals("put-append")) {
                  mode = ChannelSftp.APPEND;
                }
                channelSftp.put(p1, p2, monitor, mode);
              }
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("ln") || cmd.equals("symlink") ||
             cmd.equals("rename") || cmd.equals("hardlink")) {
            if(cmds.length!=3) continue;
            String p1 = cmds[1];
            String p2 = cmds[2];
            try {
              if(cmd.equals("hardlink")) {
                channelSftp.hardlink(p1, p2);
              } else if(cmd.equals("rename")) {
                channelSftp.rename(p1, p2);
              } else {
                channelSftp.symlink(p1, p2);
              }
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("df")) {
            if(cmds.length > 2) continue;
            String p1 = cmds.length == 1 ? ".": cmds[1];
            SftpStatVFS stat = channelSftp.statVFS(p1);

            long size = stat.getSize();
            long used = stat.getUsed();
            long avail = stat.getAvailForNonRoot();
            long root_avail = stat.getAvail();
            long capacity = stat.getCapacity();

            addText(text, "Size: " + size + '\n');
            addText(text, "Used: " + used + '\n');
            addText(text, "Avail: " + avail + '\n');
            addText(text, "(root): " + root_avail + '\n');
            addText(text, "%Capacity: " + capacity + '\n');

            continue;
          }
          if(cmd.equals("stat") || cmd.equals("lstat")) {
            if(cmds.length !=2) continue;
            String p1 = cmds[1];
            SftpATTRS attrs = null;
            try {
              if(cmd.equals("stat")) {
                attrs = channelSftp.stat(p1);
              } else {
                attrs = channelSftp.lstat(p1);
              }
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            if(attrs!=null) {
              addText(text, attrs + "\n");
            }
            continue;
          }
          if(cmd.equals("readlink")) {
            if(cmds.length != 2) continue;
            String p1 = cmds[1];
            String filename = null;
            try{
              filename = channelSftp.readlink(p1);
              addText(text, filename + '\n');
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("realpath")) {
            if(cmds.length !=2) continue;
            String p1 = cmds[1];
            String filename = null;
            try {
              filename = channelSftp.realpath(p1);
              addText(text, filename + '\n');
            }
            catch(SftpException e) {
              System.out.println(e.toString());
            }
            continue;
          }
          if(cmd.equals("version")) {
            addText(text, "SFTP protocol version " + channelSftp.version() + '\n');
            continue;
          }
          if(cmd.equals("help") || cmd.equals("?")) {
            addText(text, help + '\n');
            continue;
          }
          addText(text, "unimplemented command: " + cmd + '\n');
        }
        dispose(); // all done
      }
      catch(Exception e) {
        e.printStackTrace();
      }
    }
  } // public static class SftpWindow extends JFrame

  /**
   * This subclass handles a shell window. It does not handle control
   * sequences.
   */
  public static class ShellWindow extends Sftp {
    static final long serialVersionUID = 42L;

    JTextPane text;
    JScrollPane scroll;
    PipedOutputStream pipedOutputStream;
    PipedInputStream pipedInputStream;

    /**
     * Implements an OutputStream that writes to the window.
     */
    class MyOutputStream extends OutputStream {

      /**
       * Add a character to the window. It might be more efficient to
       * also implement writing multiple characters at once too.
       *
       * @param c the 8-bit character to be written to the window
       */
      public void write(int c) {
        addText(text, Character.toString((char)c));
      }
    }

    /**
     * Make a shell window with the given title
     *
     * @param title The title for the window
     * @param session The session to open the shell channel on
     */
    ShellWindow(String title, Session session) {
      super(title);
      text = new JTextPane();
      text.setEditable(false);
      text.addFocusListener(new FocusListener() {

          public void focusLost(FocusEvent fe) {
          }

          public void focusGained(FocusEvent fe) {
            Document doc = text.getDocument();
            text.setCaretPosition(doc.getLength());
            text.getCaret().setVisible(true);
          }
        });
      scroll = new JScrollPane(text);
      text.setPreferredSize(new Dimension(300, 100));
      add(scroll);

      pipedOutputStream = new PipedOutputStream();
      try {
        pipedInputStream = new PipedInputStream(pipedOutputStream, 65536);
      } catch(IOException e) {
        e.printStackTrace();
      }
      MyOutputStream myOutputStream = new MyOutputStream();
      channel = openSSHChannel(session, pipedInputStream, myOutputStream);
      
      addWindowListener(new WindowAdapter() {
          public void windowClosing(WindowEvent e) {
          }
          public void windowClosed(WindowEvent e) {
            //try { myOutputStream.close(); } catch(IOException ex) {}
            try { pipedOutputStream.close(); } catch(IOException ex) {}
            //try { pipedInputStream.close(); } catch(IOException ex) {}
          }
        });
    }

    /**
     * Starts the window operating. Characters typed by the user are
     * sent to the shell and characters optput by the shell are
     * displayed in the window.
     */
    void start() {
      try {
        text.addKeyListener(new KeyListener() {
            public void keyPressed(KeyEvent e) {}
            public void keyReleased(KeyEvent e) {}
            public void keyTyped(KeyEvent e) {
              char chr = e.getKeyChar();
              try {
                pipedOutputStream.write(chr);
              } catch(IOException ex) {
                //// shut down channel & close window
                //// need a timer task to close window earlier
                System.out.println("Close down channel");
                ex.printStackTrace();
              }
            }
          });
      }
      catch(Exception e) {
        e.printStackTrace();
      }
    }
  } // public static class ShellWindow extends JFrame

  public static void main(String[] arg) {

    // launch thread window
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          ThreadWindow threadWin = new ThreadWindow("Threads");
          threadWin.pack();
          threadWin.setLocationRelativeTo(null);
          threadWin.setVisible(true);
          threadWin.run(1000);
        }
      });

    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          Session session1 = null;
          //Session session2 = null;

          /*
            JSch.setLogger(new com.jcraft.jsch.Logger() {
            public void log(int i, String s) {
            System.out.println(i + ": " + s);
            }

            public boolean isEnabled(int i) {
            return true;
            }
            });
          */

          String host = arg.length > 0 ? host = arg[0] : null;
          session1 = openSession(host);
          if(session1 == null) return; // could not open session
          //session2 = openSession(host);
          final SftpWindow window1 = new SftpWindow("sftp-1", session1);
          window1.pack();
          window1.setVisible(true);
          new Thread() {
            public void run() {
              window1.start();
              //System.out.println("Done");
            }
          }.start();

          final SftpWindow window2 = new SftpWindow("sftp-2", session1);
          window2.pack();
          window2.setVisible(true);
          new Thread() {
            public void run() {
              window2.start();
              //System.out.println("Done");
            }
          }.start();

          final ShellWindow window3 = new ShellWindow("shell-1", session1);
          window3.pack();
          window3.setVisible(true);
          new Thread() {
            public void run() {
              window3.start();
              //System.out.println("Shell done");
            }
          }.start();

          /*
            final ShellWindow window4 = new ShellWindow("shell-2", session1);
            window4.pack();
            window4.setVisible(true);
            new Thread() {
            public void run() {
            window4.start();
            //System.out.println("Shell done");
            }
            }.start();
          */
        }
      });
  }

  public static class MyUserInfo implements UserInfo, UIKeyboardInteractive{
    public String getPassword() { return passwd; }
    public boolean promptYesNo(String str) {
      Object[] options = { "yes", "no" };
      int foo = JOptionPane.showOptionDialog(null,
                                             str,
                                             "Warning",
                                             JOptionPane.DEFAULT_OPTION,
                                             JOptionPane.WARNING_MESSAGE,
                                             null, options, options[0]);
      return foo == 0;
    }

    String passwd;
    JTextField passwordField = (JTextField)new JPasswordField(20);

    public String getPassphrase() {
      return null;
    }

    public boolean promptPassphrase(String message) {
      return true;
    }

    public boolean promptPassword(String message) {
      Object[] ob = {passwordField};
      int result = JOptionPane.showConfirmDialog(null, ob, message,
                                                 JOptionPane.OK_CANCEL_OPTION);
      if(result == JOptionPane.OK_OPTION) {
	passwd = passwordField.getText();
	return true;
      } else {
        return false;
      }
    }

    public void showMessage(String message) {
      JOptionPane.showMessageDialog(null, message);
    }

    final GridBagConstraints gbc  =
      new GridBagConstraints(0,0,1,1,1,1,
                             GridBagConstraints.NORTHWEST,
                             GridBagConstraints.NONE,
                             new Insets(0,0,0,0),0,0);
    private Container panel;
    public String[] promptKeyboardInteractive(String destination,
                                              String name,
                                              String instruction,
                                              String[] prompt,
                                              boolean[] echo) {
      panel = new JPanel();
      panel.setLayout(new GridBagLayout());

      gbc.weightx = 1.0;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridx = 0;
      panel.add(new JLabel(instruction), gbc);
      gbc.gridy++;

      gbc.gridwidth = GridBagConstraints.RELATIVE;

      JTextField[] texts = new JTextField[prompt.length];
      for(int i = 0; i < prompt.length; i++) {
        gbc.fill = GridBagConstraints.NONE;
        gbc.gridx = 0;
        gbc.weightx = 1;
        panel.add(new JLabel(prompt[i]),gbc);

        gbc.gridx = 1;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weighty = 1;
        if(echo[i]) {
          texts[i] = new JTextField(20);
        } else {
          texts[i] = new JPasswordField(20);
        }
        panel.add(texts[i], gbc);
        gbc.gridy++;
      }

      if(JOptionPane.showConfirmDialog(null, panel,
                                       destination + ": " + name,
                                       JOptionPane.OK_CANCEL_OPTION,
                                       JOptionPane.QUESTION_MESSAGE)
         == JOptionPane.OK_OPTION) {
        String[] response = new String[prompt.length];
        for(int i = 0; i < prompt.length; i++) {
          response[i] = texts[i].getText();
        }
	return response;
      } else {
        return null;  // cancel
      }
    }
  }

  public static class MyProgressMonitor implements SftpProgressMonitor{
    ProgressMonitor monitor;
    long count = 0;
    long max = 0;
    public void init(int op, String src, String dest, long max) {
      this.max = max;
      monitor = new ProgressMonitor(null,
                                    ((op == SftpProgressMonitor.PUT)?
                                     "put" : "get") + ": " + src,
                                    "",  0, (int)max);
      count = 0;
      percent = -1;
      monitor.setProgress((int)this.count);
      monitor.setMillisToDecideToPopup(1000);
    }
    private long percent = -1;
    public boolean count(long count) {
      this.count += count;

      if(percent >= this.count*100/max) return true;
      percent = this.count*100/max;

      monitor.setNote("Completed " + this.count + "(" + percent + "%) out of " + max + ".");
      monitor.setProgress((int)this.count);

      return !(monitor.isCanceled());
    }
    public void end() {
      monitor.close();
    }
  }

  private static String help =
    "      Available commands:\n" +
    "      * means unimplemented command.\n" +
    "cd path                       Change remote directory to 'path'\n" +
    "lcd path                      Change local directory to 'path'\n" +
    "chgrp grp path                Change group of file 'path' to 'grp'\n" +
    "chmod mode path               Change permissions of file 'path' to 'mode'\n" +
    "chown own path                Change owner of file 'path' to 'own'\n" +
    "df [path]                     Display statistics for current directory or\n" +
    "                              filesystem containing 'path'\n" +
    "help                          Display this help text\n" +
    "get remote-path [local-path]  Download file\n" +
    "get-resume remote-path [local-path]  Resume to download file.\n" +
    "get-append remote-path [local-path]  Append remote file to local file\n" +
    "hardlink oldpath newpath      Hardlink remote file\n" +
    "*lls [ls-options [path]]      Display local directory listing\n" +
    "ln oldpath newpath            Symlink remote file\n" +
    "*lmkdir path                  Create local directory\n" +
    "lpwd                          Print local working directory\n" +
    "ls [path]                     Display remote directory listing\n" +
    "*lumask umask                 Set local umask to 'umask'\n" +
    "mkdir path                    Create remote directory\n" +
    "put local-path [remote-path]  Upload file\n" +
    "put-resume local-path [remote-path]  Resume to upload file\n" +
    "put-append local-path [remote-path]  Append local file to remote file.\n" +
    "pwd                           Display remote working directory\n" +
    "stat path                     Display info about path\n" +
    "exit                          Quit sftp\n" +
    "quit                          Quit sftp\n" +
    "rename oldpath newpath        Rename remote file\n" +
    "rmdir path                    Remove remote directory\n" +
    "rm path                       Delete remote file\n" +
    "symlink oldpath newpath       Symlink remote file\n" +
    "readlink path                 Check the target of a symbolic link\n" +
    "realpath path                 Canonicalize the path\n" +
    "rekey                         Key re-exchanging\n" +
    "compression level             Packet compression will be enabled\n" +
    "version                       Show SFTP version\n" +
    "?                             Synonym for help";

    /**
     * This is a window that displays all of the running threads.
     * It is independent of all other parts of the program.
     */
  private static class ThreadWindow extends JFrame {
    static final long serialVersionUID = 42L;

    private JList<String> jlist;
    private JScrollPane threadList;
    private Timer timer = null;
    private ArrayList<Thread> threads = new ArrayList<Thread>();

    DefaultListModel<String> model = new DefaultListModel<String>() {
        static final long serialVersionUID = 42L;

        ArrayList<ListDataListener> listeners = new ArrayList<ListDataListener>();
        public void addListDataListener(ListDataListener l) {
          listeners.add(l);
        }

        public String getElementAt(int index) {
          if(index < threads.size()) {
            Thread thread = threads.get(index);
            return (thread.isDaemon() ? "*" : "") + thread;
          } else {
            return "";
          }
        }

        public int getSize() {
          return threads.size();
          //return 20;
        }

        public void removeListDataListener(ListDataListener l) {
          listeners.remove(l);
        }
      };

    /**
     * Create a thread window with the given title.
     *
     * @param title The title of the window.
     */
    public ThreadWindow(String title) {
      super(title);
      addWindowListener(new WindowAdapter() {
          public void windowClosing(WindowEvent e) {
            dispose();
            if(timer != null) {
              timer.stop();
            }
          }

          public void windowClosed(WindowEvent e) {
          }
        });

      jlist = new JList<String>(model);
      threadList = new JScrollPane(jlist);
      jlist.setPreferredSize(new Dimension(200, 600));
      jlist.setVisibleRowCount(20);
      getContentPane().add("Center", threadList);
      jlist.addMouseListener(new MouseAdapter() {
        public void mouseClicked(MouseEvent e) {
          if(e.getButton() == 1) {
            int i = jlist.locationToIndex(e.getPoint());
            if(i >= 0) {
              System.err.println("---------");
              System.err.println(threads.get(i));
              System.err.println("---------");
              for(StackTraceElement ste : threads.get(i).getStackTrace()) {
                System.err.println(ste);
              }
              System.err.println();
            }
          } else if(e.getButton() == 3) {
            int i = jlist.locationToIndex(e.getPoint());
            threads.get(i).interrupt();
          }
        }
      });
    }

    /**
     * Starts the update process to update the list of threads.
     *
     * @param delay The update interval in milliseconds
     */
    public void run(int delay) {
      //int delay = seconds * 1000; // update interval in milliseconds
      timer = new Timer(delay, new ActionListener() {
          public void actionPerformed(ActionEvent evt) {
            ThreadGroup top = Thread.currentThread().getThreadGroup();
            while(top.getParent() != null) {
              top = top.getParent();
            }
            Thread groups[] = new Thread[100];
            int n = top.enumerate(groups, true);
            threads.clear();
            for(int i = 0; i < n; i++) {
              threads.add(groups[i]);
            }
            /////// this seems to work ????????
            jlist.setListData(new String[threads.size()]); // data from model
            jlist.setModel(model);
          }
        });
      timer.start();
    }
  }
} // public class Sftp
