001/* A program to mimic the windows file browser. It allows:
002 *   displaying files and directories
003 *   editing file names
004 *   moving and copying multiple files and directories
005 *   folder (tree view) or file view
006 *
007 * Copyright (C) 2017-20 Sidney Marshall (swm@cs.rit.edu)
008 *
009 * This program is free software: you can redistribute it and/or modfy
010 * it under the terms of the GNU General Public License as published
011 * by the Free Software Foundation, either version 3 of the License,
012 * or (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017 * General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program. If not, see
021 * <http://www.gnu.org/licenses/>.
022 */
023
024/*
025 * javac -g -cp myjsch.jar:. FileBrowser.java
026 * javac -g -cp myjsch.jar\;. FileBrowser.java
027 * jdb -classpath myjsch.jar:. FileBrowser
028 * javac -g -cp myjsch.jar:. FileBrowser1.java
029 * javac -g -cp myjsch.jar\;. FileBrowser1.java
030 * jdb -classpath myjsch.jar:. FileBrowser1
031 *
032 * cp ~/home/Projects/sftp/jsch-0.1.54/src/main/java/myjsch.jar .
033 *
034 * java -Djava.net.preferIPv4Stack=true -jar filebrowser.jar -ssh sidney@server
035 */
036
037/*    *** TODO ***
038 *
039 * format date
040 * fix undo / redo
041 * rename file in place - mostly done - need to stop editing when selecting in file tree
042 *    TableCellEditor te = table.getCellEditor();
043 *    if(te != null) te.stopCellEditing();
044 * selectFromPath should do something reasonable with malformed or missing files
045 * get copy links working - linux done; windows impossible
046 * fix filenames with . and .. and ~
047 * make relative file names on command line work
048 * get scripting working (make or explicit)
049 * clean out unused methods and variables
050 * get .filepart working
051 *   x.filepart date only compares to x
052 * reopen a closed session if necessary
053 * redisplay if suspect directory listing has changed
054 *
055 * static MyPath stringToMyPath(String s)
056 *       make ~ work - hard because no sftp channel
057 *       make . work (current directory)
058 *       make .. work (parent directory)
059 *
060 * enable file transfers
061 * enable drag and drop from browser to comparison combo box(es)
062 *
063 * check .reload() calls - change to nodesWereInserted() ???
064 */
065
066/*
067 * Note: Using File objects will not work with non-UTF8 file
068 * names. Always use Path objects. Also, using FileInputStream and
069 * FileOutputStream cause GC slowups. Here is a handy table:
070 *
071 * Path Paths.get(uri);
072 *
073 * URI path.toUri();
074 *
075 * java.io.File (class)
076 * java.nio.file.Path (interface)
077 *
078 * file = new File("path/to/file.txt")
079 * path = Paths.get("path/to/file.txt")
080 *
081 * file = new File(parentFile, "file.txt")
082 * path = parentPath.resolve("file.txt")
083 *
084 * String file.getName()
085 * Path path.getFileName()
086 * String path.getFileName().toString()
087 *
088 * File file.getParentFile()
089 * Path path.getParent()
090 *
091 * boolean file.mkdir()
092 * boolean file.mkdirs()
093 * Path Files.createDirectory(path)
094 * Path Files.createDirectories(path)
095 *
096 * long file.length()
097 * long Files.size(path)
098 *
099 * boolean file.exists()
100 * boolean Files.exists(path)
101 *
102 * boolean file.delete()
103 * void Files.delete(path)
104 * boolean Files.deleteIfExists(path)
105 *
106 * FileOutputStream new FileOutputStream(file)
107 * OutputStream Files.newOutputStream(path)
108 *
109 * FileInputStream new FileInputStream(file)
110 * InputStream Files.newInputStream(path)
111 *
112 * file.listFiles(filter)
113 * Stream<Path> Files.list(path)
114 * List<Path> Files.list(path).filter(filter).collect(Collectors.toList())
115 */
116
117//import com.jcraft.jsch.Channel;
118//import com.jcraft.jsch.SftpStatVFS;
119//import java.awt.FlowLayout;
120//import java.awt.Shape;
121//import java.awt.event.FocusEvent;
122//import java.awt.event.FocusListener;
123//import java.awt.event.WindowListener;
124//import java.awt.geom.Rectangle2D;
125//import java.awt.im.InputContext;
126//import java.io.PipedInputStream;
127//import java.io.PipedOutputStream;
128//import java.io.Reader;
129//import java.io.StringBufferInputStream;
130//import java.io.StringReader;
131//import java.io.StringWriter;
132//import java.nio.file.FileVisitResult;
133//import java.nio.file.SimpleFileVisitor;
134//import java.nio.file.Watchable;
135//import java.nio.file.attribute.BasicFileAttributes;
136//import java.time.Instant;
137//import javax.swing.Action;
138//import javax.swing.BoxLayout;
139//import javax.swing.CellEditor;
140//import javax.swing.ComboBoxModel;
141//import javax.swing.DefaultListModel;
142//import javax.swing.JCheckBox;
143//import javax.swing.JEditorPane;
144//import javax.swing.JToggleButton;
145//import javax.swing.ListModel;
146//import javax.swing.ProgressMonitor;
147//import javax.swing.ScrollPaneConstants;
148//import javax.swing.SizeRequirements;
149//import javax.swing.WindowConstants;
150//import javax.swing.event.CellEditorListener;
151//import javax.swing.event.ChangeEvent;
152//import javax.swing.event.DocumentEvent;
153//import javax.swing.event.ListDataListener;
154//import javax.swing.plaf.UIResource;
155//import javax.swing.plaf.basic.BasicTransferable;
156//import javax.swing.plaf.metal.MetalIconFactory;
157//import javax.swing.table.DefaultTableModel;
158//import javax.swing.table.TableCellEditor;
159//import javax.swing.text.AbstractDocument;
160//import javax.swing.text.BoxView;
161//import javax.swing.text.Caret;
162//import javax.swing.text.CompositeView;
163//import javax.swing.text.DefaultEditorKit;
164//import javax.swing.text.EditorKit;
165//import javax.swing.text.Element;
166//import javax.swing.text.GlyphView;
167//import javax.swing.text.Position;
168//import javax.swing.text.StyledEditorKit;
169//import javax.swing.text.View;
170//import javax.swing.text.ViewFactory;
171//import javax.swing.text.WrappedPlainView;
172//import javax.swing.text.html.HTMLEditorKit.HTMLFactory;
173//import javax.swing.text.html.HTMLEditorKit;
174//import javax.swing.text.html.InlineView;
175//import javax.swing.text.html.ParagraphView;
176//import javax.swing.text.rtf.RTFEditorKit;
177//import javax.swing.tree.DefaultMutableTreeNode;
178//import javax.swing.tree.DefaultTreeCellEditor;
179//import javax.swing.tree.DefaultTreeCellRenderer;
180//import javax.swing.tree.DefaultTreeModel;
181//import javax.swing.tree.MutableTreeNode;
182//import javax.swing.tree.TreeCellEditor;
183//import javax.swing.tree.TreeNode;
184
185import com.jcraft.jsch.ChannelSftp;
186import com.jcraft.jsch.ChannelShell;
187import com.jcraft.jsch.JSch;
188import com.jcraft.jsch.JSchException;
189import com.jcraft.jsch.Session;
190import com.jcraft.jsch.SftpATTRS;
191import com.jcraft.jsch.SftpException;
192import com.jcraft.jsch.SftpProgressMonitor;
193import com.jcraft.jsch.UIKeyboardInteractive;
194import com.jcraft.jsch.UserInfo;
195import java.awt.BorderLayout;
196import java.awt.Color;
197import java.awt.Component;
198import java.awt.Container;
199import java.awt.Dimension;
200import java.awt.Font;
201import java.awt.Graphics;
202import java.awt.GridBagConstraints;
203import java.awt.GridBagLayout;
204import java.awt.GridLayout;
205import java.awt.Insets;
206import java.awt.Point;
207import java.awt.Rectangle;
208import java.awt.Toolkit;
209import java.awt.Window;
210import java.awt.datatransfer.Clipboard;
211import java.awt.datatransfer.DataFlavor;
212import java.awt.datatransfer.StringSelection;
213import java.awt.datatransfer.Transferable;
214import java.awt.datatransfer.UnsupportedFlavorException;
215import java.awt.event.ActionEvent;
216import java.awt.event.ActionListener;
217import java.awt.event.ComponentAdapter;
218import java.awt.event.ComponentEvent;
219import java.awt.event.HierarchyEvent;
220import java.awt.event.HierarchyListener;
221import java.awt.event.KeyAdapter;
222import java.awt.event.KeyEvent;
223import java.awt.event.KeyListener;
224import java.awt.event.MouseAdapter;
225import java.awt.event.MouseEvent;
226import java.awt.event.MouseListener;
227import java.awt.event.MouseMotionListener;
228import java.awt.event.WindowAdapter;
229import java.awt.event.WindowEvent;
230import java.awt.font.FontRenderContext;
231import java.awt.font.TextLayout;
232import java.awt.print.PrinterException;
233import java.io.File;
234import java.io.IOException;
235import java.io.InputStream;
236import java.io.OutputStream;
237import java.io.Serializable;
238import java.io.UnsupportedEncodingException;
239import java.lang.reflect.InvocationTargetException;
240import java.net.URI;
241import java.net.URISyntaxException;
242import java.nio.charset.Charset;
243import java.nio.charset.StandardCharsets;
244import java.nio.file.AccessDeniedException;
245import java.nio.file.DirectoryStream;
246import java.nio.file.FileAlreadyExistsException;
247import java.nio.file.FileSystem;
248import java.nio.file.FileSystems;
249import java.nio.file.Files;
250import java.nio.file.InvalidPathException;
251import java.nio.file.LinkOption;
252import java.nio.file.NoSuchFileException;
253import java.nio.file.Path;
254import java.nio.file.Paths;
255import java.nio.file.WatchEvent;
256import java.nio.file.WatchKey;
257import java.nio.file.WatchService;
258import java.nio.file.attribute.FileTime;
259import java.nio.file.attribute.PosixFilePermission;
260import java.text.MessageFormat;
261import java.util.ArrayDeque;
262import java.util.ArrayList;
263import java.util.Arrays;
264import java.util.Calendar;
265import java.util.Collections;
266import java.util.Comparator;
267import java.util.Date;
268import java.util.Enumeration;
269import java.util.HashMap;
270import java.util.Iterator;
271import java.util.List;
272import java.util.Map;
273import java.util.Set;
274import java.util.Stack;
275import java.util.TreeMap;
276import java.util.TreeSet;
277import javax.swing.AbstractAction;
278import javax.swing.ActionMap;
279import javax.swing.ComboBoxEditor;
280import javax.swing.DefaultComboBoxModel;
281import javax.swing.DropMode;
282import javax.swing.InputMap;
283import javax.swing.JButton;
284import javax.swing.JComboBox;
285import javax.swing.JComponent;
286import javax.swing.JFrame;
287import javax.swing.JLabel;
288import javax.swing.JList;
289import javax.swing.JMenu;
290import javax.swing.JMenuBar;
291import javax.swing.JOptionPane;
292import javax.swing.JPanel;
293import javax.swing.JPasswordField;
294import javax.swing.JScrollPane;
295import javax.swing.JSplitPane;
296import javax.swing.JTable;
297import javax.swing.JTextArea;
298import javax.swing.JTextField;
299import javax.swing.JTextPane;
300import javax.swing.JTree;
301import javax.swing.KeyStroke;
302import javax.swing.ListCellRenderer;
303import javax.swing.ListSelectionModel;
304import javax.swing.Spring;
305import javax.swing.SpringLayout;
306import javax.swing.SwingConstants;
307import javax.swing.SwingUtilities;
308import javax.swing.Timer;
309import javax.swing.TransferHandler;
310import javax.swing.border.EmptyBorder;
311import javax.swing.event.CaretEvent;
312import javax.swing.event.CaretListener;
313import javax.swing.event.EventListenerList;
314import javax.swing.event.PopupMenuEvent;
315import javax.swing.event.PopupMenuListener;
316import javax.swing.event.TableModelEvent;
317import javax.swing.event.TreeExpansionEvent;
318import javax.swing.event.TreeModelEvent;
319import javax.swing.event.TreeModelListener;
320import javax.swing.event.TreeSelectionEvent;
321import javax.swing.event.TreeSelectionListener;
322import javax.swing.event.TreeWillExpandListener;
323import javax.swing.event.UndoableEditEvent;
324import javax.swing.event.UndoableEditListener;
325import javax.swing.table.AbstractTableModel;
326import javax.swing.table.DefaultTableCellRenderer;
327import javax.swing.table.TableColumn;
328import javax.swing.text.BadLocationException;
329import javax.swing.text.DefaultCaret;
330import javax.swing.text.Document;
331import javax.swing.text.JTextComponent;
332import javax.swing.text.Style;
333import javax.swing.text.StyleConstants;
334import javax.swing.text.StyledDocument;
335import javax.swing.tree.TreeCellRenderer;
336import javax.swing.tree.TreeModel;
337import javax.swing.tree.TreePath;
338import javax.swing.tree.TreeSelectionModel;
339import javax.swing.undo.CannotUndoException;
340import javax.swing.undo.UndoManager;
341import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
342import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
343import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
344import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
345
346/*
347 * ////////////////////////////////////////////////
348 * Sessions and Channels are not daemon threads and must be closed for
349 * the application to Shut down.
350 *
351 * Closing a session closes all channels associated with it.
352 * //////////////////////////////////////////////////
353 * 0 OVERWRITE
354 * 1 RESUME
355 * 2 APPEND
356 *
357 * 0 SSH_FX_OK
358 * 1 SSH_FX_EOF
359 * 2 SSH_FX_NO_SUCH_FILE
360 * 3 SSH_FX_PERMISSION_DENIED
361 * 4 SSH_FX_FAILURE
362 * 5 SSH_FX_BAD_MESSAGE
363 * 6 SSH_FX_NO_CONNECTION
364 * 7 SSH_FX_CONNECTION_LOST
365 * 8 SSH_FX_OP_UNSUPPORTED
366 */
367
368class FileBrowser {
369  final static long serialVersionUID = 42;
370
371  final static Color BLACK = Color.BLACK;
372  final static Color LIGHT_GRAY = Color.LIGHT_GRAY;
373  final static Color WHITE = Color.WHITE;
374  final static Color BROWN = new Color(0xAA4400);
375  final static Color RED = Color.RED;
376  final static Color PINK = Color.PINK;
377  final static Color ORANGE = new Color(0xFF8000); // not currently used
378  final static Color YELLOW = Color.YELLOW;
379  final static Color LIGHT_YELLOW = new Color(0xffffc0);
380  final static Color GREEN = Color.GREEN;
381  final static Color CYAN = Color.CYAN;
382  final static Color BLUE = Color.BLUE;
383  final static Color MAGENTA = Color.MAGENTA;
384
385  static boolean windows = File.pathSeparatorChar == ';';
386
387  static int windowCount = 0; // number of open windows
388  static JSch jsch = new JSch();
389
390  static Charset charSet = StandardCharsets.UTF_8;
391  static FileSystem fileSystem = FileSystems.getDefault();
392  static Toolkit toolkit = Toolkit.getDefaultToolkit();
393  // not toolkit.getSystemClipboard()
394  static Clipboard clipboard = toolkit.getSystemSelection();
395  static String home = System.getProperty("user.home");
396  static Path optionPath = fileSystem.getPath(home, ".filebrowserrc");
397  static TreeMap<String, ArrayList<String>> options
398    = new TreeMap<String, ArrayList<String>>();
399  static final String passwordKey = "Passwords";
400  static final String selectionsKey = "Selections";
401
402  static Charset fileCharset = Charset.availableCharsets().get("UTF-8");
403
404  /**
405   * Removes all % escape sequences from a String. No check is made
406   * that the % escape sequences are valid.
407   *
408   * @param uri the String to be flattened
409   * @return the flattened String
410   */
411  static String flatten(String uri) {
412    StringBuilder sb = new StringBuilder();
413    for(int i = 0; i < uri.length(); ++i) {
414      char c = uri.charAt(i);
415      if(c == '%') {
416        char c1 = uri.charAt(i + 1);
417        int i1 = legalHex.indexOf(c1);
418        if(i1 < 0) throw new Error("bad hex digit #1: " + c1);
419        if(i1 >= 16) i1 -= 6;
420        char c2 = uri.charAt(i + 2);
421        int i2 = legalHex.indexOf(c2);
422        if(i2 < 0) throw new Error("bad hex digit #2: " + c2);
423        if(i2 >= 16) i2 -= 6;
424        c = (char)(16*i1 + i2);
425        i += 2; // skip over hex value following %
426      }
427      sb.append(c);
428    }
429    return sb.toString();
430  } // static String flatten(String uri)
431
432  /**
433   * Convert a byte[] to an escaped String
434   *
435   * List of non-escaped characters determined empirically by
436   * converting to Path and back to URI.
437   *
438   * @param bytes array of bytes to convert
439   * @return the escaped String
440   */
441  static String bytesToString(byte[] bytes) { ////////////////////////
442    StringBuilder sb = new StringBuilder();
443    for(byte b : bytes) {
444      char c = (char)b;
445      if(c >= '0' && c <= '9') sb.append(c);
446      else if(c >= 'A' && c <= 'Z') sb.append(c);
447      else if(c >= 'a' && c <= 'z') sb.append(c);
448      else if("!$&'()*+,-./:;=@_~".indexOf(c) >= 0) sb.append(c);
449      else {
450        sb.append('%');
451        sb.append(hex[(b >> 4) & 0xF]);
452        sb.append(hex[b & 0xF]);
453      }
454    }
455    return sb.toString();
456  } // static String bytesToString(byte[] bytes)
457
458  /**
459   * Convert a String to byte[]
460   *
461   * @param s the String to convert
462   * @return the path with escapes converted from hex
463   */
464  static byte[] stringToBytes(String s) {
465    int count = 0;
466    for(int i = 0; i < s.length(); ++i) {
467      if(s.charAt(i) == '%') ++count;
468    }
469    byte[] bytes = new byte[s.length() - 2*count];
470    for(int i = 0, j = 0; i < s.length(); ++i, ++j) {
471      char c = s.charAt(i);
472      if(c == '%') {
473        bytes[j] = (byte)Integer.parseInt(s.substring(i + 1, i + 3), 16);
474        //System.out.println((int)bytes[j]);
475        i += 2;
476      }
477      //////////////////// removing this causes fail
478      else {
479        if(c <= ' ' || c > '~') {
480          System.err.println("Bad character in URI: " + c);
481          new Error("Bad character in URI: " + c).printStackTrace();
482        }
483        bytes[j] = (byte)c; /////// need something like this
484      }
485      ///////////////////
486    }
487    return bytes;
488  }
489
490  final static char[] hex = new char[]{'0','1','2','3','4','5','6','7',
491                                       '8','9','A','B','C','D','E','F'};
492  final static String legalHex = "0123456789ABCDEFabcdef";
493
494  /**
495   * Convert an "escaped" string to a URI. Characters (other than a
496   * %xx where xx is a legitimate hex string) are escaped with the
497   * appropriate %xx escape sequence. % is assumed to introduce a hex
498   * escape and is left untouched if well-formed. Otherwise the % is
499   * escaped and processing continues.
500   *
501   * /////////////// maybe use bytes to convert UTF-8 characters ??????????
502   * /////////////// need to convert &gt; 128 chars properly ????????
503   *
504   * @param s the string to be converted
505   * @return the equivalent URI
506   */
507  static URI toUri(String s) {
508    StringBuilder sb = new StringBuilder();
509    for(int i = 0; i < s.length(); ++i) {
510      char c = s.charAt(i);
511      percent: if(c == '%') {
512        if(i + 2 >= s.length()) break percent;
513        if(legalHex.indexOf(s.charAt(i + 1)) < 0) break percent;
514        if(legalHex.indexOf(s.charAt(i + 2)) < 0) break percent;
515        ////////////// maybe convert to char and let the rest take care of it
516        ////////////// what about UTF-8 extended char codes????????
517        sb.append(s.substring(i, i + 3).toUpperCase());
518        i += 2;
519        continue;
520      }
521      if(c >= '0' && c <= '9') sb.append(c);
522      else if(c >= 'A' && c <= 'Z') sb.append(c);
523      else if(c >= 'a' && c <= 'z') sb.append(c);
524      // String generated by converting characters to Path and then to URI
525      else if("!$&'()*+,-./:;=@_~".indexOf(c) >= 0) sb.append(c);
526      else {
527        sb.append('%');
528        sb.append(hex[(c >> 4) & 0xF]);
529        sb.append(hex[c & 0xF]);
530      }
531    }
532
533    try {
534      //////////////////// need to escape string
535      return new URI(sb.toString());
536    } catch(URISyntaxException e) {
537      e.printStackTrace();
538      return root.uri;
539    }
540  } // static URI toUri(String s)
541
542  static String rootName = "file system root(s)";
543  static RootPath root = new RootPath();
544
545  static ArrayList<RemotePath> remoteRoots = new ArrayList<RemotePath>();
546
547  // map from user@host to Session for all sessions
548  static TreeMap<String, Session> sessions = new TreeMap<String, Session>();
549
550  // map from user@host to Stack<ChannelSftp>
551  static TreeMap<String, Stack<ChannelSftp>> channels
552    = new TreeMap<String, Stack<ChannelSftp>>();
553
554  static DataFlavor remotePathArrayFlavor
555    = new DataFlavor(ArrayList.class, "remotePathListFlavor");
556
557  //Set the location of the known hosts file
558  static {
559    try {
560      jsch.setKnownHosts("~/.ssh/known_hosts");
561    } catch(JSchException e) {
562      e.printStackTrace();
563    }
564  }
565
566  /**
567   * Open a session on the given user@host. If a session is cached for
568   * this user@host, it is returned. Otherwise, if the password is
569   * cached then it is used for an attempted open. If successful, the
570   * opened session is cached and returned. Otherwise, a dialog is
571   * opened to get the password for the open. If successful then the
572   * opened session is cached and returned. If the open fails a
573   * SftpOpenFailedException is thrown.
574   *
575   * This method is synchronized on the interned user@host.
576   *
577   * @param userHost the user@host string specifying where to open the session
578   * @return the opened session
579   * @throws SftpOpenFailedException if the open fails
580   */
581  static Session getSession(String userHost) throws SftpOpenFailedException {
582    userHost = userHost.intern(); // to make unique for following synchronized
583    synchronized(userHost) {
584      try {
585        Session session = sessions.get(userHost);
586        if(session != null && session.isConnected()) {
587          return session;
588        }
589        int index = userHost.indexOf('@');
590        String user = userHost.substring(0, index);
591        String host = userHost.substring(index + 1);
592        int port = 22;
593        session = jsch.getSession(user, host, port);
594        // hash user/host in ~/.ssh/known_hosts file
595        session.setConfig("HashKnownHosts",  "yes");
596        ArrayList<String> passwords = options.get(passwordKey);
597        if(passwords != null) {
598          for(String entry : passwords) {
599            int colon = entry.indexOf(":");
600            String userHost1 = entry.substring(0, colon);
601            if(colon >= 0 && userHost1.equals(userHost)) {
602              session.setPassword(decrypt(userHost, entry.substring(colon+1)));
603              break;
604            }
605          }
606        }
607        MyUserInfo myUserInfo = new MyUserInfo();
608        session.setUserInfo(myUserInfo);
609        session.connect(20*1000);
610        sessions.put(userHost, session);
611        String password = myUserInfo.getPassword();
612        if(password != null) {
613          passwords = options.get(passwordKey);
614          if(passwords == null) passwords = new ArrayList<String>();
615          for(String entry : passwords) {
616            int colon = entry.indexOf(":");
617            String userHost1 = entry.substring(0, colon);
618            if(colon < 0 || userHost1.equals(userHost)) {
619              passwords.remove(entry); // remove old entry(s) if exist
620            }
621          }
622          String newItem = "//" + userHost + "/";
623          String newEntry = userHost + ':' + encrypt(userHost, password);
624          if(passwords.indexOf(newEntry) == -1) {
625            passwords.add(newEntry);
626            options.put(passwordKey, passwords);
627          }
628        }
629        remoteRoots.add(new RemotePath(toUri("//" + user + '@' + host + "/")));
630        return session;
631      } catch(JSchException e) {
632        System.err.println(e);
633        System.err.println("Opening Session failed: " + userHost);
634        //e.printStackTrace();
635        throw new SftpOpenFailedException("Open failed");
636        //return null;
637      }
638    }
639  } // static Session getSession(String userHost)
640
641  /**
642   * Get a sftp channel on the given user@host. First, a session for
643   * user@host is obtained. If there is a cached unused sftp channel
644   * for the given user@host it is returned. Otherwise, a new sftp
645   * channel is opened on the given user@host.
646   *
647   * Note: all channels must be returned for possible reuse by calling
648   * releaseChannel()
649   *
650   * This method is synchronized on channels
651   *
652   * @param userHost the user@host to open the sftp channel
653   * @return an opened sftp channel
654   * @throws SftpOpenFailedException if the open fails
655   */
656  static ChannelSftp getSftpChannel(String userHost)
657    throws SftpOpenFailedException {
658    synchronized(channels) {
659      try {
660        Stack<ChannelSftp> channelStack = channels.get(userHost);
661        if(channelStack == null) {
662          channelStack = new Stack<ChannelSftp>();
663          channels.put(userHost, channelStack);
664        }
665        getChannel: if(channelStack.size() != 0) {
666          ChannelSftp channel = channelStack.pop();
667          if(!channel.getSession().isConnected()) {
668            channelStack.clear();
669            break getChannel;
670          }
671          return channel;
672        }
673        Session session = getSession(userHost);
674        if(session == null) {
675          new Error().printStackTrace();
676        }
677        ChannelSftp channel = (ChannelSftp)session.openChannel("sftp");
678        channel.connect(10*1000);
679        return channel;
680      } catch(JSchException e) {
681        System.err.println(e);
682        System.err.println("Opening Session failed: " + userHost);
683        //e.printStackTrace();
684        throw new SftpOpenFailedException();
685      }
686    }
687  } // static ChannelSftp getSftpChannel(String userHost)
688
689  /**
690   * When a client is done using a channel it must be released by
691   * calling this method so the channel can be used by someone
692   * else. This should be done in a "finally" clause.
693   *
694   * This method is synchronized on channels.
695   *
696   * @param channel the channel to be released for future use
697   */
698  static void releaseChannel(ChannelSftp channel) {
699    if(channel == null) {
700      System.err.println("releasing null channel");
701      new Error("releasing null channel").printStackTrace();
702      return;
703    }
704    String userHost = "none";
705    synchronized(channels) {
706      try {
707        Session session = channel.getSession();
708        String user = session.getUserName();
709        String host = session.getHost();
710        userHost = user + '@' + host;
711        Stack<ChannelSftp> channelStack = channels.get(userHost);
712        channelStack.push(channel); // for possible future use
713      } catch(JSchException e) {
714        System.err.println(e);
715        System.err.println("Returning Channel failed: " + userHost);
716        e.printStackTrace();
717      }
718    }
719  } // static void releaseChannel(ChannelSftp channel)
720
721  static class SftpOpenFailedException extends Exception {
722    final static long serialVersionUID = 42;
723    SftpOpenFailedException() {}
724    SftpOpenFailedException(String message) {
725      super(message);
726    }
727  } // static class SftpOpenFailedException extends Exception
728
729  /**
730   * Normalize file specifier
731   *   replace /./ with /
732   *   remove final /.
733   *   replace /x/../ with /
734   *   remove final x/..
735   *   user@host/~ to user@host/[home-directory]
736   *
737   * Canonicalizes a path string:
738   *   replace // with /
739   *   remove "." element from path
740   *   remove single trailing "/" not part of name@server
741   *
742   * Note that //x@y/. -&gt; //x@y
743   *
744   * @param s a String representing a path in a file system
745   * @return a String representing the same target with redundancies removed
746   */
747
748  /**
749   * Creates an appropriate subclass instance of MyPath from a
750   * string. LocalPaths start with a single "/" and RemotePaths start
751   * with "//" with the format "//user@host/".
752   *
753   * @param s String to convert
754   * @return an instance of an approprate subclass of MyPath
755   */
756  static MyPath stringToMyPath(String s) { //////////// stringToUri
757    x: {
758      if(s.equals("")) break x;
759      if(s.equals(".")) {
760        s = "";
761        break x;
762      }
763      if(s.equals("/")) break x;
764      if(s.indexOf(':') == 1) {
765        s = Character.toUpperCase(s.charAt(0)) + s.substring(1);
766      }
767      if(s.indexOf(':') == 2 && s.charAt(0) == '/') {
768        s = "" + s.charAt(0) + Character.toUpperCase(s.charAt(1))
769          + s.substring(2);
770      }
771      int index = 0;
772      while((index = s.indexOf("//", 2)) > 0) {
773        s = s.substring(0, index) + s.substring(index + 1);
774      }
775      int length = s.length();
776      if(s.charAt(length - 1) == '/') {
777        if(!s.substring(0, 2).equals("//")) break x;
778        else if(s.indexOf('/', 2) < length - 1) s = s.substring(0, length - 1);
779      }
780
781    }
782    if(s.length() == 0) {
783      return root;
784    }
785    if(s.length() > 1 && s.substring(0,2).equals("//")) {
786      int at = s.indexOf('@');
787      int start = s.indexOf('/', 2);
788      if(start < 0) {
789        start = s.length();
790      }
791      if(at < 0 || at > start)
792        return new RemotePath(toUri("//unknown@unknown" + s.substring(2)));
793      String user = s.substring(2, at);
794      String host = s.substring(at + 1, start);
795      String fileName = s.substring(start);
796      ChannelSftp channel = null;
797      if(fileName.length() == 0) {
798        try {
799          channel = getSftpChannel(user + '@' + host);
800          fileName = channel.getHome(); ////// getHomeUri() ???
801        } catch(SftpOpenFailedException e) {
802          System.err.println("Can't find home: " + user + '@' + host);
803        } catch(SftpException e) {
804          System.err.println("Error finding home: " + user + '@' + host);
805        } finally {
806          releaseChannel(channel);
807        }
808      }
809      return new RemotePath(toUri("//" + user + '@' + host + fileName));
810    } else {
811      //////////////// make relative to home
812      if(s.charAt(0) != '/') s = '/' + s; ///difference between windows and unix
813      return new LocalPath(toUri("file://" + s)); ///////// toAbsolutePath
814    }
815  } // static MyPath stringToMyPath(String s)
816
817  /**
818   * Returns the current root Paths. The current root Paths are the
819   * file system root paths plus the roots of all open sftp
820   * channels. Paths are sorted ignoring case with directories
821   * before files.
822   *
823   * @param dotFiles true if dot files are to be included
824   * @return an ArrayList&lt;Path&gt; created from all of the roots
825   */
826  static TreeSet<TableData> getRootPaths(boolean dotFiles) {
827    TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
828    Iterable<Path> iter = fileSystem.getRootDirectories();
829    for(Path p : iter) {
830      Path pp = p.getFileName();
831      if(pp == null) pp = p.getRoot(); /////////// ????????
832      else System.out.println("not null");
833      String name = pp.toString();
834      if(dotFiles || !(name.length() > 0 && name.charAt(0) == '.')) {
835        list.add(new TableData(new LocalPath(p.toUri())));
836      }
837    }
838    // add remote roots
839    for(RemotePath path : remoteRoots) {
840      list.add(new TableData(path));
841    }
842    return list;
843  } // static TreeSet<TableData> getRootPaths(boolean dotFiles)
844
845  /**
846   * Takes a String that is presumed to be an integer and inserts
847   * commas every third digit.
848   *
849   * @param s String containing an integer
850   * @return a String with commas inserted every third digit
851   */
852  static String addCommas(String s) {
853    StringBuilder sb = new StringBuilder();
854    for(int i = 0; i < s.length(); ++i) {
855      if(i != 0 && (s.length() - i) % 3 == 0) sb.append(',');
856      sb.append(s.charAt(i));
857    }
858    return sb.toString();
859  } // static String addCommas(String s)
860
861  static PosixFilePermission[] permissions = {
862    PosixFilePermission.OTHERS_EXECUTE,
863    PosixFilePermission.OTHERS_WRITE,
864    PosixFilePermission.OTHERS_READ,
865    PosixFilePermission.GROUP_EXECUTE,
866    PosixFilePermission.GROUP_WRITE,
867    PosixFilePermission.GROUP_READ,
868    PosixFilePermission.OWNER_EXECUTE,
869    PosixFilePermission.OWNER_WRITE,
870    PosixFilePermission.OWNER_READ
871  };
872
873  /**
874   * Get an integer file permissions from a set of POSIX file permissions.
875   *
876   * @param perms set of POSIX file permissions
877   * @return integer file permissions
878   */
879  static int octalFilePermissionsx(Set<PosixFilePermission> perms) {
880    int octal = 0;
881    for(PosixFilePermission perm : perms) {
882      octal |= 1 << (8 - perm.ordinal());
883    }
884    return octal;
885  } // static int octalFilePermissions(Set<PosixFilePermission> perms)
886
887  /**
888   * Get a set of POSIX file permissions from an octal file permissions.
889   *
890   * @param octal integer file permissions
891   * @return set of POSIX file permissions
892   */
893  static Set<PosixFilePermission> javaFilePermissionsx(int octal) {
894    Set<PosixFilePermission> set = new TreeSet<PosixFilePermission>();
895    for(int i = 0; i < 9; ++i) {
896      if((octal & (1 << i)) != 0) {
897        System.out.println(i + " " + permissions[i]);
898        set.add(permissions[i]);
899      }
900    }
901    return set;
902  } // static Set<PosixFilePermission> javaFilePermissions(int octal)
903
904  // backwards
905  // not used ////////////////
906  static PosixFilePermission[] stuff = PosixFilePermission.values();
907
908  /**
909   * Gets the home directory using the authority of a URI. If the
910   * authority is null, gets the home directory on the local file
911   * system.
912   *
913   * @param authority the authority of a URI
914   * @return the MyPath of the requested home directory.
915   */
916  static MyPath getHomeDirectoryx(String authority) {
917    if(authority == null) {
918      return new LocalPath(fileSystem.getPath(home).toUri());
919    } else {
920      ChannelSftp channel = null;
921      try {
922        channel = getSftpChannel(authority);
923        return new RemotePath(channel.getHomeUri());
924      } catch(SftpException | SftpOpenFailedException e) {
925        return root;
926      } finally {
927        releaseChannel(channel);
928      }
929    }
930  } // static MyPath getHomeDirectory(String authority)
931
932  /**
933   * MyProgMon is an implementation of a file transfer progress
934   * monitor. It implements a progress window showing file transfer
935   * progress to or from remote files. The interface first calls init
936   * when a transfer starts, then calls count for each block of data
937   * transferred, and finally calls end after the last block of data
938   * is transferred.
939   *
940   * Calls to setVisible(true) also grab focus so calls to repaint()
941   * are used to prevent this.
942   */
943  static class MyProgMon extends JFrame implements SftpProgressMonitor {
944    final static long serialVersionUID = 42;
945
946    long count=0;
947    long max=0;
948    long percent = -1;
949    String title = null;
950    JLabel label = null;
951    Timer timer = null;
952
953    MyProgMon() {
954      label = new JLabel("                              ");
955      setTitle("File Transfer Progress Monitor");
956      pack();
957      addWindowListener(new WindowAdapter() {
958          @Override
959          public void windowClosing(WindowEvent e) {
960            setVisible(false);
961            dispose();
962          }
963        });
964    }
965
966    /**
967     * Initialize a MyProgMonitor to start monitoring progress of a
968     * file transfer.
969     *
970     * @param op I think this is PUT=0, GET=1 - not used by me
971     * @param src source name of the file transfer
972     * @param dest destination name of the file transfer
973     * @param max length of the file transfer
974     */
975    @Override
976    public void init(int op, String src, String dest, long max) {
977      title = src + " -> " + dest;
978      this.max=max;
979      count=0;
980      percent = -1;
981      SwingUtilities.invokeLater(new Runnable() {
982          @Override
983          public void run() {
984            add(label);
985            pack();
986            if(isVisible()) {
987              repaint();
988            } else {
989              setVisible(true);
990            }
991            if(timer != null) timer.stop();
992          }
993        });
994    }
995
996    /**
997     * Size of the current block of the transfer. The counts must be
998     * summed to get the number of bytes currently transferred.
999     *
1000     * @param count number of bytes transferred in current block
1001     * @return true to continue, false leaves a mess
1002     */
1003    @Override
1004    public boolean count(long count) {
1005      this.count += count;
1006      long newPercent = max == 0 ? count : this.count*100/max;
1007      if(percent != newPercent) {
1008        percent = newPercent;
1009        SwingUtilities.invokeLater(new Runnable() {
1010            @Override
1011            public void run() {
1012              label.setText(percent + "% " + title);
1013              pack();
1014              repaint();
1015            }
1016          });
1017      }
1018      return true;
1019    }
1020
1021    /**
1022     * Called when the transfer is completed
1023     */
1024    @Override
1025    public void end() {
1026      SwingUtilities.invokeLater(new Runnable() {
1027          @Override
1028          public void run() {
1029            if(timer == null) {
1030              timer = new Timer(5000, new ActionListener() {
1031                  public void actionPerformed(ActionEvent e) {
1032                    myProgMon.dispose();
1033                  }
1034                });
1035              timer.setRepeats(false);
1036              timer.start();
1037            } else {
1038              timer.restart();
1039            }
1040          }});
1041    }
1042  }
1043
1044  static MyProgMon myProgMon = new MyProgMon();
1045
1046  /**
1047   * This class is the superclass of all path-type objects. The
1048   * current implementing subclasses are LocalPath and RemotePath. All
1049   * subclass constructors and serialized reads must set the parent
1050   * field.
1051   */
1052  static abstract class MyPath implements Serializable {
1053    final static long serialVersionUID = 42;
1054
1055    URI uri;
1056
1057    // just child directories
1058    protected transient ArrayList<MyPath> treeChildren = null; // cached
1059
1060    /**
1061     * Returns the appropriate transfer DataFlavor for this MyPath.
1062     *
1063     * @return DataFlavor for this MyPath
1064     */
1065    abstract DataFlavor getFlavor();
1066
1067    /**
1068     * Checks if the path refers to an existing file or directory.
1069     *
1070     * @return true if the path refers to an existing file or directory
1071     */
1072    abstract boolean exists(); // use stat comand
1073
1074    /**
1075     * Checks if the path refers to a directory.
1076     *
1077     * @return true if path refers to a directory
1078     */
1079    abstract boolean isDirectory(); // use stat command and isDir()
1080
1081    /**
1082     * Checks if the path refers to an ordinary file.
1083     *
1084     * @return true if the path refers to an ordinary file
1085     */
1086    abstract boolean isFile(); // use stat command and isFile()
1087
1088    /**
1089     * Checks if the path refers to a link.
1090     *
1091     * @return true if the path refers to a link
1092     */
1093    abstract boolean isLink(); // use stat command and isLink()
1094
1095    /**
1096     * returns a path to the parent of this path or null if there is
1097     * no parent.
1098     *
1099     * @return parent path
1100     */
1101    abstract MyPath getParent();
1102
1103    /**
1104     * Gets the exposed directory children of this path. Only gets
1105     * directories.
1106     *
1107     * @return exposed children of this path
1108     */
1109    abstract ArrayList<MyPath> getTreeChildren(); // ls(path)
1110
1111    /**
1112     * Enumerates all children (files and directories) of this path.
1113     *
1114     * @param dotFiles true if dot files are to be included
1115     * @return a list of all children of this path
1116     */
1117    abstract TreeSet<TableData> getChildren(boolean dotFiles);
1118
1119    /**
1120     * Gets the index of child in children or -1 if not found.
1121     *
1122     * @param child the child to search for
1123     * @return the index of the child in children
1124     */
1125    int getIndex(MyPath child) {
1126      if(treeChildren == null) return -1;
1127      for(int i = 0; i < treeChildren.size(); ++i) {
1128        if(treeChildren.get(i).equals(child)) return i;
1129      }
1130      return -1;
1131    }
1132
1133    /**
1134     * Appends other to the current path and returns it. "other" is a
1135     * simple file name.
1136     *
1137     * @param other the name to append to this path
1138     * @return the augmented path
1139     */
1140    abstract MyPath resolve(String other);
1141
1142    /**
1143     * Appends other to the current path and returns it. "other" is a
1144     * simple file name.
1145     *
1146     * @param other the name to append to this path
1147     * @return the augmented path
1148     */
1149    abstract MyPath resolve(Path other);
1150
1151    /**
1152     * Returns the length of the file in bytes refered to by this
1153     * path.
1154     *
1155     * @return the length of the file in bytes
1156     */
1157    abstract long size();
1158
1159    /**
1160     * Returns the modification date of this file as the number of
1161     * milliseconds after 00:00:00 GMT, January 1, 1970.
1162     *
1163     * @return number of milliseconds after 00:00:00 GMT, January 1, 1970
1164     */
1165    abstract long getMTime();
1166
1167    /**
1168     * Sets the modification date of this file as the number of
1169     * milliseconds after 00:00:00 GMT, January 1, 1970.
1170     *
1171     * @param time Number of milliseconds after 00:00:00 GMT, January 1, 1970
1172     * @return true if success
1173     */
1174    abstract boolean setMTime(long time);
1175
1176    /**
1177     * creates an empty directory at the location specified by this.
1178     *
1179     * @return true if successful
1180     */
1181    abstract boolean makeDirectory();
1182
1183    /**
1184     * Renames this file or directory, keeping it in the same parent
1185     * directory.
1186     *
1187     * @param newName the new name of the file or directory
1188     * @return true is successful
1189     */
1190    abstract boolean renameFile(String newName);
1191
1192    /**
1193     * Will fill data with read information.
1194     *
1195     * @param data buffer to read data into
1196     */
1197    abstract void readFile(byte[] data);
1198
1199    /**
1200     * Will write data to file starting at offset.
1201     *
1202     * @param data buffer to write data from
1203     */
1204    abstract void writeFile(byte[] data);
1205
1206    /**
1207     * Reads lines from this file and returns them as a List&lt;String&gt;
1208     *
1209     * @return the lines from the file
1210     */
1211    abstract List<String> readAllLines();
1212
1213    /**
1214     * Reads the link value (not it's target)
1215     *
1216     * @return the value of the link
1217     */
1218    abstract byte[] readLink();
1219
1220    /**
1221     * Creates a symbolic link from this to target.
1222     *
1223     * @param target the target of the link
1224     * @return the value of the link
1225     */
1226    abstract boolean makeLinkTo(byte[] target);
1227
1228    /**
1229     * Will touch file or directory.
1230     */
1231    abstract void touch();
1232
1233    /**
1234     * Moves a file or directory from one directory to
1235     * another.
1236     *
1237     * @param file the file to be moved to this
1238     */
1239    abstract void moveFileFrom(MyPath file);
1240
1241    /**
1242     * Move this to other.
1243     *
1244     * @param other directory to move this to
1245     */
1246    abstract void moveFileTo(LocalPath other);
1247
1248    /**
1249     *  Move this to other.
1250     *
1251     * @param other directory to move this to
1252     */
1253    abstract void moveFileTo(RemotePath other);
1254
1255    /**
1256     * Copies a file to this.
1257     *
1258     * @param file the file to be copied
1259     * @return true is successful
1260     */
1261    abstract boolean copyFileFrom(MyPath file);
1262
1263    /**
1264     *  Copy this to toFile.
1265     *
1266     * @param toFile LocalPath to copy this to
1267     * @return true is successful
1268     */
1269    abstract boolean copyFileTo(LocalPath toFile);
1270
1271    /**
1272     *  Copy this to toFile.
1273     *
1274     * @param toFile RemotePath to copy this to
1275     * @return true is successful
1276     */
1277    abstract boolean copyFileTo(RemotePath toFile);
1278
1279    /**
1280     * Deletes a file or a symbolic link
1281     *
1282     * @return true is successful
1283     */
1284    abstract boolean delete();
1285
1286    /**
1287     * Returns the base name of this path
1288     *
1289     * @return the base name of this path
1290     */
1291    @Override
1292    public abstract String toString();
1293
1294    /**
1295     * Returns the full path of this path
1296     *
1297     * @return full path name
1298     */
1299    abstract String fullName();
1300
1301    @Override
1302    abstract public boolean equals(Object other); // needed for JTree
1303
1304    @Override
1305    abstract public int hashCode(); // needed for JTree
1306  } // static abstract class MyPath implements Serializable
1307
1308  /**
1309   * A singleton class representing the root node of the tree.
1310   */
1311  static class RootPath extends MyPath {
1312    final static long serialVersionUID = 42;
1313
1314    @Override
1315    DataFlavor getFlavor() {
1316      throw new Error("root getFlavor");
1317    };
1318
1319    @Override
1320    boolean exists() {
1321      return true;
1322      //return Files.exists(path);
1323    }
1324
1325    @Override
1326    boolean isDirectory() {
1327      return true;
1328    }
1329
1330    @Override
1331    boolean isFile() {
1332      return false;
1333    }
1334
1335    @Override
1336    boolean isLink() {
1337      return false;
1338    }
1339
1340    @Override
1341    LocalPath getParent() {
1342      return null;
1343    }
1344
1345    @Override
1346    TreeSet<TableData> getChildren(boolean dotFiles) {
1347      return getRootPaths(dotFiles); //////????????????????
1348    }
1349
1350    /**
1351     * Only gets directories
1352     */
1353    @Override
1354    ArrayList<MyPath> getTreeChildren() {
1355      return treeChildren;
1356    }
1357
1358    @Override
1359    LocalPath resolve(String other) {
1360      throw new Error("root resolve");
1361    }
1362
1363    @Override
1364    LocalPath resolve(Path other) {
1365      throw new Error("root resolve");
1366    }
1367
1368    @Override
1369    long size() {
1370      return 0;
1371    }
1372
1373    @Override
1374    long getMTime() {
1375      return 0;
1376    }
1377
1378    @Override
1379    boolean setMTime(long time) {
1380      return true;
1381    }
1382
1383    @Override
1384    boolean makeDirectory() {
1385      return true;
1386    }
1387
1388    @Override
1389    boolean renameFile(String newName) {
1390      return false;
1391    }
1392
1393    @Override
1394    void readFile(byte[] data) {
1395      throw new Error("root readFile");
1396    }
1397
1398    @Override
1399    void writeFile(byte[] data) {
1400      throw new Error("root writeFile");
1401    }
1402
1403    @Override
1404    List<String> readAllLines() {
1405      throw new Error("root readAllLines");
1406    }
1407
1408    @Override
1409    byte[] readLink() {
1410      throw new Error("root readLink");
1411    }
1412
1413    @Override
1414    boolean makeLinkTo(byte[] target) {
1415      throw new Error("root makeLinkTo");
1416    }
1417
1418    @Override
1419    void touch() {
1420      throw new Error("root touch");
1421    }
1422
1423    @Override
1424    void moveFileFrom(MyPath file) {
1425      throw new Error("root moveFileFrom");
1426    }
1427
1428    @Override
1429    // other is a directory
1430    void moveFileTo(LocalPath other) {
1431      throw new Error("root moveFileTo");
1432    }
1433
1434    @Override
1435    // other is a directory
1436    void moveFileTo(RemotePath other) {
1437      throw new Error("root moveFileTo");
1438    }
1439
1440    @Override
1441    boolean copyFileFrom(MyPath file) {
1442      throw new Error("root copyFileFrom");
1443    }
1444
1445    @Override
1446    boolean copyFileTo(LocalPath toFile) {
1447      throw new Error("root copyFileTo");
1448    }
1449
1450    @Override
1451    boolean copyFileTo(RemotePath toFile) {
1452      throw new Error("root copyFileTo");
1453    }
1454
1455    @Override
1456    boolean delete() {
1457      throw new Error("root delete");
1458    }
1459
1460    @Override
1461    public String toString() {
1462      return rootName;
1463    }
1464
1465    @Override
1466    String fullName() {
1467      return rootName;
1468    }
1469
1470    @Override
1471    public boolean equals(Object other) {
1472      if(other instanceof RootPath) return true;
1473      return false;
1474    }
1475
1476    @Override
1477    public int hashCode() {
1478      return 7176405;
1479    }
1480  }
1481
1482  /**
1483   * Implements a path (MyPath) in the local file system. Illegal
1484   * characters for a linux file name are: '/' and null. Illegal
1485   * characters for a windows file name are /\:*?"&lt;&gt;|. Transferring a
1486   * file from a linux machine to a windows machine can be a problem
1487   * if one of the illegal windows file name characters is in the
1488   * linux file name. I don't know what to do about this.
1489   */
1490  static class LocalPath extends MyPath {
1491    final static long serialVersionUID = 42;
1492
1493    WatchKey key; // the watch key for a visible directory entry in tree
1494    Path path; // cached Path for URI
1495    /**
1496     * For linux: root directory ends in '/', no other LocalPath does
1497     * For windows: top-level drives /x:/ ends in '/', no other LocalPath does
1498     *
1499     * NB: InvalidPathException.getIndex() returns an index to the
1500     * unescaped URI - all %xx count as one character. It also appears
1501     * to use -1 based indexing, i.e., need to add +1 to returned
1502     * index to get address of bad character (byte). (Need to check
1503     * this out on linux.) We don't use the index value in this code.
1504     *
1505     * 1. expand URI removing all % quoted characters.
1506     * 2. quote with % all characters not on approved list.
1507     * 3. convert back to uri
1508     * 4. convert URI to Path
1509     * 5. catch any exceptions and quote forbidden character
1510     * 6. when no more - set URI and cache Path in class variable
1511     *
1512     * Attempts to make a valid Path for the local file system.
1513     *
1514     * The strategy is to clear all %'s and add %'s for known
1515     * characters requiring escaping. The resulting String is
1516     * converted to a URI and then to a Path. If this fails then the
1517     * second part of the algorithm is tried.
1518     *
1519     * The URI is truncated and tried again. If this works another
1520     * character is added to the URI. If this fails then the character
1521     * is escaped. If it is already escaped then the % is
1522     * escaped. Must check that forward progress is made or it is a
1523     * failure. When the end of the String is reached then we are
1524     * done.
1525     *
1526     * The starting point depends on whether this is a windows or a
1527     * linux machine.
1528     *
1529     * @param uri the URI to be converted to a Path
1530     */
1531    LocalPath(URI uri) {
1532      this.uri = uri.normalize();
1533      String s = this.uri.getRawPath();
1534      StringBuilder sb = new StringBuilder();
1535      for(int i = 0; i < s.length(); ++i) {
1536        // build % free string
1537        char c = s.charAt(i);
1538        if(c == '%') {
1539          char c1 = s.charAt(i + 1);
1540          int i1 = legalHex.indexOf(c1);
1541          if(i1 < 0) throw new Error("bad hex digit #1: " + c1);
1542          if(i1 >= 16) i1 -= 6;
1543          char c2 = s.charAt(i + 2);
1544          int i2 = legalHex.indexOf(c2);
1545          if(i2 < 0) throw new Error("bad hex digit #2: " + c2);
1546          if(i2 >= 16) i2 -= 6;
1547          c = (char)(16*i1 + i2);
1548          i += 2; // skip over hex value following %
1549        }
1550        if(c >= '0' && c <= '9') sb.append(c);
1551        else if(c >= 'A' && c <= 'Z') sb.append(c);
1552        else if(c >= 'a' && c <= 'z') sb.append(c);
1553        else if("!$&'()*+,-./:;=@_~".indexOf(c) >= 0) sb.append(c);
1554        // if %25 check next 2 chars for legality and output %xy; i += 2;
1555        else {
1556          sb.append('%');
1557          sb.append(hex[(c >> 4) & 0xF]);
1558          sb.append(hex[c & 0xF]);
1559        }
1560      }
1561      s = sb.toString();
1562      if(s.charAt(s.length() - 1) == '/') s = s.substring(0, s.length() - 1);
1563      if(windows) {
1564        if(s.length() == 3 && s.charAt(0) == '/' && s.charAt(2) == ':') {
1565          s = s + '/';
1566        }
1567      } else {
1568        if(s.length() == 0) {
1569          s = "/";
1570        }
1571      }
1572      // have good uri - now check platform legality
1573      try {
1574        this.uri = new URI("file://" + s); // normalize???
1575        path = Paths.get(this.uri); // this might throw
1576        //System.out.println(path);
1577        return; // early exit - all good
1578      } catch(URISyntaxException | InvalidPathException e) { // continue
1579        System.out.println("Paths.get(uri) threw");
1580        e.printStackTrace();
1581        System.exit(1); //////// stop gap until following code is debugged
1582      }
1583      // OK, now need to find the bad characters
1584      String sout;
1585      int i;
1586      if(windows) {
1587        if(s.length() == 3 && s.charAt(0) == '/' && s.charAt(2) == ':') {
1588          s = s + '/';
1589        }
1590        sout = s.substring(0, 4);
1591        i = 4;
1592      } else {
1593        if(s.length() == 0) {
1594          s = "/";
1595        }
1596        i = 1;
1597        sout = s.substring(0, 1);
1598      }
1599      // Note: getIndex returns unescaped character position starting with 1?
1600      next: while(i < s.length()) {
1601        char c = s.charAt(i);
1602        String suffix = "" + c;
1603        if(c == '%') {
1604          suffix += s.substring(i+1, i+3);
1605          i += 3;
1606        } else try {
1607            ++i;
1608            this.uri = new URI("file://" + sout + suffix); // normalize???
1609            path = Paths.get(this.uri); // this might throw
1610            sout += suffix;
1611            continue next;
1612          } catch(URISyntaxException | InvalidPathException e) { // continue
1613          }
1614        if((c = suffix.charAt(0)) != '%') {
1615          suffix = "%" + hex[(c >> 4) & 0xF] + hex[c & 0xF];
1616          try {
1617            this.uri = new URI("file://" + sout + suffix); // normalize???
1618            path = Paths.get(this.uri);
1619            sout += suffix;
1620            continue next;
1621          } catch(URISyntaxException | InvalidPathException e) {}
1622        }
1623        suffix = "%25" + suffix.substring(1);
1624        try {
1625          // fix uri
1626          this.uri = new URI("file://" + sout + suffix); // normalize???
1627          path = Paths.get(this.uri);
1628          sout += suffix;
1629          continue next;
1630        } catch(URISyntaxException | InvalidPathException e) {
1631          throw new Error(sout);
1632        }
1633      }
1634    }
1635
1636    @Override
1637    DataFlavor getFlavor() {
1638      return DataFlavor.javaFileListFlavor;
1639    };
1640
1641    @Override
1642    boolean exists() {
1643      return Files.exists(path, LinkOption.NOFOLLOW_LINKS);
1644    }
1645
1646    @Override
1647    boolean isDirectory() {
1648      return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS);
1649    }
1650
1651    @Override
1652    boolean isFile() {
1653      return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
1654    }
1655
1656    @Override
1657    boolean isLink() {
1658      return Files.isSymbolicLink(path);
1659    }
1660
1661    @Override
1662    MyPath getParent() {
1663      Path parent = path.getParent();
1664      if(parent == null) return root; // required by DefaultMutableTreeNode
1665      return new LocalPath(parent.toUri());
1666    }
1667
1668    @Override
1669    TreeSet<TableData> getChildren(boolean dotFiles) {
1670      TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
1671      DirectoryStream<Path> iter = null;
1672      try {
1673        iter = Files.newDirectoryStream(path);
1674        for(Path p : iter) {
1675          String rawPath = p.toUri().getRawPath();
1676          String name = p.getFileName().toString();
1677          if(dotFiles || !(name.length() > 0 && name.charAt(0) == '.')) {
1678            list.add(new TableData(new LocalPath(p.toUri())));
1679          }
1680        }
1681      } catch(NoSuchFileException e) {
1682        System.err.println("NoSuchFileException caught");
1683        e.printStackTrace();
1684      } catch(IOException e) {
1685        System.err.println("IOException caught");
1686        e.printStackTrace();
1687        System.err.println(list);
1688      } finally {
1689        try {
1690          if(iter != null) iter.close();
1691        } catch(IOException ex) {
1692          System.err.println("Error closing iter");
1693          ex.printStackTrace();
1694        }
1695      }
1696      return list;
1697    }
1698
1699    /**
1700     * Only gets directories
1701     */
1702    @Override
1703    ArrayList<MyPath> getTreeChildren() {
1704      if(treeChildren != null) return treeChildren; //////////////////
1705      treeChildren = new ArrayList<MyPath>();
1706      DirectoryStream<Path> iter = null;
1707      try {
1708        iter = Files.newDirectoryStream(path);
1709        for(Path p : iter) {
1710          LocalPath localPath = new LocalPath(p.toUri());
1711          //// links are followed - not
1712          if(localPath.isDirectory()) {
1713            treeChildren.add(localPath);
1714          }
1715        }
1716      } catch(IOException e) {
1717        System.err.println("Error listing children: " + uri);
1718        e.printStackTrace();
1719      } finally {
1720        try {
1721          iter.close();
1722        } catch(IOException ex) {
1723          ex.printStackTrace();
1724        }
1725      }
1726      Collections.sort(treeChildren, pathComparator);
1727      return treeChildren;
1728    }
1729
1730    @Override
1731    LocalPath resolve(String other) {
1732      String name = uri.toString();
1733      if(name.charAt(name.length() - 1) != '/') name = name + '/';
1734      try {
1735        return new LocalPath(new URI(name + other));
1736      } catch(URISyntaxException e) {
1737        e.printStackTrace();
1738        return this;
1739      }
1740    }
1741
1742    @Override
1743    LocalPath resolve(Path other) {
1744      Path p = Paths.get(uri).resolve(other);
1745      return(new LocalPath(p.toUri()));
1746    }
1747
1748    @Override
1749    long size() {
1750      try {
1751        return Files.size(path);
1752      } catch(IOException e) {
1753        System.err.println("size() failed: " + uri);
1754        return 0;
1755      }
1756    }
1757
1758    @Override
1759    long getMTime() {
1760      try {
1761        FileTime time
1762          = (FileTime)Files.getAttribute(path,
1763                                         "lastModifiedTime",
1764                                         LinkOption.NOFOLLOW_LINKS);
1765        return time.toMillis();
1766      } catch(IOException e) {
1767        System.err.println("getMTime() failed: " + uri);
1768        return 0;
1769      }
1770    }
1771
1772    @Override
1773    boolean setMTime(long time) {
1774      try {
1775        Files.setLastModifiedTime(path, FileTime.fromMillis(time));
1776      } catch(IOException e) {
1777        e.printStackTrace();
1778        return false;
1779      }
1780      return true;
1781    }
1782
1783    @Override
1784    boolean makeDirectory() {
1785      try {
1786        Files.createDirectory(path);
1787      } catch(IOException e) {
1788        System.err.println("makeDirectory() failed: " + uri);
1789        return false;
1790      }
1791      return true;
1792    }
1793
1794    @Override
1795    boolean renameFile(String newName) {
1796      // cannot change case of local directories ///////////////////
1797      Path newPath = path.resolveSibling(newName);
1798      boolean force = false;
1799      try {
1800        if(force) {
1801          Files.move(path, newPath,
1802                     java.nio.file.StandardCopyOption.REPLACE_EXISTING,
1803                     LinkOption.NOFOLLOW_LINKS);
1804        } else {
1805          Files.move(path, newPath, LinkOption.NOFOLLOW_LINKS);
1806        }
1807        return true;
1808      } catch(AccessDeniedException e) {
1809        System.err.println(e);
1810        System.err.println(e.getReason());
1811        System.err.println("Failed renaming file: " + fullName());
1812        return false;
1813      } catch(IOException e) {
1814        System.err.println(e);
1815        System.err.println("Failed renaming file: " + fullName());
1816        return false;
1817      }
1818    }
1819
1820    @Override
1821    void readFile(byte[] data) {
1822      InputStream is = null;
1823      try {
1824        is = Files.newInputStream(path);
1825        int index = 0;
1826        while(index < data.length) {
1827          int count = is.read(data, index, data.length - index);
1828          if(count <= 0) throw new Error("readFile did not fill buffer");
1829          index += count;
1830        }
1831      } catch(IOException e) {
1832        System.err.println("Failed reading file: " + fullName());
1833        e.printStackTrace();
1834      } finally {
1835        try {
1836          if(is != null) is.close();
1837        } catch(IOException e) {
1838          System.err.println(e);
1839          System.err.println("Failed closing file: " + fullName());
1840        }
1841      }
1842    }
1843
1844    @Override
1845    void writeFile(byte[] data) {
1846      OutputStream os = null;
1847      try {
1848        os = Files.newOutputStream(path, LinkOption.NOFOLLOW_LINKS);
1849        os.write(data);
1850      } catch(IOException e) {
1851        System.err.println(e);
1852        System.err.println("Failed writing file: " + fullName());
1853      } finally {
1854        try {
1855          if(os != null) os.close();
1856        } catch(IOException e) {
1857          System.err.println(e);
1858          System.err.println("Failed closing file: " + fullName());
1859        }
1860      }
1861    }
1862
1863    @Override
1864    List<String> readAllLines() {
1865      try {
1866        return Files.readAllLines(path, Charset.forName("ISO-8859-1"));
1867      } catch(IOException e) {
1868        e.printStackTrace();
1869        return new ArrayList<String>();
1870      }
1871    }
1872
1873    @Override
1874    byte[] readLink() {
1875      try {
1876        ///////////// not quite correct
1877        return stringToBytes(Files.readSymbolicLink(path).toString());
1878      } catch(IOException e) {
1879        e.printStackTrace();
1880        return null;
1881      }
1882    }
1883
1884    @Override
1885    boolean makeLinkTo(byte[] target) {
1886      try {
1887        //URI uri = toUri(bytesToString(target));
1888        //Path targetPath = Paths.get(toUri(bytesToString(target)));
1889        //Path targetPath = Paths.get(uri);
1890        Path targetPath = Paths.get(bytesToString(target));
1891        Files.createSymbolicLink(path, targetPath);
1892      } catch(IOException e) {
1893        e.printStackTrace();
1894        return false;
1895      }
1896      return true;
1897    }
1898
1899    @Override
1900    void touch() {
1901      try {
1902        try {
1903          Files.createFile(path);
1904        } catch(FileAlreadyExistsException e) {
1905          Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
1906        }
1907      } catch(IOException e) {
1908        e.printStackTrace();
1909      }
1910    }
1911
1912    @Override
1913    void moveFileFrom(MyPath file) {
1914      file.moveFileTo(this);
1915    }
1916
1917    @Override
1918    // other is a directory
1919    void moveFileTo(LocalPath other) {
1920      boolean force = false;
1921      try {
1922        if(force) {
1923          Files.move(path, other.path,
1924                     java.nio.file.StandardCopyOption.REPLACE_EXISTING,
1925                     LinkOption.NOFOLLOW_LINKS);
1926        } else {
1927          Files.move(path, other.path, LinkOption.NOFOLLOW_LINKS);
1928        }
1929      } catch(IOException e) {
1930        System.err.println("Failed moving file: " + fullName());
1931      }
1932    }
1933
1934    @Override
1935    void moveFileTo(RemotePath other) {
1936      ////////////////////////////////
1937    }
1938
1939    @Override
1940    boolean copyFileFrom(MyPath file) {
1941      return file.copyFileTo(this);
1942    }
1943
1944    @Override
1945    boolean copyFileTo(LocalPath toFile) {
1946      boolean force = true;
1947      try {
1948        Files.copy(path, toFile.path,
1949                   java.nio.file.StandardCopyOption.REPLACE_EXISTING,
1950                   java.nio.file.StandardCopyOption.COPY_ATTRIBUTES,
1951                   LinkOption.NOFOLLOW_LINKS);
1952      } catch(IOException e) {
1953        e.printStackTrace();
1954        System.err.println("Failed copying file: " + fullName());
1955        return false;
1956      }
1957      return true;
1958    }
1959
1960    @Override
1961    boolean copyFileTo(RemotePath toFile) {
1962      ChannelSftp channel = null;
1963      try {
1964        channel = getSftpChannel(toFile.uri.getRawAuthority());
1965        channel.putUri(uri, toFile.uri, myProgMon);
1966        toFile.setMTime(getMTime());
1967        ////////// set permissions
1968        try {
1969          //System.out.println(Files.getPosixFilePermissions(path));
1970          //Set<PosixFilePermission> set = Files.getPosixFilePermissions(path);
1971          /*
1972          int octal = 0;
1973          for(PosixFilePermission perm : set) {
1974            System.out.println(perm.ordinal());
1975            octal |= 1 << (8 - perm.ordinal());
1976          }
1977          System.out.println(Integer.toOctalString(octal));
1978          System.out.println(Integer.toOctalString(octalFilePermissions(set)));
1979          */
1980        } catch(UnsupportedOperationException e) {
1981          System.err.println("Could not copy permissions on file: " + fullName());
1982        }
1983      } catch(SftpException | SftpOpenFailedException e) {
1984        e.printStackTrace();
1985        return false;
1986      } finally {
1987        releaseChannel(channel);
1988      }
1989      return true;
1990    }
1991
1992    @Override
1993    boolean delete() {
1994      try {
1995        Files.delete(path); // does not follow links
1996      } catch(IOException e) {
1997        e.printStackTrace();
1998        return false;
1999      }
2000      return true;
2001    }
2002
2003    @Override
2004    public String toString() {
2005      String localFileName = uri.getRawPath();
2006      int lastSlash = localFileName.lastIndexOf('/');
2007      if(lastSlash == localFileName.length() - 1) return fullName();
2008      return localFileName.substring(lastSlash + 1);
2009    }
2010
2011    @Override
2012    String fullName() {
2013      return uri.getRawPath();
2014    }
2015
2016    @Override
2017    public boolean equals(Object other) {
2018      if(other == null || !(other instanceof LocalPath)) return false;
2019      return path.equals(((LocalPath)other).path);
2020    }
2021
2022    @Override
2023    public int hashCode() {
2024      return path.hashCode();
2025    }
2026  } // static class LocalPath extends MyPath
2027
2028  /**
2029   * An object that may be used to locate a file in a remote file
2030   * system.
2031   *
2032   * A RemotePath represents a path that is hierarchical and composed
2033   * of a sequence of directory and file name elements separated by a
2034   * special separator or delimiter. A root component, that identifies
2035   * a file system hierarchy, must be present. The name element that
2036   * is farthest from the root of the directory hierarchy is the name
2037   * of a file or directory. The other name elements are directory
2038   * names. A RemotePath can represent a root or a root and a sequence
2039   * of names, i.e., all RemotePaths are absolute. RemotePath defines
2040   * the getFileName(), getParent() getRoot(), and methods to access
2041   * the path components or a subsequence of its name elements.
2042   *
2043   * [In addition to accessing the components of a path, a Path also
2044   * defines the resolve and resolveSibling(Path) methods to combine
2045   * paths. (The relativize method that can be used to construct a
2046   * relative path between two paths. Paths can be compared, and
2047   * tested against each other using the startsWith and endsWith
2048   * methods.]
2049   *
2050   * RemotePaths may be used to operate on files, directories, and
2051   * other types of files.
2052   *
2053   * Implementations of this interface are immutable and safe for use
2054   * by multiple concurrent threads.
2055   */
2056  static class RemotePath extends MyPath {
2057    final static long serialVersionUID = 42;
2058
2059    transient SftpATTRS linkAttributes = null;
2060
2061    RemotePath(URI uri) {
2062      if(uri.getRawAuthority() == null) new Error("NULL constructor uri: " + uri).printStackTrace();
2063      this.uri = uri;
2064    }
2065
2066    RemotePath(URI uri, SftpATTRS linkAttributes) {
2067      if(uri.getRawAuthority() == null) new Error("NULL constructor uri: " + uri).printStackTrace();
2068      this.uri = uri;
2069      this.linkAttributes = linkAttributes;
2070    }
2071
2072    SftpATTRS getLinkAttributes() {
2073      if(linkAttributes != null) return linkAttributes;
2074      ChannelSftp channel = null;
2075      try {
2076        channel = getSftpChannel(uri.getRawAuthority());
2077        return linkAttributes = channel.lstatUri(uri);
2078      } catch(SftpException | NullPointerException | SftpOpenFailedException e) {
2079        return null;
2080      } finally {
2081        releaseChannel(channel);
2082      }
2083    }
2084
2085    @Override
2086    DataFlavor getFlavor() {
2087      return remotePathArrayFlavor;
2088    }
2089
2090    @Override
2091    boolean exists() {
2092      SftpATTRS attrs = getLinkAttributes();
2093      return attrs != null;
2094    }
2095
2096    @Override
2097    boolean isDirectory() {
2098      if(getLinkAttributes() == null) return false;
2099      return getLinkAttributes().isDir();
2100    }
2101
2102    @Override
2103    boolean isFile() {
2104      if(getLinkAttributes() == null) return false;
2105      return getLinkAttributes().isReg();
2106    }
2107
2108    @Override
2109    boolean isLink() {
2110      if(getLinkAttributes() == null) return false;
2111      return getLinkAttributes().isLink();
2112    }
2113
2114    @Override
2115    MyPath getParent() {
2116      String remoteFileName = uri.getRawPath();
2117      String userHost = uri.getRawAuthority();
2118      int lastSlash = remoteFileName.lastIndexOf('/');
2119      if(lastSlash < 0 || remoteFileName.length() == 1) {
2120        return root;
2121      }
2122      if(lastSlash == 0) return new RemotePath(toUri("//" + userHost + "/"));
2123      return new RemotePath(toUri("//" + userHost + remoteFileName.substring(0, lastSlash)));
2124    }
2125
2126    /**
2127     * Fix failures to return empty ArrayList&lt;TableData&gt; /////////////
2128     */
2129    @Override
2130    TreeSet<TableData> getChildren(boolean dotFiles) {
2131      TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
2132      java.util.Vector<ChannelSftp.LsEntryUri> vv = null;
2133      ChannelSftp channel = null;
2134      String userHost = uri.getRawAuthority();
2135      String remoteFileName = uri.getRawPath();
2136      try {
2137        channel = getSftpChannel(userHost);
2138        vv = channel.lsUri(uri);
2139      } catch(SftpException e) {
2140        System.err.println("Can't get Paths: " + uri.toString()); /////////////
2141        e.printStackTrace();
2142      } catch(SftpOpenFailedException e) {
2143        System.err.println("Couldn't open " + userHost + "  " + remoteFileName);
2144        return root.getChildren(dotFiles);
2145      } finally {
2146        releaseChannel(channel);
2147      }
2148      if(vv!=null) {
2149        for(int ii = 0; ii < vv.size(); ++ii) {
2150          ChannelSftp.LsEntryUri obj = vv.get(ii);
2151          ChannelSftp.LsEntryUri entry = obj;
2152          SftpATTRS attrs = entry.getAttrs();
2153          String name = entry.getUri().getRawPath(); ////////////////
2154          int lastSlash = name.lastIndexOf('/');
2155          if(name.length() != 1) {
2156            name = name.substring(lastSlash + 1);
2157          }
2158          if(name.equals(".") || name.equals("..")) continue;
2159          if(!dotFiles && name.length() > 0 && name.charAt(0) == '.') continue;
2160          list.add(new TableData(new RemotePath(entry.getUri(), attrs)));
2161        }
2162      }
2163      return list;
2164    }
2165
2166    /**
2167     * Only gets directories
2168     */
2169    @Override
2170    ArrayList<MyPath> getTreeChildren() {
2171      if(treeChildren != null) return treeChildren; // needed to prevent expansion of upper directories
2172      treeChildren = new ArrayList<MyPath>();
2173      // get remote directory listing
2174      java.util.Vector<ChannelSftp.LsEntryUri> vv =null;
2175      ChannelSftp channel = null;
2176      String userHost = uri.getRawAuthority();
2177      String remoteFileName = uri.getRawPath();
2178      try {
2179        channel = getSftpChannel(userHost);
2180        vv = channel.lsUri(uri);
2181      } catch(SftpException e) {
2182        System.err.println("SftpException: " + "   " + userHost + "  " + remoteFileName);
2183        e.printStackTrace();
2184      } catch(SftpOpenFailedException e) {
2185        System.err.println("getTreeChildren() failed " + "   " + userHost + "  " + remoteFileName);
2186        e.printStackTrace();
2187      } finally {
2188        releaseChannel(channel);
2189      }
2190      if(vv!=null) {
2191        for(int ii = 0; ii < vv.size(); ++ii) {
2192          ChannelSftp.LsEntryUri entry = vv.get(ii);
2193          SftpATTRS attrs = entry.getAttrs();
2194          String name = entry.getUri().getRawPath();
2195          int lastSlash = name.lastIndexOf('/');
2196          if(name.length() != 1) {
2197            name = name.substring(lastSlash + 1);
2198          }
2199          if(name.equals(".") || name.equals("..")) continue;
2200          if(attrs.isDir()) {
2201            treeChildren.add(new RemotePath(entry.getUri()));
2202          }
2203        }
2204      }
2205      Collections.sort(treeChildren, pathComparator);
2206      return treeChildren;
2207    }
2208
2209    @Override
2210    RemotePath resolve(String other) {
2211      String name = uri.toString();
2212      if(name.charAt(name.length() - 1) != '/') name = name + '/';
2213      try {
2214        return new RemotePath(new URI(name + other));
2215      } catch(URISyntaxException e) {
2216        e.printStackTrace();
2217        return this;
2218      }
2219    }
2220
2221    @Override
2222    RemotePath resolve(Path other) {
2223      return(new RemotePath(uri.resolve(other.toUri())));
2224    }
2225
2226    @Override
2227    long size() {
2228      return getLinkAttributes().getSize();
2229    }
2230
2231    @Override
2232    long getMTime() {
2233      return getLinkAttributes().getMTime()*1000L; // only second resolution
2234    }
2235
2236    @Override
2237    boolean setMTime(long time) {
2238      ChannelSftp channel = null;
2239      String userHost = uri.getRawAuthority();
2240      try {
2241        channel = getSftpChannel(userHost);
2242        channel.setMtimeUri(uri, (int)(time/1000));
2243      } catch(SftpOpenFailedException e) {
2244        System.err.println(e);
2245        System.err.println("Opening Session failed: " + userHost);
2246        e.printStackTrace();
2247        return false;
2248      } catch(Exception e) {
2249        e.printStackTrace();
2250      } finally {
2251        releaseChannel(channel);
2252      }
2253      return true;
2254    }
2255
2256    @Override
2257    boolean makeDirectory() {
2258      ChannelSftp channel = null;
2259      String userHost = uri.getRawAuthority();
2260      try {
2261        channel = getSftpChannel(userHost);
2262        channel.mkdirUri(uri);
2263      } catch(SftpException e) {
2264        System.err.println("makeDirectory failed: " + uri.toString());
2265        return false;
2266      } catch(SftpOpenFailedException e) {
2267        e.printStackTrace();
2268        return false;
2269      } finally {
2270        releaseChannel(channel);
2271      }
2272      return true;
2273    }
2274
2275    @Override
2276    boolean renameFile(String newName) {
2277      String fullName = fullName();
2278      int firstSlash = fullName.indexOf('/', 2);
2279      int lastSlash = fullName.lastIndexOf('/');
2280      String targetFullName
2281        = fullName.substring(firstSlash, lastSlash + 1) + newName;
2282      ChannelSftp channel = null;
2283      try {
2284        channel = getSftpChannel(uri.getAuthority());
2285        channel.rename(uri.getPath(), targetFullName); ///////////////uri
2286      } catch(SftpException e) {
2287        System.err.println("makeDirectory failed: " + uri.getPath());
2288        return false;
2289      } catch(SftpOpenFailedException e) {
2290        e.printStackTrace();
2291        return false;
2292      } finally {
2293        releaseChannel(channel);
2294      }
2295      return true;
2296    }
2297
2298    @Override
2299    void readFile(byte[] data) {
2300      ChannelSftp fromChannel = null;
2301      InputStream is = null;
2302      try {
2303        fromChannel = getSftpChannel(uri.getRawAuthority());
2304        is = fromChannel.getUri(uri, null, 0L);
2305        int index = 0;
2306        while(index < data.length) {
2307          int count = is.read(data, index, data.length - index);
2308          if(count <= 0) throw new Error("readFile did not fill buffer");
2309          index += count;
2310        }
2311      } catch(Exception e) {
2312        System.err.println("Failed reading file: " + fullName());
2313        e.printStackTrace();
2314      } finally {
2315        try {
2316          if(is != null) is.close();
2317        } catch(IOException e) {
2318          System.err.println(e);
2319          System.err.println("Failed closing file: " + fullName());
2320        }
2321        releaseChannel(fromChannel);
2322      }
2323    }
2324
2325    @Override
2326    void writeFile(byte[] data) {
2327      ChannelSftp toChannel = null;
2328      OutputStream os = null;
2329      try {
2330        toChannel = getSftpChannel(uri.getRawAuthority());
2331        os = toChannel.putUri(uri, null, ChannelSftp.OVERWRITE, 0L);
2332        os.write(data);
2333      } catch(Exception e) {
2334        e.printStackTrace();
2335      } finally {
2336        try {
2337          if(os != null) os.close();
2338        } catch(IOException e) {
2339          System.err.println(e);
2340          System.err.println("Failed closing file: " + fullName());
2341        }
2342        releaseChannel(toChannel);
2343      }
2344    }
2345
2346    @Override
2347    List<String> readAllLines() {
2348      byte[] data = new byte[(int)size()]; // possible overflow
2349      readFile(data);
2350      ArrayList<String> lines = new ArrayList<String>();
2351      int index = 0;
2352      int i = 0;
2353      for(i = index; i < data.length; ++i) {
2354        if(data[i] == '\n') {
2355          lines.add(new String(data, index, i - index)); ////// set charset????
2356          if(i+1 < data.length && data[i+1] == '\r') ++i;
2357          index = i + 1; // past \n or \r
2358        } else if(i < data.length && data[i] == '\r') {
2359          lines.add(new String(data, index, i - index)); ////// set charset????
2360          index = i + 1; // past \r
2361        }
2362      }
2363      if(index < data.length && i < data.length) {
2364        lines.add(new String(data, index, i - index));
2365      }
2366      return lines;
2367    }
2368
2369    @Override
2370    byte[] readLink() {
2371      ChannelSftp channel = null;
2372      String userHost = uri.getRawAuthority();
2373      try {
2374        channel = getSftpChannel(userHost);
2375        return channel.readlinkUri(uri);
2376        //return bytesToString(bytes);
2377      } catch(SftpException e) {
2378        System.err.println("readLink failed: " + uri.toString());
2379        return null;
2380      } catch(SftpOpenFailedException e) {
2381        e.printStackTrace();
2382        return null;
2383      } finally {
2384        releaseChannel(channel);
2385      }
2386    }
2387
2388    @Override
2389    boolean makeLinkTo(byte[] target) {
2390      ChannelSftp channel = null;
2391      String userHost = uri.getRawAuthority();
2392      try {
2393        channel = getSftpChannel(userHost);
2394        channel.symlinkUri(target, uri);
2395      } catch(SftpException e) {
2396        System.err.println("makeLinkTo failed: " + uri.toString());
2397        return false;
2398      } catch(SftpOpenFailedException e) {
2399        e.printStackTrace();
2400        return false;
2401      } finally {
2402        releaseChannel(channel);
2403      }
2404      return true;
2405    }
2406
2407    @Override
2408    void touch() {
2409      if(exists()) {
2410        setMTime(System.currentTimeMillis());
2411      } else {
2412        writeFile(new byte[0]);
2413      }
2414    }
2415
2416    @Override
2417    void moveFileFrom(MyPath file) {
2418      file.moveFileTo(this);
2419    }
2420
2421    @Override
2422    void moveFileTo(LocalPath file) {
2423      throw new Error("Not Implemented Yet");
2424      ///////////////////////////////
2425    }
2426
2427    @Override
2428    void moveFileTo(RemotePath file) {
2429      throw new Error("Not Implemented Yet");
2430      ///////////////////////////////
2431    }
2432
2433    @Override
2434    boolean copyFileFrom(MyPath file) {
2435      return file.copyFileTo(this);
2436    }
2437
2438    @Override
2439    boolean copyFileTo(LocalPath toPath) {
2440      ChannelSftp channel = null;
2441      try {
2442        channel = getSftpChannel(uri.getRawAuthority());
2443        channel.getUri(uri, toPath.uri, myProgMon);
2444        toPath.setMTime(getMTime());
2445        ////////// set permissions
2446      } catch(SftpException | SftpOpenFailedException e) {
2447        System.err.println(e.getMessage());
2448        e.printStackTrace();
2449        return false;
2450      } finally {
2451        releaseChannel(channel);
2452      }
2453      return true;
2454    }
2455
2456    @Override
2457    boolean copyFileTo(RemotePath toPath) {
2458      ChannelSftp fromChannel = null;
2459      ChannelSftp toChannel = null;
2460      InputStream is = null;
2461      OutputStream os = null;
2462      try {
2463        fromChannel = getSftpChannel(uri.getRawAuthority());
2464        toChannel = getSftpChannel(toPath.uri.getRawAuthority());
2465        is = fromChannel.getUri(uri, null, 0L);
2466        os = toChannel.putUri(toPath.uri, myProgMon, ChannelSftp.OVERWRITE, 0L);
2467        byte[] buf = new byte[4096];
2468        int len;
2469        while((len = is.read(buf)) > 0) {
2470          os.write(buf, 0, len);
2471        }
2472        os.flush();
2473        toPath.setMTime(getMTime());
2474      } catch(Exception e) {
2475        e.printStackTrace();
2476        return false;
2477      } finally {
2478        try {
2479          if(is != null) is.close();
2480        } catch(IOException e) {}
2481        try {
2482          if(os != null) os.close();
2483        } catch(IOException e) {}
2484        releaseChannel(toChannel);
2485        releaseChannel(fromChannel);
2486      }
2487      return true;
2488    }
2489
2490    @Override
2491    boolean delete() {
2492      ChannelSftp channel = null;
2493      try {
2494        channel = getSftpChannel(uri.getRawAuthority());
2495        if(isDirectory()) {
2496          channel.rmdirUri(uri);
2497        } else if(isFile() || isLink()) {
2498          channel.rmUri(uri);
2499        } else {
2500          System.err.println("Unknown file type: " + fullName());
2501          return false;
2502        }
2503      } catch(SftpException | SftpOpenFailedException e) {
2504        e.printStackTrace();
2505        return false;
2506      } finally {
2507        releaseChannel(channel);
2508      }
2509      return true;
2510    }
2511
2512    @Override
2513    public String toString() {
2514      String remoteFileName = uri.getRawPath();
2515      int lastSlash = remoteFileName.lastIndexOf('/');
2516      if(remoteFileName.charAt(remoteFileName.length() - 1) == '/') return fullName();
2517      return remoteFileName.substring(lastSlash + 1);
2518    }
2519
2520    @Override
2521    String fullName() {
2522      return "//" + uri.getRawAuthority() + uri.getRawPath();
2523    }
2524
2525    @Override
2526    public boolean equals(Object other) {
2527      if(other == null || !(other instanceof RemotePath)) return false;
2528      RemotePath p = (RemotePath)other;
2529      return uri.equals(p.uri);
2530    }
2531
2532    @Override
2533    public int hashCode() {
2534      return uri.hashCode();
2535    }
2536  } // static class RemotePath extends MyPath
2537
2538  final static int replaceFile = 0x1; // allow replacing a file with a file
2539  final static int replaceFileWithDirectory = 0x2; // allow replacing file with directory
2540  final static int replaceDirectoryWithFile = 0x4; // allow replacing directory with file
2541
2542  /**
2543   * Copies a file or a complete directory tree.
2544   *
2545   * ////////////// need to copy file permissions (chmod)
2546   *
2547   * @param from the file or directory to copy
2548   * @param to the file or directory to copy to
2549   * @param flags bit map of what to allow
2550   * @param feedback a JLabel where to display current file/directory
2551   * @return true if successful
2552   */
2553  static boolean copyTree(MyPath from, MyPath to, int flags, JLabel feedback) {
2554    if(feedback != null) {
2555      feedback.setText("copying: " + from.fullName() + " -> " + to.fullName());
2556    }
2557    if(!from.exists()) {
2558      System.err.println("Copying from nonexistant path: " + from.fullName());
2559      return false;
2560    }
2561    if(from.isFile()) {
2562      if(to.exists()) {
2563        if((flags & (replaceFile | replaceDirectoryWithFile)) == 0) {
2564          System.err.println("flag1 fail: " + flags + "   to: " + to.uri);
2565          return false; //////////// dialog here
2566        }
2567        deleteTree(to, feedback);
2568      }
2569      return to.copyFileFrom(from);
2570    } else if(from.isLink()) {
2571      if(to.exists()) {
2572        if((flags & (replaceFile | replaceDirectoryWithFile)) == 0) {
2573          System.err.println("flag1 fail: " + flags + "   to: " + to.uri);
2574          return false; //////////// dialog here
2575        }
2576        deleteTree(to, feedback);
2577      }
2578      byte[] target = from.readLink();
2579      if(target == null) return false; ///////// error message
2580      return from.makeLinkTo(target);
2581    } else if(from.isDirectory()) {
2582      if(to.exists()) {
2583        if((flags & replaceFileWithDirectory) == 0) {
2584          System.err.println("flag2 fail: " + flags + "   to: " + to.uri);
2585          return false; //////////// dialog here
2586        }
2587        deleteTree(to, feedback);
2588      }
2589      to.makeDirectory();
2590      for(TableData child : from.getChildren(true)) {
2591        try {
2592          String name = to.uri.toString();
2593          URI toUri = new URI(name + '/');
2594          URI suffix = from.uri.relativize(child.path.uri);
2595          toUri = toUri.resolve(suffix);
2596          if(toUri.getRawAuthority() == null) {
2597            copyTree(child.path, new LocalPath(toUri), flags, feedback);
2598          } else {
2599            copyTree(child.path, new RemotePath(toUri), flags, feedback);
2600          }
2601        } catch(Throwable e) {
2602          e.printStackTrace();
2603          return false;
2604        }
2605      }
2606      return true;
2607    } else {
2608      System.err.println("unknown file type: " + from.fullName());
2609      return false;
2610    }
2611  } // static boolean copyTree(from, to, flags, feedback)
2612
2613  /**
2614   * Moves a file or directory. Does a copy followed by a delete of
2615   * the old file or directory. For testing the delete is not
2616   * performed.
2617   *
2618   * @param from the file or tree to move
2619   * @param to the directory to move the file or directory to
2620   * @return true if successful
2621   */
2622  static boolean moveTree(MyPath from, MyPath to) {
2623    if(copyTree(from, to, 0, null)) {
2624      // return deleteTree(from); //////// add this statement when confident
2625      return true;
2626    } else return false;
2627  } // static boolean moveTree(MyPath from, MyPath to)
2628
2629  /**
2630   * Deletes a file or an entire directory tree
2631   *
2632   * @param path the file or directory to delete
2633   * @param feedback A JLabel for posting progress feedback
2634   * @return true if successful
2635   */
2636  static boolean deleteTree(MyPath path, JLabel feedback) {
2637    if(path.isDirectory()) {
2638      for(TableData p : path.getChildren(true)) {
2639        if(!deleteTree(p.path, feedback)) return false;
2640      }
2641    }
2642    return path.delete();
2643  } // static boolean deleteTree(MyPath path, JLabel feedback)
2644
2645  /**
2646   * Called to shut down everything at the end of running the program.
2647   */
2648  static void finish() {
2649    for(Map.Entry<String, Session> pair : sessions.entrySet()) {
2650      pair.getValue().disconnect();
2651    }
2652    if(myProgMon != null) myProgMon.dispose();
2653    writeOptions();
2654  }
2655
2656  /**
2657   * This class provides a search and replace function for a file edit
2658   * window.
2659   *
2660   * Layout:
2661   *
2662   * (((Table |XXX| Field |XXX| _rowid_ |XXX|)))
2663   *
2664   * |   Find   | |XXXXX|   |Replace/Find| |XXXXX|
2665   * |  Replace | |XXXXX|   | Find Lines | |XXXXX|
2666   *
2667   * Find: select next occurance after current selection
2668   * Replace: replace current selection with replacement string
2669   * Replace/Find: do a Replace followed by a Find
2670   * Find Lines: find lines by number,number specified in second window
2671   */
2672  static class FindReplace extends JFrame {
2673    final static long serialVersionUID = 42;
2674
2675    JTextArea textArea;
2676    JLabel status = new JLabel("Status area");
2677    JTextArea findField = new UndoableTextArea(4, 15);
2678    JTextArea replaceField = new UndoableTextArea(4, 15);
2679
2680    /**
2681     * Make a find/replace window operating on the given EditWindow.
2682     *
2683     * @param editWindow the EditWindow to operate on
2684     */
2685    FindReplace(EditWindow editWindow) {
2686      setTitle("Find - Replace");
2687      addWindowListener(new WindowAdapter() {
2688          @Override
2689          public void windowClosing(WindowEvent e) {
2690            if(--windowCount == 0) {
2691              finish();
2692            }
2693            dispose();
2694          }
2695        });
2696      ++windowCount;
2697      this.textArea = editWindow.textArea;
2698      Rectangle r = editWindow.getBounds();
2699      setLocation(r.x + r.width, r.y);
2700      setTitle("Find - Replace");
2701      JPanel args2 = new JPanel();
2702      JPanel btns1 = new JPanel(new GridLayout(2, 1));
2703      btns1.add(new JButton("Find:") {
2704          final static long serialVersionUID = 42;
2705
2706          {
2707            addActionListener(new ActionListener() {
2708                @Override
2709                public void actionPerformed(ActionEvent e) {
2710                  status.setText("");
2711                  String text = textArea.getText();
2712                  String find = findField.getText();
2713                  int end = textArea.getSelectionEnd();
2714                  int location = text.indexOf(find, end);
2715                  if(location < 0) {
2716                    status.setText("not found");
2717                    textArea.select(0, 0);
2718                  } else {
2719                    textArea.select(location, location + find.length());
2720                    editWindow.toFront();
2721                  }
2722                }
2723              });
2724          }
2725        });
2726      btns1.add(new JButton("Replace:") {
2727          final static long serialVersionUID = 42;
2728
2729          {
2730            addActionListener(new ActionListener() {
2731                @Override
2732                public void actionPerformed(ActionEvent e) {
2733                  status.setText("");
2734                  String replace = replaceField.getText();
2735                  textArea.replaceSelection(replace);
2736                  int end = textArea.getSelectionEnd();
2737                  textArea.select(end - replace.length(), end);
2738                }
2739              });
2740          }
2741        });
2742      args2.add(btns1);
2743      JScrollPane findScrollPane = new JScrollPane(findField);
2744      args2.add(findScrollPane);
2745      args2.add(new JLabel("      "));
2746      JPanel btns2 = new JPanel(new GridLayout(2, 1));
2747      args2.add(btns2);
2748      btns2.add(new JButton("Replace/Find:") {
2749          final static long serialVersionUID = 42;
2750
2751          {
2752            addActionListener(new ActionListener() {
2753                @Override
2754                public void actionPerformed(ActionEvent e) {
2755                  status.setText("");
2756                  String replace = replaceField.getText();
2757                  textArea.replaceSelection(replace);
2758                  int end = textArea.getSelectionEnd();
2759                  textArea.select(end - replace.length(), end);
2760                  String text = textArea.getText();
2761                  String find = findField.getText();
2762                  int location = text.indexOf(find, end);
2763                  if(location < 0) {
2764                    status.setText("not found");
2765                    textArea.select(0, 0);
2766                  } else {
2767                    textArea.select(location, location + find.length());
2768                    editWindow.toFront();
2769                  }
2770                }
2771              });
2772          }
2773        });
2774      btns2.add(new JButton("Find Lines") {
2775          final static long serialVersionUID = 42;
2776
2777          {
2778            addActionListener(new ActionListener() {
2779                @Override
2780                public void actionPerformed(ActionEvent e) {
2781                  status.setText("");
2782                  try {
2783                    String lineSelection = replaceField.getText();
2784                    int comma = lineSelection.indexOf(',');
2785                    if(comma < 0) comma = lineSelection.length();
2786                    int startLine = Integer.parseUnsignedInt(lineSelection.substring(0, comma));
2787                    int endLine = startLine;
2788                    if(comma != lineSelection.length()) {
2789                      endLine = Integer.parseUnsignedInt(lineSelection.substring(comma + 1));
2790                    }
2791                    int start = textArea.getLineStartOffset(startLine - 1);
2792                    int end = textArea.getLineEndOffset(endLine - 1);
2793                    textArea.select(start, end);
2794                  } catch(NumberFormatException | BadLocationException ex) {
2795                    status.setText("Bad line designation");
2796                  }
2797                  editWindow.toFront();
2798                }
2799              });
2800          }
2801        });
2802      args2.add(btns2);
2803      JScrollPane replaceScrollPane = new JScrollPane(replaceField);
2804      args2.add(replaceScrollPane);
2805      getContentPane().add(args2, BorderLayout.CENTER);
2806      getContentPane().add(status, BorderLayout.SOUTH);
2807    }
2808  } // static class FindReplace extends JFrame
2809
2810  static class UndoableTextArea extends JTextArea {
2811    final static long serialVersionUID = 42;
2812
2813    UndoableTextArea() {
2814      super();
2815    }
2816
2817    UndoableTextArea(Document doc) {
2818      super(doc);
2819    }
2820
2821    UndoableTextArea(Document doc, String text, int rows, int columns) {
2822      super(doc, text, rows, columns);
2823    }
2824
2825    UndoableTextArea(int rows, int columns) {
2826      super(rows, columns);
2827    }
2828
2829    UndoableTextArea(String text) {
2830      super(text);
2831    }
2832
2833    UndoableTextArea(String text, int rows, int columns) {
2834      super(text, rows, columns);
2835    }
2836
2837    {
2838      UndoManager undoManager = new UndoManager();
2839      undoManager.setLimit(-1); // unlimited undos
2840      Document doc = getDocument();
2841      doc.addUndoableEditListener(new UndoableEditListener() {
2842          @Override
2843          public void undoableEditHappened(UndoableEditEvent e) {
2844            undoManager.addEdit(e.getEdit());
2845          }
2846        });
2847
2848      InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
2849      ActionMap am = getActionMap();
2850
2851      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
2852                                    toolkit.getMenuShortcutKeyMask()),
2853             "Undo");
2854      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
2855                                    toolkit.getMenuShortcutKeyMask()),
2856             "Redo");
2857
2858      am.put("Undo", new AbstractAction() {
2859          final static long serialVersionUID = 42;
2860          @Override
2861          public void actionPerformed(ActionEvent e) {
2862            try {
2863              if(undoManager.canUndo()) {
2864                undoManager.undo();
2865              }
2866            } catch(CannotUndoException exp) {
2867              exp.printStackTrace();
2868            }
2869          }
2870        });
2871      am.put("Redo", new AbstractAction() {
2872          final static long serialVersionUID = 42;
2873          @Override
2874          public void actionPerformed(ActionEvent e) {
2875            try {
2876              if(undoManager.canRedo()) {
2877                undoManager.redo();
2878              }
2879            } catch(CannotUndoException exp) {
2880              exp.printStackTrace();
2881            }
2882          }
2883        });
2884    }
2885  } // static class UndoableTextArea extends JTextArea
2886
2887  /**
2888   * An undoable text edit window
2889   *
2890   * This is the base class for all of the text windows. Since the
2891   * layout is BorderLayout (the default) clients can add buttons or
2892   * other things around the text window. The variable textArea can be
2893   * used to add listeners to the text area. The number of undos or
2894   * redos is unlimited.
2895   */
2896  static class EditWindow extends JFrame {
2897    final static long serialVersionUID = 42;
2898
2899    ArrayList<JFrame> dependents = new ArrayList<JFrame>();
2900    JTextArea textArea;
2901
2902    /**
2903     * Make a text edit window with given title and
2904     * contents. Dependent windows will be closed. The caller is
2905     * responsible for displaying the window and adding any
2906     * appropriate listeners.
2907     *
2908     * @param title the title of the window
2909     * @param contents the initial contents of the window
2910     */
2911    EditWindow(String title, String contents) {
2912      super(title);
2913      addWindowListener(new WindowAdapter() {
2914          @Override
2915          public void windowClosing(WindowEvent e) {
2916            if(--windowCount == 0) {
2917              finish();
2918            }
2919            dispose();
2920          }
2921        });
2922      ++windowCount;
2923      textArea = new UndoableTextArea(contents);
2924      textArea.setLineWrap(true);
2925      textArea.setWrapStyleWord(true);
2926      textArea.addMouseListener(new MouseAdapter() {
2927          @Override
2928          public void mouseClicked(MouseEvent e) {
2929            if(e.getButton() == 3) {
2930              JFrame findReplace = new FindReplace(EditWindow.this);
2931              EditWindow.this.dependents.add(findReplace);
2932              findReplace.pack();
2933              findReplace.setVisible(true);
2934            }
2935          }
2936        });
2937      JScrollPane areaScrollPane = new JScrollPane(textArea);
2938      setPreferredSize(new Dimension(600, 300));
2939      getContentPane().add(areaScrollPane);
2940    }
2941
2942    /**
2943     * Wrap lines that are too long. Insert prefix before broken
2944     * lines. Delete (some) trailing whitespace
2945     *
2946     * @param text the text to wrap lines in
2947     * @param maxLineLength the maximum line length desired
2948     * @param prefix the prefix for lines that had to be split
2949     * @return a String with all lines wrapped
2950     */
2951    String wrapLines(String text, int maxLineLength, String prefix) {
2952      int maxLength = maxLineLength;
2953      int shortLength = maxLineLength - prefix.length();
2954      StringBuilder newText = new StringBuilder();
2955      int textLength = text.length();
2956      while(textLength > 0 && (text.charAt(textLength - 1) == '\n' ||
2957                               text.charAt(textLength - 1) == ' ')) {
2958        --textLength;
2959      }
2960      text = text.substring(0, textLength) + '\n';
2961      int end;
2962
2963      for(int start = 0 ; start < textLength ; start = end) {
2964        end = text.indexOf('\n', start) + 1;
2965        if(end == -1) end = textLength;
2966        int lineLength = end - start - 1;
2967        if(lineLength <= maxLength) {
2968          newText.append(text.substring(start, end));
2969          maxLength = maxLineLength;
2970        } else {
2971          end = text.lastIndexOf(' ', start + maxLength);
2972          if(end <= start) end = start + maxLength;
2973          newText.append(text.substring(start, end));
2974          newText.append('\n');
2975          newText.append(prefix);
2976          maxLength = shortLength;
2977        }
2978      }
2979      return newText.toString();
2980    }
2981
2982    /**
2983     * Print the contents of the EditWindow.
2984     * //////////// should be in separate thread.
2985     *
2986     * @param title The title on each page
2987     */
2988    void print(String title) {
2989      MessageFormat footer = new MessageFormat("Page - {0}");
2990      try {
2991        JTextArea ta = new JTextArea(wrapLines(textArea.getText(), 80, ">>>>") + "\n "); // needed to print last line
2992        ta.print(new MessageFormat(title), footer);
2993      } catch(PrinterException e) {
2994        System.err.println(e);
2995      }
2996    }
2997  } // static class EditWindow extends JFrame
2998
2999  /**
3000   * Read the file at MyPath and return it as a String.
3001   *
3002   * @param path the path to the file to be read
3003   * @return the contents of the file as a String
3004   */
3005  static String readPath(MyPath path) {
3006    long length = path.size();
3007    if(length > 1000000) return ""; //////// or null ?????????
3008    byte[] buffer = new byte[(int)length];
3009    path.readFile(buffer);
3010    String s = new String(buffer, fileCharset);
3011    return s;
3012  }
3013
3014  /**
3015   * Write the contents of a String to the file at MyPath.
3016   *
3017   * @param path the path to the file to be written
3018   * @param s the String to be written out
3019   */
3020  static void writePath(MyPath path, String s) {
3021    byte[] buffer = s.getBytes(fileCharset);
3022    path.writeFile(buffer);
3023  }
3024
3025  /**
3026   * Make a file edit window for editing files.
3027   *
3028   * The Read File button rereads the file and replaces the window
3029   * contents with the contents of the file. This operation is
3030   * undoable.
3031   *
3032   * The Write File button writes the window contents back to the
3033   * file.
3034   *
3035   * The Print button prints the edit window to a printer.
3036   *
3037   * @param path the Path to the file to be edited
3038   */
3039  void makeFileEditWindow(MyPath path) {
3040    String contents = readPath(path);
3041    EditWindow editWindow = new EditWindow(path.fullName(), contents);
3042    JTextArea textArea = editWindow.textArea;
3043    JPanel inner = new JPanel(new BorderLayout());
3044    JPanel btns = new JPanel(new GridLayout(1,3));
3045    JLabel status = new JLabel("(status line)");
3046    btns.add(new JButton("Read File") {
3047        final static long serialVersionUID = 42;
3048
3049        {
3050          addActionListener(new ActionListener() {
3051              @Override
3052              public void actionPerformed(ActionEvent e) {
3053                String s = readPath(path);
3054                if(s != null) {
3055                  textArea.setText(s);
3056                  textArea.setCaretPosition(0);
3057                  textArea.grabFocus();
3058                }
3059              }
3060            });
3061        }
3062      });
3063    btns.add(new JButton("Write File") {
3064        final static long serialVersionUID = 42;
3065
3066        {
3067          addActionListener(new ActionListener() {
3068              @Override
3069              public void actionPerformed(ActionEvent e) {
3070                writePath(path, textArea.getText());
3071                textArea.grabFocus();
3072              }
3073            });
3074        }
3075      });
3076    btns.add(new JButton("Print") {
3077        final static long serialVersionUID = 42;
3078
3079        {
3080          addActionListener(new ActionListener() {
3081              @Override
3082              public void actionPerformed(ActionEvent e) {
3083                editWindow.print(path.fullName());
3084              }
3085            });
3086        }
3087      });
3088    textArea.addCaretListener(new CaretListener() {
3089        @Override
3090        public void caretUpdate(CaretEvent e) {
3091          try {
3092            int lwb = textArea.getLineOfOffset(textArea.getSelectionStart());
3093            int upb = textArea.getLineOfOffset(textArea.getSelectionEnd());
3094            String s;
3095            if(lwb++ == upb++) {
3096              status.setText("line:  " + lwb);
3097            } else {
3098              status.setText("lines: " + lwb + "-" + upb);
3099            }
3100          } catch(BadLocationException ex) {
3101            ex.printStackTrace();
3102          }
3103        }
3104      });
3105    inner.add(btns, BorderLayout.NORTH);
3106    inner.add(status, BorderLayout.SOUTH);
3107    editWindow.getContentPane().add(inner, BorderLayout.SOUTH);
3108    editWindow.setLocationByPlatform(true);
3109    editWindow.pack();
3110    editWindow.setVisible(true);
3111  } // makeFileEditWindow(MyPath path)
3112
3113  static class MyComboBox extends JComboBox<MyPath> {
3114    final static long serialVersionUID = 42;
3115
3116    MyPath selectedPath = root; /////////////////////////
3117
3118    class MyComboBoxModel extends DefaultComboBoxModel<MyPath> {
3119      final static long serialVersionUID = 42;
3120
3121      ArrayList<MyPath> comboBoxArray = new ArrayList<MyPath>();
3122
3123      /*
3124       *  Create an empty model that will use the specified Comparator
3125       */
3126      MyComboBoxModel(MyPath path) {
3127        super();
3128        comboBoxArray.add(path); /////////////////////////
3129      }
3130
3131      @Override
3132      public void addElement(MyPath element) {
3133        insertElementAt(element, comboBoxArray.size());
3134      }
3135
3136      @Override
3137      public MyPath getElementAt(int index) {
3138        return comboBoxArray.get(index);
3139      }
3140
3141      @Override
3142      public int getIndexOf(Object anObject) {
3143        return comboBoxArray.indexOf(anObject);
3144      }
3145
3146      @Override
3147      public Object getSelectedItem() {
3148        return selectedPath;
3149      }
3150
3151      @Override
3152      public int getSize() {
3153        return comboBoxArray.size();
3154      }
3155
3156      @Override
3157      public void insertElementAt(MyPath element, int index) {
3158        comboBoxArray.add(index, element);
3159        fireIntervalAdded(this, index, index);
3160      }
3161
3162      @Override
3163      public void removeAllElements() {
3164        if(comboBoxArray.size() > 0) {
3165          int firstIndex = 0;
3166          int lastIndex = comboBoxArray.size() - 1;
3167          comboBoxArray.clear();
3168          fireIntervalRemoved(this, firstIndex, lastIndex);
3169        }
3170      }
3171
3172      @Override
3173      public void removeElement(Object anObject) {
3174        int index = comboBoxArray.indexOf(anObject);
3175        if(index != -1) {
3176          removeElementAt(index);
3177          fireIntervalRemoved(this, index, index);
3178        }
3179      }
3180
3181      @Override
3182      public void removeElementAt(int index) {
3183        comboBoxArray.remove(index);
3184        fireIntervalRemoved(this, index, index);
3185      }
3186
3187      @Override
3188      public void setSelectedItem(Object anObject) {
3189        if((selectedPath != null && !selectedPath.equals(anObject)) ||
3190            selectedPath == null && anObject != null) {
3191          selectedPath = (MyPath)anObject;
3192          fireContentsChanged(this, -1, -1);
3193        }
3194      }
3195    } // class MyComboBoxModel extends DefaultComboBoxModel<MyPath>
3196    MyComboBoxModel myComboBoxModel = new MyComboBoxModel(selectedPath);
3197
3198    class MyComboBoxEditor implements ComboBoxEditor {
3199      final static long serialVersionUID = 42;
3200
3201      JTextField editor;
3202
3203      MyComboBoxEditor() {
3204        setOpaque(true);
3205        editor = new JTextField(selectedPath.fullName());
3206        editor.setFocusTraversalKeysEnabled(false); // allow VK_TAB events
3207        editor.addActionListener(new ActionListener() {
3208            @Override
3209            public void actionPerformed(ActionEvent e) {
3210              // canonize file name and set selectedPath
3211              try {
3212                selectedPath = stringToMyPath(editor.getText());
3213                while(!selectedPath.exists() || !selectedPath.isDirectory()) {
3214                  selectedPath = selectedPath.getParent();
3215                  if(selectedPath == null) {
3216                    selectedPath = root;
3217                    break;
3218                  }
3219                }
3220              } catch(NullPointerException ex) {
3221                System.err.println("caught null pointer"); /////////
3222                ex.printStackTrace();
3223                selectedPath = root;
3224              }
3225              String name = selectedPath.fullName();
3226              editor.setText(name);
3227              //setSelectedItem(selectedPath);
3228              //selectFromPath((MyPath)selectionBox.getSelectedItem(), true);
3229            }
3230          });
3231        editor.addKeyListener(new KeyAdapter() {
3232            @Override
3233            public void keyTyped(KeyEvent event) {
3234              if(event.getKeyChar() == KeyEvent.VK_TAB) {
3235                String s = editor.getText();
3236                if(s.length() == 0) {
3237                  selectedPath = root;
3238                  editor.setText(selectedPath.fullName());
3239                  return;
3240                }
3241                String candidate = s;
3242                MyPath parent;
3243                ArrayList<MyPath> childPaths;
3244                if(s.charAt(s.length() - 1) == '/') {
3245                  parent = stringToMyPath(s);
3246                  if(!parent.exists()) return;
3247                  childPaths = parent.getTreeChildren();
3248                } else {
3249                  parent = stringToMyPath(s).getParent();
3250                  if(!parent.exists()) return;
3251                  childPaths = parent.getTreeChildren();
3252                }
3253                Iterator<MyPath> iterator = childPaths.iterator();
3254                while(iterator.hasNext()) {
3255                  MyPath directoryPath = iterator.next();
3256                  String child = directoryPath.fullName();
3257                  if(child.length() < s.length()) continue;
3258                  if(!s.equals(child.substring(0, s.length()))) continue;
3259                  candidate = child + '/';
3260                  break;
3261                }
3262                int parentLength = parent.fullName().length();
3263                w: while(iterator.hasNext()) {
3264                  MyPath directoryPath = iterator.next();
3265                  String child = directoryPath.fullName() + '/';
3266                  if(child.length() < s.length()) continue;
3267                  int min = Math.min(child.length(), candidate.length());
3268                  for(int i = parentLength; i < min; ++i) {
3269                    if(child.charAt(i) != candidate.charAt(i)) {
3270                      if(i < s.length()) continue w;
3271                      candidate = candidate.substring(0, i);
3272                      break;
3273                    }
3274                  }
3275                }
3276                editor.setText(candidate);
3277                selectedPath = stringToMyPath(candidate);
3278                event.consume(); //////// do I need this?
3279              } else {
3280                //super.keyTyped(event); /////////// ???????????????
3281              }
3282            }
3283          });
3284      }
3285
3286      @Override
3287      public void addActionListener(ActionListener l) {
3288        listenerList.add(ActionListener.class, l);
3289      }
3290
3291      @Override
3292      public Component getEditorComponent() {
3293        return editor;
3294      }
3295
3296      @Override
3297      public Object getItem() {
3298        return selectedPath;
3299      }
3300
3301      @Override
3302      public void removeActionListener(ActionListener l) {
3303        listenerList.remove(ActionListener.class, l);
3304      }
3305
3306      @Override
3307      public void selectAll() {
3308      }
3309
3310      @Override
3311      public void setItem(Object newValue) {
3312        MyPath path = selectedPath = (MyPath) newValue;
3313        if(newValue != null) {
3314          editor.setText(path.fullName());
3315          selectedPath = path;
3316        }
3317      }
3318    } // class MyComboBoxEditor implements ComboBoxEditor
3319    MyComboBoxEditor myComboBoxEditor = new MyComboBoxEditor();
3320
3321    class MyComboBoxRenderer extends JLabel
3322      implements ListCellRenderer<MyPath> {
3323      final static long serialVersionUID = 42;
3324
3325      MyComboBoxRenderer() {
3326        setOpaque(true);
3327        //setHorizontalAlignment(CENTER);
3328        //setVerticalAlignment(CENTER);
3329      }
3330      /*
3331       * This method finds the image and text corresponding
3332       * to the selected value and returns the label, set up
3333       * to display the text and image.
3334       */
3335      @Override
3336      public Component
3337        getListCellRendererComponent(JList<? extends MyPath> list,
3338                                     MyPath path,
3339                                     int index,
3340                                     boolean isSelected,
3341                                     boolean cellHasFocus) {
3342        // the index is -1 when rendering the ConboBox itself
3343        if(isSelected) {
3344          setBackground(list.getSelectionBackground());
3345          setForeground(list.getSelectionForeground());
3346          if(index >= 0) {
3347            list.setToolTipText(path.fullName());
3348          }
3349        } else {
3350          setBackground(list.getBackground());
3351          setForeground(list.getForeground());
3352        }
3353        if(index < 0) {
3354          setText(path.fullName());
3355        } else {
3356          setText(path.fullName());
3357        }
3358        return this;
3359      }
3360    } // class MyComboBoxRenderer extends JLabel
3361    MyComboBoxRenderer myComboBoxRenderer = new MyComboBoxRenderer();
3362
3363    MyComboBox(MyPath path) {
3364      setMaximumRowCount(10);
3365      selectedPath = path;
3366      setModel(myComboBoxModel);
3367      setEditor(myComboBoxEditor);
3368      setRenderer(myComboBoxRenderer);
3369      setEditable(true);
3370
3371      // Just for testing - should be added by client ????????????
3372      addPopupMenuListener(new PopupMenuListener() {
3373          @Override
3374          public void popupMenuCanceled(PopupMenuEvent e) {
3375          }
3376
3377          @Override
3378          public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
3379          }
3380
3381          @Override
3382          public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
3383            JTextField textEditor = myComboBoxEditor.editor;
3384            String text = textEditor.getText();
3385            if(textEditor.getText().length() == 0
3386               || selectedPath == root && text.equals(rootName)) {
3387              textEditor.setText(rootName);
3388              myComboBoxModel.removeAllElements();
3389              TreeSet<TableData> fileRoots = getRootPaths(true);
3390              ArrayList<MyPath> roots = new ArrayList<MyPath>();
3391              for(TableData p : fileRoots) {
3392                roots.add(p.path); //////////
3393              }
3394              selectedPath.treeChildren = roots;
3395              for(MyPath child : roots) {
3396                myComboBoxModel.addElement(child);
3397              }
3398              for(String entry : options.get(selectionsKey)) {
3399                addItem(stringToMyPath(entry));
3400              }
3401            } else {
3402              try {
3403                selectedPath = stringToMyPath(text);
3404                while(!selectedPath.exists() || !selectedPath.isDirectory()) {
3405                  selectedPath = selectedPath.getParent();
3406                  if(selectedPath == null || selectedPath == root) {
3407                    selectedPath = root;
3408                    textEditor.setText(rootName);
3409                    break;
3410                  }
3411                }
3412              } catch(NullPointerException ex) {
3413                System.err.println("caught null pointer"); /////////
3414                selectedPath = root;
3415              }
3416              textEditor.setText(selectedPath.fullName());
3417              myComboBoxModel.removeAllElements();
3418              if(selectedPath != null) {
3419                if(selectedPath == root) {
3420                  TreeSet<TableData> fileRoots = getRootPaths(true);
3421                  ArrayList<MyPath> roots = new ArrayList<MyPath>();
3422                  for(TableData p : fileRoots) {
3423                    roots.add(p.path); //////////
3424                  }
3425                  selectedPath.treeChildren = roots;
3426                  for(MyPath child : roots) {
3427                    myComboBoxModel.addElement(child);
3428                  }
3429                  return;
3430                }
3431                for(MyPath child : selectedPath.getTreeChildren()) {
3432                  myComboBoxModel.addElement(child);
3433                }
3434              }
3435            }
3436          }
3437        });
3438    } // MyComboBox(MyPath path)
3439  } // static class MyComboBox extends JComboBox<MyPath>
3440
3441  /**
3442   * This class is just a data structure with the table information
3443   */
3444  static class TableData /* implements Comparable<TableData> */ {
3445    MyPath path; // the file path / name
3446    long size; // the size of the file
3447    long mtime; // the modification date / time
3448
3449    /**
3450     * initialize a TableData
3451     *
3452     * @param path the full path
3453     */
3454    TableData(MyPath path) {
3455      this.path = path;
3456      if(path.isLink()) {
3457        size = -1;
3458      } else {
3459        size = path.size();
3460      }
3461      mtime = path.getMTime();
3462    }
3463
3464    @Override
3465    public String toString() {
3466      return path.fullName();
3467    }
3468  } // static class TableData
3469
3470  static class TableDataComparator implements Comparator<TableData> {
3471    /**
3472     * This comparator puts directories first and sorts names
3473     * differing only in case together.
3474     */
3475    @Override
3476    public int compare(TableData d1, TableData d2) {
3477      if(d1.path.isDirectory()) {
3478        if(d2.path.isDirectory()) {
3479          String s1 = flatten(d1.path.uri.getRawPath());
3480          String s2 = flatten(d2.path.uri.getRawPath());
3481          int c = s1.compareToIgnoreCase(s2);
3482          if(c != 0) return c;
3483          return s1.compareTo(s2);
3484        } else {
3485          return -1;
3486        }
3487      } else {
3488        if(d2.path.isDirectory()) {
3489          return 1;
3490        }
3491      }
3492      String s1 = flatten(d1.path.uri.getRawPath());
3493      String s2 = flatten(d2.path.uri.getRawPath());
3494      int c = s1.compareToIgnoreCase(s2);
3495      if(c != 0) return c;
3496      return s1.compareTo(s2);
3497    }
3498  } // static class TableDataComparator implements Comparator<TableData>
3499  static TableDataComparator tableDataComparator = new TableDataComparator();
3500
3501  static class DataNameComparator implements Comparator<TableData> {
3502    /**
3503     * This comparator respects case and does not treat directories
3504     * specially.
3505     */
3506    @Override
3507    public int compare(TableData d1, TableData d2) {
3508      String s1 = flatten(d1.path.toString());
3509      String s2 = flatten(d2.path.toString());
3510      int c = s1.compareToIgnoreCase(s2);
3511      if(c != 0) return c;
3512      return s1.compareTo(s2);
3513    }
3514  } // static class DataNameComparator implements Comparator<TableData>
3515  static DataNameComparator dataNameComparator = new DataNameComparator();
3516
3517  /**
3518   * This comparator of Paths puts directories first, then sorts
3519   * ignoring case, then considering case.
3520   */
3521  static class PathComparator implements Comparator<MyPath> {
3522    @Override
3523    public int compare(MyPath p1, MyPath p2) {
3524      boolean isDirectory1 = p1.isDirectory();
3525      boolean isDirectory2 = p2.isDirectory();
3526      if(isDirectory1 && !isDirectory2) return -1;
3527      if(!isDirectory1 && isDirectory2) return 1;
3528      String s1 = p1.uri.getRawAuthority();
3529      if(s1 == null) s1 = "";
3530      else s1 += '/';
3531      s1 = flatten(s1 + p1.uri.getRawPath());
3532      String s2 = p2.uri.getRawAuthority();
3533      if(s2 == null) s2 = "";
3534      else s2 += '/';
3535      s2 = flatten(s2 + p2.uri.getRawPath());
3536      int c = s1.compareToIgnoreCase(s2);
3537      if(c != 0) return c;
3538      return s1.compareTo(s2);
3539    }
3540  } // static class PathComparator implements Comparator<MyPath>
3541
3542  static PathComparator pathComparator = new PathComparator();
3543
3544  /**
3545   * This class is a pure data structure and represents a node in a
3546   * JTree.
3547   */
3548  static class MyTreeNode {
3549    MyPath myPath;
3550    MyTreeNode parent;
3551    ArrayList<MyTreeNode> children;
3552
3553    MyTreeNode(MyPath myPath, MyTreeNode parent) {
3554      this.myPath = myPath;
3555      this.parent = parent;
3556      children = null;
3557    }
3558
3559    public String toString() {
3560      return myPath.toString(); // needed for tree cell editing
3561    }
3562  }
3563
3564  static class TreeNodeComparator implements Comparator<MyTreeNode> {
3565    @Override
3566    public int compare(MyTreeNode t1, MyTreeNode t2) {
3567      return pathComparator.compare(t1.myPath, t2.myPath);
3568    }
3569  } // static class PathComparator implements Comparator<MyPath>
3570
3571  static TreeNodeComparator treeNodeComparator = new TreeNodeComparator();
3572
3573  /**
3574   * This class handles JTextField drag &amp; drop. Strings and files
3575   * can be dropped. For files only the file name without directory
3576   * information is retained. Dropping multiple files is not
3577   * supported.
3578   */
3579  static class MyTextTransferHandler extends TransferHandler {
3580    final static long serialVersionUID = 42;
3581
3582    @Override
3583    public boolean canImport(TransferHandler.TransferSupport info) {
3584      // we only import Strings and single Files
3585      try {
3586        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
3587          if(((List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)).size() == 1) {
3588            return true;
3589          } else {
3590            return false;
3591          }
3592        }
3593      } catch(UnsupportedFlavorException e) {
3594        System.err.println("UnsupportedFlavorException");
3595        return false;
3596      } catch(IOException e) {
3597        e.printStackTrace();
3598        return false;
3599      }
3600      if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
3601        return true;
3602      }
3603      return false;
3604    }
3605
3606    @Override
3607    public boolean importData(TransferHandler.TransferSupport info) {
3608      if(!canImport(info)) {
3609        return false;
3610      }
3611      JTextField tf = (JTextField)info.getComponent();
3612      String data;
3613      try {
3614        if(info.isDrop()) {
3615          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
3616            List<?> files = (List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
3617            if(files.size() != 1) return false; // can handle only one file
3618            data = files.get(0).toString();
3619            tf.setText(data);
3620          } else if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
3621            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
3622            if(info.isDrop()) {
3623              tf.setText(data);
3624            } else {
3625              tf.replaceSelection(data);
3626            }
3627          } else {
3628            return false;
3629          }
3630        } else {
3631          // paste
3632          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
3633            List<?> files = (List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
3634            if(files.size() != 1) return false; // can handle only one file
3635            data = files.get(0).toString();
3636            tf.setText(data);
3637          } else if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
3638            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
3639            tf.replaceSelection(data);
3640          }
3641        }
3642      } catch(UnsupportedFlavorException e) {
3643        System.err.println("UnsupportedFlavorException");
3644        return false;
3645      } catch(IOException e) {
3646        e.printStackTrace();
3647        return false;
3648      }
3649      SwingUtilities.invokeLater(new Runnable() {
3650          @Override
3651          public void run() {
3652            tf.grabFocus();
3653          }
3654        });
3655      return true;
3656    }
3657
3658    @Override
3659    public int getSourceActions(JComponent c) {
3660      return COPY_OR_MOVE;
3661    }
3662
3663    protected Transferable createTransferable(JComponent c) {
3664      JTextField source = (JTextField)c;
3665      int start = source.getSelectionStart();
3666      int end = source.getSelectionEnd();
3667      if(start == end) {
3668        return null;
3669      }
3670      String data = source.getSelectedText();
3671      return new StringSelection(data);
3672    }
3673
3674    @Override
3675    public void exportDone(JComponent c, Transferable t, int action) {
3676      if(action != MOVE) {
3677        return;
3678      }
3679      ((JTextComponent)c).replaceSelection("");
3680    }
3681  } // class MyTextTransferHandler extends TransferHandler
3682
3683  static class MyUserInfo implements UserInfo, UIKeyboardInteractive {
3684    @Override
3685    public String getPassword() { return passwd; }
3686    @Override
3687    public boolean promptYesNo(String str) {
3688      Object[] options = { "yes", "no" };
3689      int foo = JOptionPane.showOptionDialog(null,
3690                                             str,
3691                                             "Warning",
3692                                             JOptionPane.DEFAULT_OPTION,
3693                                             JOptionPane.WARNING_MESSAGE,
3694                                             null, options, options[0]);
3695      return foo == 0;
3696    }
3697
3698    String passwd;
3699    JTextField passwordField = (JTextField)new JPasswordField(20);
3700
3701    {
3702      passwordField.addHierarchyListener(new HierarchyListener() {
3703          @Override
3704          public void hierarchyChanged(HierarchyEvent e) {
3705            Component c = e.getComponent();
3706            if(c.isShowing() && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
3707              Window toplevel = SwingUtilities.getWindowAncestor(c);
3708              toplevel.addWindowFocusListener(new WindowAdapter() {
3709                  @Override
3710                  public void windowGainedFocus(WindowEvent e) {
3711                    c.requestFocus();
3712                  }
3713                });
3714            }
3715          }
3716        });
3717    }
3718
3719    @Override
3720    public String getPassphrase() {
3721      return null;
3722    }
3723
3724    @Override
3725    public boolean promptPassphrase(String message) {
3726      return true;
3727    }
3728
3729    @Override
3730    public boolean promptPassword(String message) {
3731      Object[] ob = { passwordField };
3732      int result = JOptionPane.showConfirmDialog(null, ob, message,
3733                                                 JOptionPane.OK_CANCEL_OPTION);
3734      if(result == JOptionPane.OK_OPTION) {
3735        passwd = passwordField.getText();
3736        return true;
3737      } else {
3738        return false;
3739      }
3740    }
3741
3742    @Override
3743    public void showMessage(String message) {
3744      JOptionPane.showMessageDialog(null, message);
3745    }
3746
3747    GridBagConstraints gbc  =
3748      new GridBagConstraints(0,0,1,1,1,1,
3749                             GridBagConstraints.NORTHWEST,
3750                             GridBagConstraints.NONE,
3751                             new Insets(0,0,0,0),0,0);
3752    private Container panel;
3753    @Override
3754    public String[] promptKeyboardInteractive(String destination,
3755                                              String name,
3756                                              String instruction,
3757                                              String[] prompt,
3758                                              boolean[] echo) {
3759      panel = new JPanel();
3760      panel.setLayout(new GridBagLayout());
3761
3762      gbc.weightx = 1.0;
3763      gbc.gridwidth = GridBagConstraints.REMAINDER;
3764      gbc.gridx = 0;
3765      panel.add(new JLabel(instruction), gbc);
3766      ++gbc.gridy;
3767
3768      gbc.gridwidth = GridBagConstraints.RELATIVE;
3769
3770      JTextField[] texts = new JTextField[prompt.length];
3771      for(int i = 0; i < prompt.length; ++i) {
3772        gbc.fill = GridBagConstraints.NONE;
3773        gbc.gridx = 0;
3774        gbc.weightx = 1;
3775        panel.add(new JLabel(prompt[i]),gbc);
3776
3777        gbc.gridx = 1;
3778        gbc.fill = GridBagConstraints.HORIZONTAL;
3779        gbc.weighty = 1;
3780        if(echo[i]) {
3781          texts[i] = new JTextField(20);
3782        } else {
3783          texts[i] = new JPasswordField(20);
3784        }
3785        panel.add(texts[i], gbc);
3786        ++gbc.gridy;
3787      }
3788      texts[0].addHierarchyListener(new HierarchyListener() {
3789          @Override
3790          public void hierarchyChanged(HierarchyEvent e) {
3791            Component c = e.getComponent();
3792            if(c.isShowing() && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
3793              Window toplevel = SwingUtilities.getWindowAncestor(c);
3794              toplevel.addWindowFocusListener(new WindowAdapter() {
3795                  @Override
3796                  public void windowGainedFocus(WindowEvent e) {
3797                    c.requestFocus();
3798                  }
3799                });
3800            }
3801          }
3802        });
3803      if(JOptionPane.showConfirmDialog(null, panel,
3804                                       destination + ": " + name,
3805                                       JOptionPane.OK_CANCEL_OPTION,
3806                                       JOptionPane.QUESTION_MESSAGE)
3807         == JOptionPane.OK_OPTION) {
3808        String[] response = new String[prompt.length];
3809        for(int i = 0; i < prompt.length; ++i) {
3810          response[i] = texts[i].getText();
3811        }
3812        passwd = response[0];
3813        return response;
3814      } else {
3815        return null;  // cancel
3816      }
3817    }
3818  }
3819
3820  /**
3821   * Perform a diff between two files. Show the results in an
3822   * EditWindow.
3823   *
3824   * @param path1 the MyPath for the first file
3825   * @param path2 the MyPath for the second file
3826   */
3827  static void diff(MyPath path1, MyPath path2) {
3828    class Diff {
3829      // Space optimized function to find the length of the longest
3830      // common subsequence of substring 'X[0:m-1]' and 'Y[0:n-1]'
3831      List<String> lines1 = path1.readAllLines();
3832      List<String> lines2 = path2.readAllLines();
3833      int m = lines1.size();
3834      int n = lines2.size();
3835      int[] lenLCS = new int[m+1];
3836      int[] locLCS = new int[m+1];
3837      int[] f = new int[n+1];
3838      int[] b = new int[n+1];
3839      StringBuilder sb = new StringBuilder();
3840
3841      Diff() {
3842        int max = LCSpart(0, m + 1, 0, n);
3843        int left1 = 0;
3844        int right1 = 0;
3845        for(int i = 0; i <= m; ++i) {
3846          if(i == m || lenLCS[i] < lenLCS[i + 1]) {
3847            int left2 = i;
3848            int right2 = i != m ? locLCS[i+1]-1 : n;
3849            String left = (left1+1) >= left2 ? "" + left2
3850              : (left1+1) + "," + left2;
3851            String right = (right1+1) >= right2 ? "" + right2
3852              : (right1+1) + "," + right2;
3853            if(left1 != left2) {
3854              if(right1 != right2) {
3855                sb.append(left + 'c' + right + '\n');
3856                for(int ii = left1; ii < left2; ++ii) {
3857                  sb.append("< " + lines1.get(ii) + '\n');
3858                }
3859                sb.append("---" + '\n');
3860                for(int ii = right1; ii < right2; ++ii) {
3861                  sb.append("> " + lines2.get(ii) + '\n');
3862                }
3863              } else {
3864                sb.append(left + 'd' + right + '\n');
3865                for(int ii = left1; ii < left2; ++ii) {
3866                  sb.append("< " + lines1.get(ii) + '\n');
3867                }
3868              }
3869            } else {
3870              if(right1 != right2) {
3871                sb.append(left + 'a' + right + '\n');
3872                for(int ii = right1; ii < right2; ++ii) {
3873                  sb.append("> " + lines2.get(ii) + '\n');
3874                }
3875              }
3876            }
3877            left1 = left2 + 1;
3878            right1 = right2 + 1;
3879          }
3880        }
3881        EditWindow editWindow
3882          = new EditWindow(path1.fullName() + " <> " + path2.fullName(),
3883                           sb.toString());
3884        editWindow.pack();
3885        editWindow.setVisible(true);
3886      } // diff
3887
3888      /**
3889       * Do a matching in a rectangle
3890       *
3891       * @param lom lower bound for m
3892       * @param him upper bound for m
3893       * @param lin lower bound for n
3894       * @param him upper bound for n
3895       */
3896      int LCSpart(int lom, int him, int lon, int hin) {
3897        int midm = (lom + him)/2;
3898        if(midm == lom) return -1;
3899        forward(lom, midm, lon, hin); // uses forward array f[]
3900        backward(midm, him, lon, hin); // uses backward array b[]
3901        int maxLCS = -1;
3902        int maxj = 0;
3903        for(int j = lon; j <= hin; ++j) {
3904          int len = f[j] + b[j];
3905          if(len > maxLCS) {
3906            maxLCS = len;
3907            maxj = j;
3908          }
3909        }
3910        locLCS[midm] = maxj;
3911        lenLCS[midm] = f[maxj] + lenLCS[lom];
3912        LCSpart(lom, midm, lon, maxj);
3913        int r = LCSpart(midm, him, maxj, hin);
3914        return r < 0 ? maxj : r;
3915      }
3916
3917      /**
3918       * Do a forward scan in a rectangle
3919       *
3920       * @param lom lower bound for m
3921       * @param him upper bound for m
3922       * @param lin lower bound for n
3923       * @param him upper bound for n
3924       */
3925      int forward(int lom, int him, int lon, int hin) {
3926        int prev;
3927        for (int i = lom; i <= him; ++i) {
3928          prev = f[lon];
3929          for (int j = lon; j <= hin; ++j) {
3930            int backup = f[j];
3931            if (i == lom || j == lon) {
3932              f[j] = 0;
3933            } else {
3934              if(lines1.get(i - 1).equals(lines2.get(j - 1))) {
3935                f[j] = prev + 1;
3936              } else {
3937                f[j] = Integer.max(f[j], f[j - 1]);
3938              }
3939            }
3940            prev = backup;
3941          }
3942        }
3943        // LCS will be the last entry in the lookup table
3944        return f[hin];
3945      }
3946
3947      /**
3948       * Do a backward scan in a rectangle
3949       *
3950       * @param lom lower bound for m
3951       * @param him upper bound for m
3952       * @param lin lower bound for n
3953       * @param him upper bound for n
3954       */
3955      int backward(int lom, int him, int lon, int hin) {
3956        int prev;
3957        // fill the lookup table in a bottom-up manner
3958        for (int i = him; i >= lom; --i) {
3959          prev = b[hin];
3960          for (int j = hin; j >= lon; --j) {
3961            int backup = b[j];
3962            if (i == him || j == hin) {
3963              b[j] = 0;
3964            } else {
3965              if (i != m && lines1.get(i).equals(lines2.get(j))) {
3966                b[j] = prev + 1;
3967              } else {
3968                b[j] = Integer.max(b[j], b[j + 1]);
3969              }
3970            }
3971            prev = backup;
3972          }
3973        }
3974        // LCS will be the last entry in the lookup table
3975        return b[lon];
3976      }
3977    }
3978    new Diff(); // do the diff
3979  } // public static void diff(MyPath path1, MyPath path2)
3980
3981  static boolean windowsXPDSTHack = false;
3982  /**
3983   * This class reads all of the user options readfrom
3984   * optionPath. There are lists of remote servers (for a drop-down
3985   * selection list), size of initial window etc.
3986   *
3987   * The format of the ~/.filebrowserrc file is a section header in
3988   * square brackets [] followed by one line for each instance of the
3989   * option, possibly followed by a blank line (or end of file).
3990   *
3991   * The options variable is a map from option header names to an
3992   * ArrayList of options. This options variable may be updated during
3993   * the running of the program and written out by the user (in the
3994   * proper format).
3995   *
3996   * Current option categories are:
3997   * [remote hosts] // user@host or user@host:password(encrypted)
3998   *
3999   * MUST USE readAllBytes to ignore character set issues /////////////
4000   */
4001  static void readOptions() { ////fffffffffffff
4002    options.clear();
4003    try {
4004      List<String> lines = Files.readAllLines(optionPath, charSet);
4005      String type = "Heading";
4006      ArrayList<String> optionList = new ArrayList<String>();
4007      for(String line : lines) {
4008        if(line.length() > 0 && line.charAt(0) == '[') {
4009          if(optionList.size() != 0) options.put(type, optionList);
4010          type = line.substring(1, line.length() - 1);
4011          optionList = new ArrayList<String>();
4012        } else {
4013          if(line.length() != 0 && optionList.indexOf(line) == -1) {
4014            optionList.add(line);
4015          }
4016        }
4017      }
4018      if(optionList.size() != 0) options.put(type, optionList);
4019    } catch(IOException e) {
4020      System.err.println("Error reading options file");
4021      System.err.println(e);
4022    }
4023    // make sure there is an entry for selectionsKey
4024    if(options.get(selectionsKey) == null) {
4025      options.put(selectionsKey, new ArrayList<String>());
4026    }
4027    ArrayList<String> optionList = options.get("Settings");
4028    if(optionList != null) {
4029      for(String option : optionList) {
4030        int index = option.indexOf(':');
4031        if(index > 0 && index + 1 < option.length()) {
4032          String optionName = option.substring(0, index);
4033          String optionValue = option.substring(index + 1);
4034          //String optionValue = option.substring(index + 1).trim();
4035          switch(optionName) {
4036          case "WindowsXPDSTHack": {
4037            if(optionValue.equalsIgnoreCase("Yes")) windowsXPDSTHack = true;
4038          }
4039          }
4040        }
4041      }
4042    }
4043  } // static void readOptions()
4044
4045  static {
4046    readOptions();
4047  }
4048
4049  /**
4050   * This class writes all of the user options from optionPath. There
4051   * are lists of remote servers (for a drop-down selection list),
4052   * size of initial window etc.
4053   *
4054   * The format of the ~/.filebrowserrc file is a section header in
4055   * square brackets [] followed by one line for each instance of the
4056   * option, possibly followed by a blank line (or end of file).
4057   *
4058   * The options variable is a map from option header names to an
4059   * ArrayList of options. This options variable may be updated during
4060   * the running of the program and written out by the user (in the
4061   * proper format).
4062   *
4063   * Current option categories are: [remote hosts] // user@host or
4064   * user@host:password(encrypted)
4065   *
4066   * MUST USE writeAllBytes to ignore character set issues /////////////
4067   */
4068  static void writeOptions() {
4069    ArrayList<String> lines = new ArrayList<String>();
4070    for(Map.Entry<String, ArrayList<String>> pair : options.entrySet()) {
4071      ArrayList<String> values = pair.getValue();
4072      if(values.size() != 0) {
4073        String key = pair.getKey();
4074        lines.add('[' + key + ']');
4075        for(String value : values) {
4076          lines.add(value);
4077        }
4078        lines.add("");
4079      }
4080    }
4081    try {
4082      Files.write(optionPath, lines, charSet); ////ffffffffff
4083    } catch(IOException e) {
4084      System.err.println("Error writing options file");
4085      System.err.println(e);
4086    }
4087  } // static void writeOptions()
4088
4089  static final char lo = ' ';
4090  static final char hi = '~';
4091
4092  /**
4093   * Encrypts a password for storing in options file.
4094   *
4095   * @param seed a seed for encryption
4096   * @param data a string for encryption
4097   * @return the encrypted string
4098   */
4099  static String encrypt(String seed, String data) {
4100    char[] chars = data.toCharArray();
4101    for(int i = 0; i < chars.length; ++i) {
4102      char c = chars[i];
4103      if(c >= lo && c <= hi) {
4104        int subst = 23*i + 7
4105          + seed.charAt(i % seed.length())
4106          + 2*seed.charAt(seed.length() - 1  - (i % seed.length()));
4107        int code = (chars[i] - lo + subst) % (hi - lo + 1) + lo;
4108        if(code < lo) code += hi - lo + 1;
4109        chars[i] = (char)code;
4110      }
4111    }
4112    return new String(chars);
4113  }
4114
4115  /**
4116   * Decrypts a password for storing in options file.
4117   *
4118   * @param seed a seed for decryption
4119   * @param data a string for decryption
4120   * @return the decrypted string
4121   */
4122  static String decrypt(String seed, String data) {
4123    char[] chars = data.toCharArray();
4124    for(int i = 0; i < chars.length; ++i) {
4125      char c = chars[i];
4126      if(c >= lo && c <= hi) {
4127        int subst = 23*i + 7
4128          + seed.charAt(i % seed.length())
4129          + 2*seed.charAt(seed.length() - 1  - (i % seed.length()));
4130        int code = (chars[i] - lo - subst) % (hi - lo + 1) + lo;
4131        if(code < lo) code += hi - lo + 1;
4132        chars[i] = (char)code;
4133      }
4134    }
4135    return new String(chars);
4136  }
4137
4138  MyPath selectedPath;
4139
4140  MyPath endEditing(MyComboBox comboBox) {
4141    MyComboBox.MyComboBoxEditor comboBoxEditor = comboBox.myComboBoxEditor;
4142    JTextField editor = comboBoxEditor.editor;
4143    MyPath path;
4144    try {
4145      path = stringToMyPath(editor.getText());
4146      while(!path.exists() || !path.isDirectory()) {
4147        path = path.getParent();
4148        if(path == null) {
4149          path = root;
4150          break;
4151        }
4152      }
4153    } catch(NullPointerException ex) {
4154      System.err.println("caught null pointer"); /////////
4155      path = root;
4156    }
4157    ////editor.setText(path.fullName()); //**********************//////
4158    return path;
4159  } // MyPath endEditing(MyComboBox comboBox)
4160
4161  class Browser extends JFrame {
4162    final static long serialVersionUID = 42;
4163
4164    JComboBox<MyPath> selectionBox = new MyComboBox(root);
4165    JTextField editor = (JTextField)selectionBox.getEditor().getEditorComponent();
4166    {
4167      selectionBox.setPreferredSize(new Dimension(200, 15)); //// 15 ?????
4168    }
4169
4170    JSplitPane splitPane; // the split pane - tree on left, table on right
4171    int splitLocation; // the remembered location of the split
4172
4173    JTree tree; // The left side of the JSplitPane
4174
4175    // tree watcher watches all expanded nodes of a tree
4176    WatchService treeWatcher;
4177    HashMap<WatchKey,TreePath> treeKeys = new HashMap<WatchKey,TreePath>();
4178    // table watcher watches only one directory
4179    WatchService tableWatcher;
4180    WatchKey tableKey = null;
4181    MyPath tablePath = null;
4182    JTable table; // The right side of the JSplitPane
4183    ArrayList<TableData> tableData = new ArrayList<TableData>();
4184    MyTreeModel myTreeModel = new MyTreeModel();
4185    MyTableModel myTableModel = new MyTableModel();
4186    ArrayList<String> tableColumnNames = new ArrayList<String>();
4187    { tableColumnNames.add("Name");
4188      tableColumnNames.add("Size");
4189      tableColumnNames.add("Date Modified");
4190    } ///// hack until user-selectable properties - if ever
4191
4192    boolean showDotFiles = false;
4193    boolean listFiles = true;
4194
4195    JTextField currentDirectory = new JTextField();
4196
4197    ArrayList<MyPath> history = new ArrayList<MyPath>();
4198    int historyIndex = -1; // points to current directory
4199
4200    //static DataFlavor remotePathArrayFlavor
4201    //= new DataFlavor(ArrayList.class, "remotePathListFlavor");
4202
4203    MyPath getSelectedPath(TreePath treePath) {
4204      return ((MyTreeNode)treePath.getLastPathComponent()).myPath;
4205    }
4206
4207    /**
4208     * This method takes a Path and selects the corresponding tree
4209     * node and table entry (if not a directory). If the Path does not
4210     * refer to an actual directory or file in the tree / table then
4211     * it selects the deepest Path that does exist. The selection is
4212     * truncated to the current visible tree.
4213     *
4214     * should return valid selection if problems, e.g., first root
4215     *
4216     * @param myPath a file path to be selected
4217     * @param mark true if path to be pushed on undo stack
4218     */
4219    void selectFromPath(MyPath myPath, boolean mark) {
4220      if(myPath == null) { ///////////////////
4221        return;
4222      }
4223      //if(mark) System.out.println(myPath);
4224      if(mark) pushDirectoryInHistory(myPath);
4225      MyPath file = null;
4226      ArrayDeque<MyPath> pathStack = new ArrayDeque<MyPath>();
4227      while(!myPath.exists() && (myPath != root)) myPath = myPath.getParent();
4228      if(myPath.exists() && !myPath.isDirectory()) {
4229        file = myPath;
4230        myPath = myPath.getParent();
4231      }
4232      while(myPath != root && myPath != null) {
4233        pathStack.push(myPath);
4234        myPath = myPath.getParent();
4235      }
4236      if(myPath == null) myPath = root;
4237      TreePath treePath = rootTreePath;
4238      while(!pathStack.isEmpty()) {
4239        MyTreeNode node = ((MyTreeNode)treePath.getLastPathComponent());
4240        myPath = pathStack.pop(); // get next MyPath to enter in tree
4241        MyPath childPath = myPath;
4242        MyTreeNode parent = node;
4243        if(parent.children == null) {
4244          parent.children = new ArrayList<MyTreeNode>();
4245        }
4246        int lo = 0;
4247        int hi = parent.children.size();
4248        int c = 1;
4249        int index;
4250        found: {
4251          while(lo < hi) {
4252            int mid = (lo + hi) >>> 1;
4253            c = pathComparator.compare(parent.children.get(mid).myPath,
4254                                       childPath);
4255            if(c == 0) {
4256              index = mid;
4257              break found;
4258            }
4259            if(c > 0) {
4260              hi = mid;
4261            } else {
4262              lo = mid + 1;
4263            }
4264          } // end while
4265          parent.children.add(lo, new MyTreeNode(childPath, parent));
4266          index = lo;
4267          myTreeModel.nodesWereInserted(parent, index);
4268        } // break found goes after here
4269        MyTreeNode child = node.children.get(index);
4270        treePath = treePath.pathByAddingChild(child); // get next TreePath
4271      }
4272      tree.setSelectionPath(treePath);
4273      if(file != null) {
4274        for(int i = 0; i < tableData.size(); ++i) {
4275          if(tableData.get(i).path.toString().equals(file.toString())) {
4276            table.setRowSelectionInterval(i, i); // scroll to this entry
4277            table.scrollRectToVisible(table.getCellRect(i, 0, true));
4278            break;
4279          }
4280        }
4281      }
4282    }
4283
4284    /**
4285     * Regenerates the table data after notification that the table data
4286     * has changed.
4287     */
4288    void regenerateTable() {
4289      if(tablePath == null) {
4290        tableData = new ArrayList<TableData>(getRootPaths(true));
4291      } else {
4292        tableData = new ArrayList<TableData>(tablePath.getChildren(showDotFiles));
4293      }
4294      myTableModel.fireTableChanged(new TableModelEvent(myTableModel));
4295    }
4296
4297    /**
4298     * Pushes path into the file/directory stack. There could be a
4299     * problem with character sets.
4300     *
4301     * @param path the Path to be pushed onto the stack.
4302     */
4303    void pushDirectoryInHistory(MyPath path) {
4304      if(historyIndex >= 0 && history.get(historyIndex).equals(path)) return;
4305      ++historyIndex;
4306      while(history.size() > historyIndex) history.remove(history.size() - 1);
4307      history.add(path);
4308      //System.out.println(history);
4309    }
4310
4311    /**
4312     * MyTableModel gets data from and stores data into the containing
4313     * Table class. MyTableModel model allows drag &amp; drop and cell
4314     * editing.
4315     */
4316    class MyTableModel extends AbstractTableModel {
4317      final static long serialVersionUID = 42;
4318
4319      @Override
4320      public int getColumnCount() {
4321        return tableColumnNames.size();
4322      }
4323
4324      @Override
4325      public int getRowCount() {
4326        return tableData.size();
4327      }
4328
4329      @Override
4330      public String getColumnName(int col) {
4331        return tableColumnNames.get(col);
4332      }
4333
4334      /**
4335       * Method used to get data to display after
4336       * getTableCellRendererComponent converts it for displaying.
4337       */
4338      @Override
4339      public Object getValueAt(int row, int col) {
4340        TableData data = tableData.get(row);
4341        switch(col) {
4342        case 0: return data.path.toString(); ////////////
4343        case 1: {
4344          if(tableData.get(row).path.isLink()) {
4345            byte[] targetBytes = tableData.get(row).path.readLink();
4346            String target = bytesToString(targetBytes);
4347            return target;
4348          } else return data.size;
4349        }
4350        case 2:
4351          Calendar c = Calendar.getInstance();
4352          c.setTime(new Date(data.mtime));
4353          //c.setTimeInMillis(long millis);
4354          //c.get(Calendar.DST_OFFSET)
4355          Date date = new Date(data.mtime);
4356          return date.toString()/* + '|' + date.getTime()*/;
4357        default: throw new Error("Bad table column");
4358        }
4359      }
4360
4361      /*
4362       * JTable uses this method to determine the default renderer/
4363       * editor for each cell. If we didn't implement this method then
4364       * the editor might not be a String editor.
4365       *
4366       * This is correct even though the cells contain Paths. It is
4367       * fixed in setValueAt
4368       */
4369      @Override
4370      public Class<?> getColumnClass(int c) {
4371        return String.class; // all columns rendered as strings
4372      }
4373
4374      /**
4375       * Disable the default editor if not the first column or the cell
4376       * contains a directory (because of an apparent race
4377       * condition). Directories must be edited in the tree view.
4378       */
4379      @Override
4380      public boolean isCellEditable(int row, int col) {
4381        return col == 0 && !tableData.get(row).path.isDirectory()
4382          || col == 1 && tableData.get(row).path.isLink();
4383      }
4384
4385      /**
4386       * Apparently only used if cell is edited. We are using Strings as
4387       * the type of the table cell so must convert to Path and check
4388       * for equality. Must also check for duplicating another file in
4389       * the same directory.
4390       *
4391       * @param value new (edited) value of cell
4392       * @param row row of edited cell
4393       * @param col column of edited cell
4394       */
4395      @Override
4396      public void setValueAt(Object value, int row, int col) {
4397        if(row < tableData.size()) {
4398          if(col == 0) {
4399            String current = tableData.get(row).path.toString();
4400            if(!value.equals(current)) {
4401              tableData.get(row).path.renameFile((String)value); ////fffffffff
4402              //renameFile(tableData.get(row).getPath(), (String)value);
4403              fireTableCellUpdated(row, col);
4404              // following needed to update display with new file name
4405              javax.swing.SwingUtilities.invokeLater(new Runnable() {
4406                  @Override
4407                  public void run() {
4408                    regenerateTable();
4409                  }
4410                });
4411            }
4412          } else if(col == 1) {
4413            byte[] targetBytes = tableData.get(row).path.readLink();
4414            String target = bytesToString(targetBytes);
4415            //System.out.println(target);
4416            //System.out.println(value);
4417            tableData.get(row).path.delete();
4418            tableData.get(row).path.makeLinkTo(stringToBytes((String)value)); ////fffffffff
4419            javax.swing.SwingUtilities.invokeLater(new Runnable() {
4420                @Override
4421                public void run() {
4422                  regenerateTable();
4423                }
4424              });
4425          }
4426        }
4427      }
4428    } // class MyTableModel extends AbstractTableModel
4429
4430    /**
4431     * This class controls the text, foreground and background color for
4432     * table cells.
4433     */
4434    class MyTableCellRenderer extends DefaultTableCellRenderer {
4435      final static long serialVersionUID = 42;
4436
4437      @Override
4438      public Component getTableCellRendererComponent(JTable table,
4439                                                     Object value,
4440                                                     boolean isSelected,
4441                                                     boolean hasFocus,
4442                                                     int row,
4443                                                     int viewColumn) {
4444        if(table == null) return this;
4445        int column = table.convertColumnIndexToModel(viewColumn); ////
4446
4447        Color fg = table.getForeground();
4448        Color bg = table.getBackground();
4449        setToolTipText(null);
4450        /*
4451         * drop location - pink/blue
4452         * selected - selection colors
4453         * link - yellow
4454         * normal - background gray for directories
4455         */
4456        JTable.DropLocation dropLocation = table.getDropLocation();
4457        if(dropLocation != null
4458           && !dropLocation.isInsertRow()
4459           && !dropLocation.isInsertColumn()
4460           && dropLocation.getColumn() == column
4461           && dropLocation.getRow() == row) {
4462          fg = BLUE;
4463          bg = PINK;
4464        } else if(isSelected) {
4465          fg = table.getSelectionForeground();
4466          bg = table.getSelectionBackground();
4467        } else {
4468          fg = table.getForeground();
4469          if(column == 0 && tableData.get(row).path.isDirectory()) {
4470            bg = LIGHT_GRAY;
4471          } else if(tableData.get(row).path.isLink()) {
4472            bg = LIGHT_YELLOW;
4473          } else {
4474            bg = (table.getBackground());
4475          }
4476        }
4477        setForeground(fg);
4478        setBackground(bg);
4479        setFont(table.getFont());
4480        setBorder(new EmptyBorder(1, 1, 1, 1));
4481        if(column == 0) {
4482          setValue(value);
4483          setHorizontalAlignment(JLabel.LEFT);
4484          setToolTipText(value.toString());
4485        } else if(column == 1) {
4486          if(value == null) {
4487            System.err.println("null at column 1 getTableCellRendererComponent");
4488            return this;
4489          }
4490          String s;
4491          if(tableData.get(row).path.isLink()) {
4492            byte[] bytes = tableData.get(row).path.readLink();
4493            if(bytes == null) s = "?????";
4494            else s = bytesToString(bytes);
4495            setHorizontalAlignment(JLabel.LEFT);
4496          } else {
4497            s = addCommas(value.toString());
4498            setHorizontalAlignment(JLabel.RIGHT);
4499          }
4500          setValue(s);
4501        } else if(column == 2) {
4502          setValue(value);
4503          setHorizontalAlignment(JLabel.LEFT);
4504        }
4505        return this;
4506      }
4507    } // class MyTableCellRenderer extends DefaultTableCellRenderer
4508
4509    /**
4510     * This class handles drag &amp; drop on Tables. Only files can be
4511     * dropped. Dropping multiple files is also supported. A drop on a
4512     * directory moves or copies the files to the dropped on
4513     * directory. A drop on a file or in a blank area of the table drops
4514     * to the node selected in the tree.
4515     */
4516    class MyTableTransferHandler extends TransferHandler {
4517      final static long serialVersionUID = 42;
4518
4519      @Override
4520      public boolean canImport(TransferHandler.TransferSupport info) {
4521        // we only support drags and drops (not clipboard paste)
4522        if(!info.isDrop()) return false;
4523        info.setShowDropLocation(true);
4524
4525        // we only import files //////// This is the destination Component
4526        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return true;
4527        if(info.isDataFlavorSupported(remotePathArrayFlavor)) return true;
4528        if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
4529          return true;
4530        }
4531        return false;
4532      }
4533
4534      /**
4535       * Return true if data successfully imported from info
4536       */
4537      @Override
4538      public boolean importData(TransferHandler.TransferSupport info) {
4539        if(!canImport(info)) {
4540          return false;
4541        }
4542        Transferable transferable = info.getTransferable();
4543        JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
4544        int row = dl.getRow();
4545        MyPath targetPath;
4546        MyPath tp; // drop directory location
4547
4548        if(row == -1) {
4549          // no table entry selected so use selected tree node
4550          targetPath = tp = getSelectedPath(tree.getSelectionPath());
4551        } else {
4552          targetPath = tp = tableData.get(row).path;
4553        }
4554        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||
4555           info.isDataFlavorSupported(remotePathArrayFlavor)) {
4556          // get directory to move/copy file(s) into
4557          if(!tp.isDirectory()) tp = tp.getParent();
4558          ArrayList<MyPath> sourceFiles = new ArrayList<MyPath>();
4559          try {
4560            // get list of MyPath of files to move/copy
4561            if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
4562              List<?> files = (List<?>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
4563              for(Object file : files) {
4564                String s = toUri(((File)file).toString()).toString(); ////ffffffff
4565                if(windows) s = s.replaceAll("%5C", "/");
4566                sourceFiles.add(new LocalPath(toUri(s))); ////fffffffff
4567              }
4568            } else if(info.isDataFlavorSupported(remotePathArrayFlavor)) {
4569              try {
4570                List<?> files = (List<?>)transferable.getTransferData(remotePathArrayFlavor);
4571                for(Object file : files) {
4572                  RemotePath remotePath = (RemotePath)file;
4573                  sourceFiles.add(remotePath);
4574                }
4575              } catch(Exception e) {
4576                System.err.println(e);
4577                e.printStackTrace();
4578                return false;
4579              }
4580            }
4581            // do the move/copy
4582            if(info.getDropAction() == COPY) {
4583              for(MyPath path : sourceFiles) {
4584                copyTree(path, tp.resolve(path.toString()), 1, null);
4585              }
4586            } else if(info.getDropAction() == MOVE) {
4587              for(MyPath path : sourceFiles) {
4588                moveTree(path, tp.resolve(path.toString()));
4589                //moveTree(path, tp);
4590              }
4591              //tp.moveFilesFrom(sourceFiles);
4592            } else return false;
4593            javax.swing.SwingUtilities.invokeLater(new Runnable() {
4594                @Override
4595                public void run() {
4596                  regenerateTable();
4597                }
4598              });
4599            return true; // success!
4600          } catch(UnsupportedFlavorException e) {
4601            System.err.println("Exception: UnsupportedFlavorException");
4602            return false;
4603          } catch(IOException e) {
4604            System.err.println("Exception: IOException");
4605            e.printStackTrace();
4606            return false;
4607          } catch(ClassCastException e) {
4608            System.err.println("Exception: ClassCastException");
4609            e.printStackTrace();
4610            return false;
4611          } catch(Throwable e) {
4612            System.err.println("Exception: Throwable");
4613            e.printStackTrace();
4614            return false;
4615          }
4616
4617        }
4618        if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
4619          if(targetPath.isDirectory()) return false;
4620          String data;
4621          try {
4622            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
4623          } catch(UnsupportedFlavorException e) {
4624            return false;
4625          } catch(IOException e) {
4626            return false;
4627          }
4628          if(info.isDrop()) {
4629            targetPath.renameFile(data);
4630            javax.swing.SwingUtilities.invokeLater(new Runnable() {
4631                @Override
4632                public void run() {
4633                  regenerateTable();
4634                }
4635              });
4636            return true;
4637          } else {
4638            return false;
4639          }
4640        }
4641        return false;
4642      }
4643
4644      @Override
4645      public int getSourceActions(JComponent c) {
4646        return COPY_OR_MOVE;
4647      }
4648
4649      @Override
4650      public Transferable createTransferable(JComponent c) {
4651        int[] rows = table.getSelectedRows();
4652        DataFlavor myFileFlavor = tableData.get(rows[0]).path.getFlavor();
4653        return new Transferable() {
4654          @Override
4655          public DataFlavor[] getTransferDataFlavors() {
4656            return new DataFlavor[]{myFileFlavor,
4657                                    DataFlavor.stringFlavor};
4658          }
4659
4660          @Override
4661          public boolean isDataFlavorSupported(DataFlavor flavor) {
4662            return flavor.equals(myFileFlavor)
4663              || flavor.equals(DataFlavor.stringFlavor);
4664          }
4665
4666          @Override
4667          public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
4668            if(flavor.equals(myFileFlavor)) { ////fffffffffffffff
4669              if(flavor.equals(DataFlavor.javaFileListFlavor)) {
4670                ArrayList<File> files = new ArrayList<File>();
4671                for(int row : rows) {
4672                  files.add(((LocalPath)tableData.get(row).path).path.toFile());
4673                }
4674                return files;
4675              }
4676              if(flavor.equals(remotePathArrayFlavor)) {
4677                ArrayList<RemotePath> files = new ArrayList<RemotePath>();
4678                for(int row : rows) {
4679                  files.add((RemotePath)tableData.get(row).path);
4680                }
4681                return files;
4682              }
4683            }
4684            if(flavor.equals(DataFlavor.stringFlavor)) {
4685              int row = table.getSelectedRows()[0];
4686              return tableData.get(row).path.toString();
4687            }
4688            return null;
4689          }
4690        };
4691      }
4692
4693      @Override
4694      public void exportDone(JComponent c, Transferable t, int i) {
4695      }
4696    } // class MyTableTransferHandler extends TransferHandler
4697
4698    MyTreeNode treeRoot = new MyTreeNode(root, null);
4699    TreePath rootTreePath = new TreePath(treeRoot);
4700
4701    /**
4702     * This class controls the updating of the tree when a node is
4703     * renamed.
4704     */
4705    class MyTreeModel implements TreeModel {
4706      final static long serialVersionUID = 42;
4707
4708      MyTreeModel() {
4709      }
4710
4711      EventListenerList listenerList = new EventListenerList();
4712      TreeModelEvent treeModelEvent = null;
4713
4714      @Override
4715      public void addTreeModelListener(TreeModelListener l) {
4716        listenerList.add(TreeModelListener.class, l);
4717      }
4718
4719      @Override
4720      public void removeTreeModelListener(TreeModelListener l) {
4721        listenerList.remove(TreeModelListener.class, l);
4722      }
4723
4724      void reload(TreePath treePath) {
4725        // Guaranteed to return a non-null array
4726        Object[] listeners = listenerList.getListenerList();
4727        // Process the listeners last to first, notifying
4728        // those that are interested in this event
4729        for(int i = listeners.length-2; i>=0; i-=2) {
4730          if(listeners[i]==TreeModelListener.class) {
4731            // Lazily create the event:
4732            treeModelEvent = new TreeModelEvent(this, treePath);
4733            ((TreeModelListener)listeners[i+1]).treeStructureChanged(treeModelEvent);
4734          }
4735        }
4736      }
4737
4738      @Override
4739      public int getIndexOfChild(Object parent, Object child) {
4740        if(((MyTreeNode)parent).children == null) return -1;
4741        return ((MyTreeNode)parent).children.indexOf((MyTreeNode)child);
4742      }
4743
4744      @Override
4745      public int getChildCount(Object parent) {
4746        ArrayList<MyTreeNode> children = ((MyTreeNode)parent).children;
4747        if(children == null) return 0;
4748        return children.size();
4749      }
4750
4751      @Override
4752      public MyTreeNode getChild(Object parent, int index) {
4753        return ((MyTreeNode)parent).children.get(index);
4754      }
4755
4756      @Override
4757      public MyTreeNode getRoot() {
4758        return treeRoot;
4759      }
4760
4761      @Override
4762      public boolean isLeaf(Object node) {
4763        return false;
4764      }
4765      ////ffffffffffffff
4766      @Override
4767      public void valueForPathChanged(TreePath treePath, Object newValue) {
4768        getSelectedPath(treePath).renameFile((String)newValue);
4769      }
4770
4771      void nodesWereInserted(MyTreeNode node, int childIndex) {
4772        if(listenerList != null && node != null) {
4773          Object[] newChildren = new Object[]{node.children.get(childIndex)};
4774          fireTreeNodesInserted(this,
4775                                getPathToRoot(node), new int[]{childIndex},
4776                                newChildren);
4777        }
4778      }
4779
4780      void nodesWereRemoved(MyTreeNode node, int childIndex,
4781                            MyTreeNode removedChild) {
4782        if(listenerList != null && node != null) {
4783
4784          if(node != null) {
4785            fireTreeNodesRemoved(this, getPathToRoot(node),
4786                                 new int[]{childIndex},
4787                                 new Object[]{removedChild});
4788          }
4789
4790          //Object[] newChildren = new Object[]{node.children.get(childIndex)};
4791          //fireTreeNodesDeleted;
4792        }
4793      }
4794
4795      /**
4796       * Builds the parents of node up to and including the root node,
4797       * where the original node is the last element in the returned
4798       * array. The length of the returned array gives the node's depth in
4799       * the tree.
4800       *
4801       * @param aNode the TreeNode to get the path for
4802       * @return array of treenodes from the root to given MyTreeNode
4803       */
4804      MyTreeNode[] getPathToRoot(MyTreeNode aNode) {
4805        return getPathToRoot(aNode, 0);
4806      }
4807
4808      /**
4809       * Builds the parents of node up to and including the root node,
4810       * where the original node is the last element in the returned
4811       * array. The length of the returned array gives the node's depth
4812       * in the tree.
4813       *
4814       * @param aNode  the TreeNode to get the path for
4815       * @param depth  an int giving the number of steps already taken towards
4816       *        the root (on recursive calls), used to size the returned array
4817       * @return an array of TreeNodes giving the path from the root to the
4818       *         specified node
4819       */
4820      MyTreeNode[] getPathToRoot(MyTreeNode aNode, int depth) {
4821        MyTreeNode[] retNodes;
4822        // This method recurses, traversing towards the root in order
4823        // size the array. On the way back, it fills in the nodes,
4824        // starting from the root and working back to the original node.
4825
4826        /* Check for null, in case someone passed in a null node, or
4827           they passed in an element that isn't rooted at root. */
4828        if(aNode == null) {
4829          if(depth == 0)
4830            return null;
4831          else
4832            retNodes = new MyTreeNode[depth];
4833        }
4834        else {
4835          ++depth;
4836          if(aNode == treeRoot)
4837            retNodes = new MyTreeNode[depth];
4838          else
4839            retNodes = getPathToRoot(aNode.parent, depth);
4840          retNodes[retNodes.length - depth] = aNode;
4841        }
4842        return retNodes;
4843      }
4844
4845      protected void fireTreeNodesInserted(Object source, Object[] path,
4846                                           int[] childIndices,
4847                                           Object[] children) {
4848        // Guaranteed to return a non-null array
4849        Object[] listeners = listenerList.getListenerList();
4850        TreeModelEvent e = null;
4851        // Process the listeners last to first, notifying those that
4852        // are interested in this event
4853        for (int i = listeners.length-2; i>=0; i-=2) {
4854          if (listeners[i]==TreeModelListener.class) {
4855            // Lazily create the event:
4856            if (e == null)
4857              e = new TreeModelEvent(source, path,
4858                                     childIndices, children);
4859            ((TreeModelListener)listeners[i+1]).treeNodesInserted(e);
4860          }
4861        }
4862      }
4863
4864      protected void fireTreeNodesRemoved(Object source, Object[] path,
4865                                          int[] childIndices,
4866                                          Object[] children) {
4867        // Guaranteed to return a non-null array
4868        Object[] listeners = listenerList.getListenerList();
4869        TreeModelEvent e = null;
4870        // Process the listeners last to first, notifying
4871        // those that are interested in this event
4872        for (int i = listeners.length-2; i>=0; i-=2) {
4873          if (listeners[i]==TreeModelListener.class) {
4874            // Lazily create the event:
4875            if (e == null)
4876              e = new TreeModelEvent(source, path,
4877                                     childIndices, children);
4878            ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e);
4879          }
4880        }
4881      }
4882    } // class MyTreeModel
4883
4884    /**
4885     * MyTreeCellRenderer renders paths as root names and sets
4886     * appropriate colors for selections etc.
4887     */
4888    class MyTreeCellRenderer extends JLabel implements TreeCellRenderer {
4889      final static long serialVersionUID = 42;
4890
4891      { setOpaque(true); } // needed to allow setting background color
4892
4893      @Override
4894      public Component getTreeCellRendererComponent(JTree tree,
4895                                                    Object value,
4896                                                    boolean isSelected,
4897                                                    boolean expanded,
4898                                                    boolean leaf,
4899                                                    int row,
4900                                                    boolean hasFocus) {
4901        if(tree == null) return this;
4902        MyPath path = ((MyTreeNode)value).myPath;
4903        String name = path == null ? "null" : path.toString();
4904        setText(name);
4905        Color fg = tree.getForeground();
4906        Color bg = tree.getBackground();
4907        JTree.DropLocation dropLocation = tree.getDropLocation();
4908        if(dropLocation != null && dropLocation.getPath() != null
4909           && getSelectedPath(dropLocation.getPath()) == path) {
4910          fg = BLUE;
4911          bg = PINK;
4912          isSelected = true;
4913        } else if(isSelected) {
4914          fg = table.getSelectionForeground();
4915          bg = table.getSelectionBackground();
4916        }
4917        setForeground(fg);
4918        setBackground(bg);
4919        return this;
4920      }
4921    } // class MyTreeCellRenderer implements TreeCellRenderer
4922    MyTreeCellRenderer myTreeCellRenderer = new MyTreeCellRenderer();
4923
4924    /**
4925     * This class handles drag &amp; drop on Trees. Files can be
4926     * dropped. For files only the file name without directory
4927     * information is retained. Dropping multiple files is not
4928     * supported.
4929     */
4930    class MyTreeTransferHandler extends TransferHandler {
4931      final static long serialVersionUID = 42;
4932
4933      @Override
4934      public boolean canImport(TransferHandler.TransferSupport info) {
4935        // we only support drags and drops (not clipboard paste)
4936        if(!info.isDrop()) return false;
4937        info.setShowDropLocation(true);
4938
4939        // we only import files
4940        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return true;
4941        if(info.isDataFlavorSupported(remotePathArrayFlavor)) return true;
4942        return false;
4943      }
4944
4945      @Override
4946      public boolean importData(TransferHandler.TransferSupport info) {
4947        if(!canImport(info)) {
4948          return false;
4949        }
4950        Transferable transferable = info.getTransferable();
4951        JTree.DropLocation dl = (JTree.DropLocation)info.getDropLocation();
4952        TreePath treePath = dl.getPath();
4953        MyPath targetPath = getSelectedPath(treePath);
4954        ArrayList<MyPath> sourceFiles = new ArrayList<MyPath>();
4955        try {
4956          // get list of MyPath of files to move/copy
4957          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
4958            List<?> files = (List<?>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
4959            for(Object file : files) { ////ffffffffffffff
4960              String s = toUri(((File)file).toString()).toString();
4961              if(windows) s = s.replaceAll("%5C", "/");
4962              sourceFiles.add(new LocalPath(toUri(s))); /////////////////
4963              //sourceFiles.add(new LocalPath(toUri(((File)file).toString())));
4964            }
4965          } else if(info.isDataFlavorSupported(remotePathArrayFlavor)) {
4966            try {
4967              List<?> files = (List<?>)transferable.getTransferData(remotePathArrayFlavor);
4968              for(Object file : files) {
4969                RemotePath remotePath = (RemotePath)file;
4970                sourceFiles.add(remotePath);
4971              }
4972            } catch(Exception e) {
4973              System.err.println(e);
4974              e.printStackTrace();
4975              return false;
4976            }
4977          } else {
4978            System.err.println("Failed to import");
4979            return false;
4980          }
4981
4982          // do the move/copy
4983          if(info.getDropAction() == COPY) {
4984            for(MyPath path : sourceFiles) {
4985              copyTree(path, targetPath.resolve(path.toString()), 1, null);
4986              //copyTree(path, targetPath);
4987            }
4988          } else if(info.getDropAction() == MOVE) {
4989            //targetPath.moveFilesFrom(sourceFiles);
4990            for(MyPath path : sourceFiles) {
4991              moveTree(path, targetPath.resolve(path.toString()));
4992              //moveTree(path, targetPath);
4993            }
4994          } else return false;
4995          return true; // success!
4996        } catch(UnsupportedFlavorException e) {
4997          System.err.println("Exception: UnsupportedFlavorException");
4998          return false;
4999        } catch(IOException e) {
5000          System.err.println("Exception: IOException");
5001          e.printStackTrace();
5002          return false;
5003        } catch(ClassCastException e) {
5004          System.err.println("Exception: ClassCastException");
5005          e.printStackTrace();
5006          return false;
5007        }
5008      }
5009
5010      @Override
5011      public int getSourceActions(JComponent c) {
5012        return COPY_OR_MOVE;
5013      }
5014
5015      @Override
5016      public Transferable createTransferable(JComponent c) {
5017        MyPath myPath = getSelectedPath(tree.getSelectionPath());
5018        DataFlavor myFileFlavor = myPath.getFlavor();
5019        return new Transferable() {
5020          @Override
5021          public DataFlavor[] getTransferDataFlavors() {
5022            return new DataFlavor[]{myFileFlavor, DataFlavor.stringFlavor};
5023          }
5024
5025          @Override
5026          public boolean isDataFlavorSupported(DataFlavor flavor) {
5027            return flavor.equals(myFileFlavor) || flavor.equals(DataFlavor.stringFlavor);
5028          }
5029
5030          @Override
5031          public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
5032            if(flavor.equals(myFileFlavor)) {
5033              if(flavor.equals(DataFlavor.javaFileListFlavor)) {
5034                ArrayList<File> files = new ArrayList<File>();
5035                files.add(((LocalPath)myPath).path.toFile());
5036                return files; ////ffffffffffffff
5037              }
5038              if(flavor.equals(remotePathArrayFlavor)) {
5039                ArrayList<RemotePath> files = new ArrayList<RemotePath>();
5040                files.add((RemotePath)myPath);
5041                return files;
5042              }
5043            }
5044            if(flavor.equals(DataFlavor.stringFlavor)) {
5045              int row = table.getSelectedRows()[0];
5046              return myPath.toString();
5047            }
5048            return null;
5049          }
5050        };
5051      }
5052
5053      @Override
5054      public void exportDone(JComponent c, Transferable t, int i) {
5055      }
5056    } // class MyTreeTransferHandler extends TransferHandler
5057
5058    /**
5059     * Initialize a FileBrowser to the root(s) of the file system
5060     */
5061    /**
5062     * This class is the main window. It contains a JSplitPane for the
5063     * files with surrounding ornaments.
5064     *
5065     * Displayed directories are "watched" for changes so that the
5066     * display accurately reflects the contents of the directory. The
5067     * selected tree node is watched as well as any expanded tree
5068     * node.
5069     *
5070     * Expanding a null node does a full expansion.
5071     * Expanding a non-null node does no recalculation.
5072     * Contracting a node sets children to null.
5073     * Selecting from the selection box only adds required node(s).
5074     * Double clicking a directory from the table only adds one directory to tree
5075     * Changing selected node recalculates table children.
5076     * Changing tree node adds or deletes child node and resets selected node.
5077     *
5078     * The file manipulation routines are:
5079     *
5080     * drag File array from tree
5081     *   only single directories can be dragged.
5082     * drag File array from table
5083     *   multiple directories and files can be dragged.
5084     * both move and copy are supported.
5085     *   With a move the files and directories are removed from the old
5086     *   directory and moved to the new directory.
5087     *   With a copy the files and directories are copied from the old
5088     *   directory to the new directory so two copies result.
5089     *
5090     * drop File array to tree
5091     *   The files and directories are put in the specified directory.
5092     * drop File array to table
5093     *   The files and directories are placed in the dropped-on directory
5094     *   or the containing directory if a file.
5095     * In all cases a collision of file/directory names results in a popup
5096     * asking if the operation should proceed.
5097     *
5098     * directory moves also check for directory move to child*
5099     *
5100     * edit File name in tree or table
5101     *   The renamed file or directory remains in the same containing
5102     *   directory. If there is a name collision the operation is aborted.
5103     * FIX TREE DIRECTORY RENAME - also check for directory move to child*
5104     *   Currently only CR causes rename otherwise editor is not closed.
5105     *     This might be good.
5106     *
5107     * If a directory replaces a directory of the same name then the
5108     * two directories are compared and the results are displayed in a
5109     * new Comparison window. The direction of transfer can be altered
5110     * or some files not transferred before the Transfer button is
5111     * pressed.
5112     *
5113     */
5114    Browser() {
5115      initBrowser();
5116      //tree.setSelectionPath(rootTreePath); ///////
5117      selectFromPath(root, true);
5118    }
5119
5120    /**
5121     * Initialize a FileBrowser to view the given path
5122     *
5123     * @param path the path to set the file browser to initially view
5124     */
5125    Browser(MyPath path) {
5126      initBrowser();
5127      selectFromPath(path, true); /////// should throw if unsuccessful
5128    }
5129
5130    /**
5131     * Initialize a 2-panel file browser (directory tree on the left
5132     * and file table on the right).
5133     */
5134    void initBrowser() {
5135      super.setLayout(new BorderLayout());
5136      super.setTitle("Files");
5137
5138      addWindowListener(new WindowAdapter() {
5139          @Override
5140          public void windowClosing(WindowEvent e) {
5141            // remove all file listeners before session(s) disconnect
5142            for(Map.Entry<WatchKey,TreePath> pair : treeKeys.entrySet()) {
5143              pair.getKey().cancel();
5144            }
5145            if(tableKey != null) {
5146              tableKey.cancel();
5147              tableKey = null;
5148              tablePath = null;
5149            }
5150            if(--windowCount == 0) {
5151              finish();
5152            }
5153            dispose();
5154          }
5155        });
5156      ++windowCount;
5157      try {
5158        treeWatcher = FileSystems.getDefault().newWatchService();
5159        tableWatcher = FileSystems.getDefault().newWatchService();
5160      } catch(IOException e) {
5161        e.printStackTrace();
5162      }
5163      setLayout(new BorderLayout());
5164
5165      JMenuBar menuBar = new JMenuBar();
5166      menuBar.setLayout(new GridLayout(0, 1));
5167
5168      JMenu menu1 = new JMenu();
5169      JMenuBar menuBar1 = new JMenuBar();
5170      menuBar1.add(menu1);
5171      menuBar.add(menuBar1);
5172
5173      // back
5174      menuBar1.add(new JButton(" ") {
5175          final static long serialVersionUID = 42;
5176
5177          {
5178            addMouseListener(new MouseAdapter() {
5179                @Override
5180                public void mouseClicked(MouseEvent e) {
5181                  if(historyIndex < 0) return;
5182                  --historyIndex;
5183                  if(historyIndex < 0) {
5184                    tree.setSelectionPath(rootTreePath);
5185                  } else {
5186                    MyPath path = history.get(historyIndex);
5187                    currentDirectory.setText(path.fullName());
5188                    selectionBox.setSelectedItem(path);
5189                    selectFromPath(path, false);
5190                  }
5191                }
5192              });
5193            setToolTipText("Goto last directory");
5194          }
5195
5196          protected void paintComponent(Graphics g) {
5197            super.paintComponent(g);
5198            int w = getWidth();
5199            int h = getHeight();
5200            // left facing arrow
5201            g.fillPolygon(new int[]{w/2+h/4, w/2-h/4, w/2+h/4},
5202                          new int[]{0, h/2, h}, 3);
5203          }
5204        });
5205
5206      // up
5207      menuBar1.add(new JButton(" ") {
5208          final static long serialVersionUID = 42;
5209
5210          {
5211            addMouseListener(new MouseAdapter() {
5212                @Override
5213                public void mouseClicked(MouseEvent e) {
5214                  TreePath treePathChild = tree.getSelectionPath();
5215                  //System.out.println('*' + treePathChild.toString());
5216                  TreePath treePathParent = treePathChild.getParentPath(); ///////// null pointer exception
5217                  if(treePathChild != null && treePathParent != null) {
5218                    tree.setSelectionPath(treePathParent);
5219                  }
5220                }
5221              });
5222            setToolTipText("Goto parent directory");
5223          }
5224
5225          protected void paintComponent(Graphics g) {
5226            super.paintComponent(g);
5227            int w = getWidth();
5228            int h = getHeight();
5229            // upward arrow
5230            g.fillPolygon(new int[]{w/2-h/2, w/2, w/2+h/2},
5231                          new int[]{h/2+h/4, h/2-h/4, h/2+h/4}, 3);
5232          }
5233        });
5234
5235      // right
5236      menuBar1.add(new JButton(" ") {
5237          final static long serialVersionUID = 42;
5238
5239          {
5240            addMouseListener(new MouseAdapter() {
5241                @Override
5242                public void mouseClicked(MouseEvent e) {
5243                  if(history.size() > historyIndex + 1) {
5244                    ++historyIndex;
5245                    MyPath path = history.get(historyIndex);
5246                    currentDirectory.setText(path.fullName());
5247                    selectionBox.setSelectedItem(path);
5248                    selectFromPath(path, false);
5249                  }
5250                }
5251              });
5252            setToolTipText("Goto next directory");
5253          }
5254
5255          protected void paintComponent(Graphics g) {
5256            super.paintComponent(g);
5257            int w = getWidth();
5258            int h = getHeight();
5259            // right facing arrow
5260            g.fillPolygon(new int[]{w/2-h/4, w/2+h/4, w/2-h/4},
5261                          new int[]{0, h/2, h}, 3);
5262          }
5263        });
5264
5265      menuBar1.add(new JButton("Show dot files") {
5266          final static long serialVersionUID = 42;
5267
5268          {
5269            addMouseListener(new MouseAdapter() {
5270                @Override
5271                public void mouseClicked(MouseEvent e) {
5272                  showDotFiles = !showDotFiles;
5273                  if(showDotFiles) setText("Hide dot files");
5274                  else setText("Show dot files");
5275                  TreePath treePath = tree.getSelectionPath();
5276                  regenerateTable();
5277                }
5278              });
5279          }
5280        });
5281
5282      menuBar1.add(new JButton("Tree View") {
5283          final static long serialVersionUID = 42;
5284
5285          {
5286            addMouseListener(new MouseAdapter() {
5287                @Override
5288                public void mouseClicked(MouseEvent e) {
5289                  listFiles = !listFiles;
5290                  if(listFiles) {
5291                    setText("Tree View");
5292                    splitLocation = splitPane.getDividerLocation();
5293                    splitPane.setDividerLocation(0);
5294                  } else {
5295                    setText("List View");
5296                    splitPane.setDividerLocation(splitLocation);
5297                  }
5298                }
5299              });
5300          }
5301        });
5302
5303      menuBar1.add(new JButton("Clone") {
5304          final static long serialVersionUID = 42;
5305
5306          {
5307            addMouseListener(new MouseAdapter() {
5308                @Override
5309                public void mouseClicked(MouseEvent e) {
5310                  makeBrowser(getSelectedPath(tree.getSelectionPath()));
5311                }
5312              });
5313          }
5314        });
5315
5316      JMenu menu2 = new JMenu();
5317      JMenuBar menuBar2 = new JMenuBar();
5318      menuBar2.add(menu2);
5319      menuBar.add(menuBar2);
5320
5321      menuBar2.add(new JLabel("Directory: "));
5322
5323      currentDirectory.setTransferHandler(new MyTextTransferHandler());
5324
5325      menuBar2.add(selectionBox);
5326
5327      menuBar2.add(new JButton("Go") {
5328          final static long serialVersionUID = 42;
5329
5330          {
5331            addMouseListener(new MouseAdapter() {
5332                @Override
5333                public void mouseClicked(MouseEvent e) {
5334                  try {
5335                    selectedPath = stringToMyPath(editor.getText());
5336                    while((!selectedPath.exists()
5337                           || !selectedPath.isDirectory())
5338                          && selectedPath != root) {
5339                      selectedPath = selectedPath.getParent();
5340                      if(selectedPath == null) {
5341                        selectedPath = root;
5342                        break;
5343                      }
5344                    }
5345                  } catch(NullPointerException ex) {
5346                    System.err.println("caught null pointer"); /////////
5347                    selectedPath = root;
5348                  }
5349                  editor.setText(selectedPath.fullName());
5350                  selectionBox.setSelectedItem(selectedPath);
5351                  selectFromPath((MyPath)selectionBox.getSelectedItem(), true);
5352                }
5353              });
5354          }
5355        });
5356
5357      menuBar2.add(new JButton("Add") {
5358          final static long serialVersionUID = 42;
5359
5360          {
5361            addMouseListener(new MouseAdapter() {
5362                @Override
5363                public void mouseClicked(MouseEvent e) {
5364                  MyPath newItem = (MyPath)selectionBox.getSelectedItem();
5365                  lookup: {
5366                    for(int i = 0; i < selectionBox.getItemCount(); ++i) {
5367                      MyPath item = selectionBox.getItemAt(i);
5368                      if(item.equals(newItem)) break lookup;
5369                    }
5370                    selectionBox.addItem(newItem);
5371                    ArrayList<String> selections = options.get(selectionsKey);
5372                    selections.add(newItem.fullName());
5373                  }
5374                }
5375              });
5376          }
5377        });
5378
5379      menuBar2.add(new JButton("Delete") {
5380          final static long serialVersionUID = 42;
5381
5382          {
5383            addMouseListener(new MouseAdapter() {
5384                @Override
5385                public void mouseClicked(MouseEvent e) {
5386                  MyPath newItem = (MyPath)selectionBox.getSelectedItem();
5387                  lookup: {
5388                    for(int i = 0; i < selectionBox.getItemCount(); ++i) {
5389                      MyPath item = selectionBox.getItemAt(i);
5390                      if(item.equals(newItem)) {
5391                        selectionBox.removeItemAt(i);
5392                        selectionBox.setSelectedItem(newItem);
5393                        ArrayList<String> selections = options.get(selectionsKey);
5394                        selections.remove(item.fullName());
5395                        break lookup;
5396                      }
5397                    }
5398                  }
5399                }
5400              });
5401          }
5402        });
5403
5404      setJMenuBar(menuBar);
5405
5406      JPanel spanel = new JPanel(new GridLayout(0,6));
5407
5408      spanel.add(new JButton("Print Selected Path") {
5409          final static long serialVersionUID = 42;
5410
5411          {
5412            addMouseListener(new MouseAdapter() {
5413                @Override
5414                public void mouseClicked(MouseEvent e) {
5415                  int index[] = table.getSelectedRows();
5416                  if(index.length == 0) {
5417                    MyTreeNode p = (MyTreeNode)tree.getLastSelectedPathComponent();
5418                    if(p == null) return;
5419                    System.out.println(p.myPath.fullName());
5420                  } else {
5421                    for(int i : index) {
5422                      MyPath p = tableData.get(i).path;
5423                      System.out.println(p.fullName());
5424                    }
5425                  }
5426                }
5427              });
5428          }
5429        });
5430
5431      spanel.add(new JButton("Print Tree") {
5432          final static long serialVersionUID = 42;
5433
5434          void printTreeNodes(String indent, MyTreeNode node) {
5435            System.out.println(indent + node.myPath);
5436            if(node.children != null) {
5437              for(MyTreeNode child : node.children) {
5438                printTreeNodes(indent + "  ", child);
5439              }
5440            }
5441          }
5442
5443          {
5444            addMouseListener(new MouseAdapter() {
5445                @Override
5446                public void mouseClicked(MouseEvent e) {
5447                  System.out.println("-----------------");
5448                  printTreeNodes("", treeRoot);
5449                  System.out.println("-----------------");
5450                }
5451              });
5452          }
5453        });
5454
5455      spanel.add(new JButton("Delete tree") {
5456          final static long serialVersionUID = 42;
5457
5458          {
5459            addMouseListener(new MouseAdapter() {
5460                @Override
5461                public void mouseClicked(MouseEvent e) {
5462                  int index[] = table.getSelectedRows();
5463                  if(index.length == 0) {
5464                    MyPath p = getSelectedPath(tree.getSelectionPath());
5465                    TreePath parentPath = tree.getSelectionPath().getParentPath();
5466                    tree.setSelectionPath(parentPath);
5467                    deleteTree(p, null);
5468                  } else {
5469                    for(int i : index) {
5470                      MyPath p = tableData.get(i).path;
5471                      deleteTree(p, null);
5472                    }
5473                  }
5474                  regenerateTable();
5475                }
5476              });
5477          }
5478        });
5479
5480      spanel.add(new JButton("Create Link") {
5481          final static long serialVersionUID = 42;
5482
5483          {
5484            addMouseListener(new MouseAdapter() {
5485                @Override
5486                public void mouseClicked(MouseEvent e) {
5487                  selectedPath = stringToMyPath(editor.getText());
5488                  if(selectedPath.exists()) return;
5489                  selectedPath.makeLinkTo(new byte[]{'x','x','x'});
5490                  regenerateTable();
5491                }
5492              });
5493          }
5494        });
5495
5496      spanel.add(new JButton("Touch File") {
5497          final static long serialVersionUID = 42;
5498
5499          {
5500            addMouseListener(new MouseAdapter() {
5501                @Override
5502                public void mouseClicked(MouseEvent e) {
5503                  selectedPath = stringToMyPath(editor.getText());
5504                  selectedPath.touch();
5505                  regenerateTable();
5506                }
5507              });
5508          }
5509        });
5510
5511      spanel.add(new JButton("Create Directory") {
5512          final static long serialVersionUID = 42;
5513
5514          {
5515            addMouseListener(new MouseAdapter() {
5516                @Override
5517                public void mouseClicked(MouseEvent e) {
5518                  selectedPath = stringToMyPath(editor.getText());
5519                  selectedPath.makeDirectory();
5520                  regenerateTable();
5521                }
5522              });
5523          }
5524        });
5525
5526      add(spanel, BorderLayout.SOUTH);
5527
5528      // Initialize tree (in left side of JSplitPane)
5529
5530      TreeSet<TableData> fileRoots = getRootPaths(true);
5531      ArrayList<MyPath> roots = new ArrayList<MyPath>();
5532      for(TableData data : fileRoots) {
5533        roots.add(data.path);
5534      }
5535      root.treeChildren = roots;
5536      tree = new JTree(myTreeModel);
5537      tree.setOpaque(true);
5538      tree.setCellRenderer(myTreeCellRenderer);
5539      tree.setDragEnabled(true);
5540      tree.setDropMode(DropMode.ON);
5541      tree.setTransferHandler(new MyTreeTransferHandler() {
5542          final static long serialVersionUID = 42;
5543        });
5544      tree.setEditable(true);
5545      tree.getSelectionModel().setSelectionMode
5546        (TreeSelectionModel.SINGLE_TREE_SELECTION);
5547      //tree.setInvokesStopCellEditing(true); //////////do we want this?
5548      tree.setShowsRootHandles(true);
5549      //tree.setRootVisible(false);
5550      tree.setRootVisible(true);
5551      //tree.setLargeModel(true);
5552      tree.addMouseListener(new MouseAdapter() {
5553          @Override
5554          public void mouseClicked(MouseEvent e) {
5555            if(e.getButton() == 3) {
5556              Point p = e.getPoint();
5557              TreePath tp = tree.getPathForLocation(p.x, p.y);
5558              if(tp == null) return;
5559              MyPath path = getSelectedPath(tp);
5560              if(path instanceof RemotePath) {
5561                String authority = ((RemotePath)path).uri.getRawAuthority();
5562                javax.swing.SwingUtilities.invokeLater(new Runnable() {
5563                    @Override
5564                    public void run() {
5565                      SSHWindow ssh = new SSHWindow(authority);
5566                      ssh.setLocationByPlatform(true);
5567                      ssh.pack();
5568                      ssh.setVisible(true);
5569                    }
5570                  });
5571              }
5572            }
5573          }
5574        });
5575      tree.addTreeWillExpandListener(new TreeWillExpandListener() {
5576          @Override
5577          public void treeWillExpand(TreeExpansionEvent event) {
5578            try {
5579              TreePath treePath = event.getPath();
5580              MyTreeNode parent = (MyTreeNode)treePath.getLastPathComponent();
5581              MyPath node = parent.myPath;
5582              if(parent.children == null) { // full expansion
5583                TreeSet<TableData> data = node.getChildren(showDotFiles);
5584                ArrayList<MyPath> treeChildren = new ArrayList<MyPath>();
5585                ArrayList<MyTreeNode> nodeChildren = new ArrayList<MyTreeNode>();
5586                for(TableData datum : data) {
5587                  if(datum.path.isDirectory()) {
5588                    treeChildren.add(datum.path);
5589                    nodeChildren.add(new MyTreeNode(datum.path, parent));
5590                  }
5591                  node.treeChildren = treeChildren;
5592                  parent.children = nodeChildren;
5593                  myTreeModel.reload(treePath);
5594                }
5595              }
5596              if(node instanceof LocalPath) {
5597                try {
5598                  WatchKey key = ((LocalPath)node).path.register(treeWatcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
5599                  treeKeys.put(key, treePath);
5600                } catch(IOException e) {
5601                  e.printStackTrace();
5602                }
5603              }
5604            } catch(Throwable e) {
5605              e.printStackTrace();
5606            }
5607          }
5608
5609          @Override
5610          public void treeWillCollapse(TreeExpansionEvent event) {
5611            // remove watcher on node.getPath()
5612            MyTreeNode parent = ((MyTreeNode)event.getPath().getLastPathComponent());
5613            MyPath node = parent.myPath;
5614            Enumeration<TreePath> treePath = tree.getExpandedDescendants(event.getPath());
5615            ArrayList<TreePath> treePaths = new ArrayList<TreePath>();
5616            if(treePath != null) while(treePath.hasMoreElements()) {
5617                treePaths.add(treePath.nextElement());
5618              }
5619            Collections.sort(treePaths, new Comparator<TreePath>() {
5620                @Override
5621                public int compare(TreePath x,TreePath y) {
5622                  return y.getPathCount() - x.getPathCount();
5623                }
5624              });
5625            for(TreePath p : treePaths) {
5626              // remove key referring to p from treeKeys
5627              if(getSelectedPath(p) instanceof LocalPath) {
5628                WatchKey key = ((LocalPath)getSelectedPath(p)).key;
5629                if(key != null) key.cancel();
5630                treeKeys.remove(((LocalPath)getSelectedPath(p)).key);
5631              }
5632              if(tree.isExpanded(p) && !getSelectedPath(p).equals(node)) {
5633                tree.collapsePath(p);
5634                myTreeModel.reload(p);
5635              }
5636            }
5637            node.treeChildren = null;
5638            parent.children = null;
5639            myTreeModel.reload(event.getPath());
5640          }
5641        });
5642      tree.addTreeSelectionListener(new TreeSelectionListener() {
5643          @Override
5644          public void valueChanged(TreeSelectionEvent e) {
5645            TreePath treePath = tree.getSelectionPath();
5646            MyTreeNode node = (MyTreeNode)tree.getLastSelectedPathComponent();
5647            if(node == null) {
5648            } else {
5649              MyTreeNode myTreeNode = node;
5650              if(rootTreePath.equals(treePath)) {
5651                // remove table watcher
5652                if(tableKey != null) {
5653                  tableKey.cancel();
5654                  tableKey = null;
5655                  tablePath = null;
5656                }
5657              } else {
5658                // change table watcher if different
5659                if(!myTreeNode.equals(tablePath)) {
5660                  if(tableKey != null) tableKey.cancel();
5661                  tableKey = null;
5662                  tablePath = null;
5663                  /////// make polymorphic
5664                  while(!myTreeNode.myPath.exists()) {
5665                    myTreeNode = myTreeNode.parent;
5666                  }
5667                  if(myTreeNode.myPath instanceof LocalPath) {
5668                    try {
5669                      tableKey = ((LocalPath)myTreeNode.myPath).path.register(tableWatcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
5670                      tablePath = myTreeNode.myPath;
5671                    } catch(NoSuchFileException ex) {
5672                      System.err.println("No file here: " + myTreeNode.myPath);
5673                      ex.printStackTrace();
5674                      tablePath = myTreeNode.myPath;
5675                    } catch(IOException ex) {
5676                      System.err.println("Could not register path: " + myTreeNode.myPath);
5677                      ex.printStackTrace();
5678                    }
5679                  }
5680                  tablePath = myTreeNode.myPath;
5681                }
5682              }
5683              String s = myTreeNode.myPath.fullName();
5684              currentDirectory.setText(s);
5685              selectionBox.setSelectedItem(myTreeNode.myPath);
5686              //selectFromPath(myTreeNode.myPath, true);
5687              pushDirectoryInHistory(myTreeNode.myPath);
5688              regenerateTable();
5689            }
5690            tree.scrollPathToVisible(treePath);
5691            return;
5692          }
5693        });
5694
5695      // Initialize table (on right side of JSplitPane)
5696
5697      table = new JTable(myTableModel);
5698      table.setOpaque(true);
5699      table.setAutoCreateColumnsFromModel(true);
5700      table.setFillsViewportHeight(true);
5701      table.setRowSelectionAllowed(true);
5702      table.setColumnSelectionAllowed(false);
5703      table.setCellSelectionEnabled(false);
5704      table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
5705      table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
5706      table.setRowSelectionAllowed(true);
5707      table.setColumnSelectionAllowed(false);
5708      table.setDragEnabled(true);
5709      table.setDropMode(DropMode.ON);
5710      table.setDefaultRenderer(String.class, new MyTableCellRenderer());
5711      table.setTransferHandler(new MyTableTransferHandler());
5712      table.addMouseListener(new MouseAdapter() {
5713          @Override
5714          public void mouseClicked(MouseEvent e) {
5715            Point p = e.getPoint();
5716            int i = table.rowAtPoint(p);
5717            if(i < 0) return;
5718            if(e.getButton() == 1 && e.getClickCount() == 2) {
5719              if(tableData.get(i).path.isDirectory()) {
5720                MyPath myPath = tableData.get(i).path;
5721                selectFromPath(myPath, true);
5722              } else {
5723                // ?????? ///////////////// link?
5724              }
5725            } else if(e.getButton() == 3) {
5726              // check for directory
5727              MyPath path = tableData.get(i).path;
5728              if(path.isFile() || path.isLink()) {
5729                makeFileEditWindow(tableData.get(i).path);
5730              } else {
5731                if(path instanceof RemotePath) {
5732                  String authority = ((RemotePath)path).uri.getRawAuthority();
5733                  javax.swing.SwingUtilities.invokeLater(new Runnable() {
5734                      @Override
5735                      public void run() {
5736                        SSHWindow ssh = new SSHWindow(authority);
5737                        ssh.setLocationByPlatform(true);
5738                        ssh.pack();
5739                        ssh.setVisible(true);
5740                      }
5741                    });
5742                }
5743              }
5744            }
5745          }
5746        });
5747      //////////// set widths differently???
5748      for(int i = 0; i < tableColumnNames.size(); ++i) {
5749        TableColumn column = table.getColumnModel().getColumn(i);
5750        column.setPreferredWidth(100);
5751      }
5752      ((DefaultTableCellRenderer)table.getTableHeader().getDefaultRenderer())
5753        .setHorizontalAlignment(SwingConstants.LEFT);
5754
5755      // table watcher
5756      new Thread() {
5757        {
5758          setDaemon(true);
5759        }
5760
5761        @Override
5762        public void run() {
5763          for(;;) {
5764            WatchKey key;
5765            try {
5766              key = tableWatcher.take();
5767            } catch(InterruptedException x) {
5768              x.printStackTrace();
5769              return; //////////////
5770            }
5771            if(tablePath == null || !key.equals(tableKey)) {
5772              System.err.println("WatchKey not recognized: " + key);
5773              continue;
5774            }
5775            for(WatchEvent<?> event: key.pollEvents()) {
5776              WatchEvent.Kind<?> kind = event.kind();
5777              if(kind == OVERFLOW) {
5778                System.err.println("OVERFLOW");
5779                continue;
5780              }
5781              Path p = (Path)event.context();
5782              //System.out.println(p);
5783              if(kind == ENTRY_CREATE) {
5784                //System.out.println("table create: " + "   " + event.context()); // add path
5785              } else if(kind == ENTRY_DELETE) {
5786                //System.out.println("table delete: " + "   " + event.context()); // remove path
5787                // remove path from children
5788              } else if(kind == ENTRY_MODIFY) {
5789                //System.out.println("table modify: " + "   " + event.context()); // not used
5790                /////// ????????????????
5791              }
5792              javax.swing.SwingUtilities.invokeLater(new Runnable() {
5793                  @Override
5794                  public void run() {
5795                    regenerateTable();
5796                  }
5797                });
5798            }
5799            // reset key and remove from set if directory no longer accessible
5800            boolean valid = key.reset(); // to enable further events
5801            if(!valid) {
5802              key.cancel();
5803            }
5804          }
5805        }
5806      }.start();
5807
5808      // tree watcher
5809      new Thread() {
5810        {
5811          setDaemon(true);
5812        }
5813
5814        int getIndex(ArrayList<MyTreeNode> a, MyPath p) {
5815          for(int i = 0; i < a.size(); ++i) {
5816            if(a.get(i).myPath.equals(p)) return i;
5817          }
5818          return -1;
5819        }
5820
5821        @Override
5822        public void run() {
5823          for(;;) {
5824            WatchKey key;
5825            try {
5826              key = treeWatcher.take();
5827            } catch(InterruptedException x) {
5828              System.err.println("InterruptedException in tree watcher");
5829              return;
5830            }
5831            TreePath treePath = treeKeys.get(key);
5832            MyTreeNode treeNode = ((MyTreeNode)treePath.getLastPathComponent());
5833            if(treeNode == null) {
5834              System.err.println("Tree WatchKey not recognized: " + key);
5835              continue;
5836            }
5837            for(WatchEvent<?> event: key.pollEvents()) {
5838              WatchEvent.Kind<?> kind = event.kind();
5839              if(kind == OVERFLOW) {
5840                System.err.println("OVERFLOW");
5841                continue;
5842              }
5843              Path p = (Path)event.context();
5844              MyPath child = treeNode.myPath.resolve(p);
5845              if(kind == ENTRY_CREATE) {
5846                javax.swing.SwingUtilities.invokeLater(new Runnable() {
5847                    @Override
5848                    public void run() {
5849                      if(getIndex(treeNode.children, child) != -1) return;
5850                      if(!child.isDirectory()) return;
5851                      if(treeNode.children == null) {
5852                        treeNode.children = new ArrayList<MyTreeNode>();
5853                      }
5854                      int lo = 0;
5855                      int hi = treeNode.children.size();
5856                      int c = 1;
5857                      int index;
5858                      found: {
5859                        while(lo < hi) {
5860                          int mid = (lo + hi) >>> 1;
5861                          c = pathComparator.compare(treeNode.children.get(mid).myPath,
5862                                                     child);
5863                          if(c == 0) {
5864                            index = mid;
5865                            break found;
5866                          }
5867                          if(c > 0) {
5868                            hi = mid;
5869                          } else {
5870                            lo = mid + 1;
5871                          }
5872                        } // end while
5873                        treeNode.children.add(lo, new MyTreeNode(child, treeNode));
5874                        index = lo;
5875                        myTreeModel.nodesWereInserted(treeNode, index);
5876                      } // break found goes after here
5877                    }
5878                  });
5879              } else if(kind == ENTRY_DELETE) {
5880                javax.swing.SwingUtilities.invokeLater(new Runnable() {
5881                    @Override
5882                    public void run() {
5883                      if(treeNode.children == null) return;
5884                      int index = getIndex(treeNode.children, child);
5885                      myTreeModel.nodesWereRemoved(treeNode, index, treeNode.children.get(index));
5886
5887                      if(index >= 0) treeNode.children.remove(index);
5888                    }
5889                  });
5890              } else if(kind == ENTRY_MODIFY) {
5891                /////// ????????????????
5892              }
5893            }
5894            boolean valid = key.reset(); // to enable further events
5895            if(!valid) key.cancel();
5896          }
5897        }
5898      }.start();
5899
5900      //Lay everything out.
5901
5902      splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
5903                                 new JScrollPane(tree) {
5904                                   final static long serialVersionUID = 42;
5905
5906                                   {
5907                                     setPreferredSize(new Dimension(200, 300));
5908                                   }
5909                                 },
5910                                 new JScrollPane(table) {
5911                                   final static long serialVersionUID = 42;
5912
5913                                   {
5914                                     setPreferredSize(new Dimension(300, 300));
5915                                   }
5916                                 }) {
5917          final static long serialVersionUID = 42;
5918          {
5919            setOneTouchExpandable(false);
5920            setResizeWeight(.4);
5921            splitLocation = 200;
5922            setDividerLocation(0);
5923          }
5924        };
5925
5926      add(splitPane, BorderLayout.CENTER);
5927    } // initFileBrowser
5928  } // class Browser extends JFrame
5929
5930  /**
5931   * Make a FileBrowser on the indicated path.
5932   *
5933   * @param path a MyPath to make a FileBrowser on
5934   */
5935  static void makeBrowser(MyPath path) {
5936    javax.swing.SwingUtilities.invokeLater(new Runnable() {
5937        @Override
5938        public void run() {
5939          JFrame frame = new FileBrowser().new Browser(path);
5940          frame.setLocationByPlatform(true);
5941          //Display the window.
5942          frame.pack();
5943          frame.setVisible(true);
5944        }
5945      });
5946  } // static void makeBrowser(MyPath path)
5947
5948  ///////////////////////////////////////////////////////////////
5949  // start of file comparison routines
5950  ///////////////////////////////////////////////////////////////
5951
5952  /**
5953   * Gets the part of a base path after the base path.
5954   *
5955   * @param base MyPath of the base Path
5956   * @param path the full path whose base will be eliminated
5957   * @return the part of the path after the base path
5958   */
5959  static String getSuffix(MyPath base, MyPath path) {
5960    String b = base.fullName();
5961    String p = path.fullName();
5962    if(p.indexOf(b) != 0) throw new Error("Not prefix: " + b + "   " + p);
5963    if(b.charAt(b.length() - 1) == '/') return p.substring(b.length());
5964    return p.substring(b.length() + 1);
5965  }
5966
5967  /**
5968   * Replaces the prefix of a path with a new prefix. Removes the old
5969   * prefix from the path and replaces it with the new prefix.
5970   *
5971   * @param oldPrefix the prefix of the path to be removed
5972   * @param newPrefix the new prefix of the truncated path
5973   * @param path the path whose prefix is to be changed
5974   * @return the MyPath with the old prefix replaced by the new prefix
5975   */
5976  static MyPath changePrefix(MyPath oldPrefix, MyPath newPrefix, MyPath path) {
5977    String o = oldPrefix.fullName();
5978    String n = newPrefix.fullName();
5979    String s = path.fullName();
5980    String tail = getSuffix(oldPrefix, path);
5981    return newPrefix.resolve(tail);
5982  }
5983
5984  /**
5985   * An enum whose members indicate the possible operations between
5986   * pairs of files.
5987   */
5988  enum Direction {
5989    leftOverwriteNewer {
5990      Direction targetDefault() { return rightOverwriteOlder; }
5991      Direction targetLeft() { return leftOverwriteNewer; }
5992      Direction targetRight() { return rightOverwriteOlder; }
5993      Direction nextDirection() { return rightOverwriteOlder; }
5994      void icon(Graphics g, int columnWidth, int rowHeight) {
5995        int w = columnWidth;
5996        int h = rowHeight;
5997        int q = h/4;
5998        int x = w/2;
5999        int y = h/2;
6000        g.setColor(MAGENTA);
6001        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6002                                -2*q+x,
6003                                0*q+x,0*q+x,2*q+x},
6004          new int[]{-1*q+y,-1*q+y,-2*q+y,
6005                    0*q+y,
6006                    2*q+y,1*q+y,1*q+y},
6007          7);
6008      }
6009      boolean defaultCheck() { return false; }
6010      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6011        return leftPath.copyFileFrom(rightPath);
6012      }
6013      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
6014        long time = rightPath.getMTime();
6015        if(time == 0) return false;
6016        return leftPath.setMTime(time);
6017      }
6018    },
6019    leftOverwriteOlder {
6020      Direction targetDefault() { return leftOverwriteOlder; }
6021      Direction targetLeft() { return leftOverwriteOlder; }
6022      Direction targetRight() { return rightOverwriteNewer; }
6023      Direction nextDirection() { return rightOverwriteNewer; }
6024      void icon(Graphics g, int columnWidth, int rowHeight) {
6025        int w = columnWidth;
6026        int h = rowHeight;
6027        int q = h/4;
6028        int x = w/2;
6029        int y = h/2;
6030        g.setColor(RED);
6031        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6032                                -2*q+x,
6033                                0*q+x,0*q+x,2*q+x},
6034          new int[]{-1*q+y,-1*q+y,-2*q+y,
6035                    0*q+y,
6036                    2*q+y,1*q+y,1*q+y},
6037          7);
6038      }
6039      boolean defaultCheck() { return true; }
6040      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6041        return leftPath.copyFileFrom(rightPath);
6042      }
6043      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
6044        long time = rightPath.getMTime();
6045        if(time == 0) return false;
6046        return leftPath.setMTime(time);
6047      }
6048    },
6049    leftCreateFile {
6050      Direction targetDefault() { return leftCreateFile; }
6051      Direction targetLeft() { return leftCreateFile; }
6052      Direction targetRight() { return rightDeleteFile; }
6053      Direction nextDirection() { return rightDeleteFile; }
6054      void icon(Graphics g, int columnWidth, int rowHeight) {
6055        int w = columnWidth;
6056        int h = rowHeight;
6057        int q = h/4;
6058        int x = w/2;
6059        int y = h/2;
6060        g.setColor(GREEN);
6061        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6062                                -2*q+x,
6063                                0*q+x,0*q+x,2*q+x},
6064          new int[]{-1*q+y,-1*q+y,-2*q+y,
6065                    0*q+y,
6066                    2*q+y,1*q+y,1*q+y},
6067          7);
6068      }
6069      boolean defaultCheck() { return true; }
6070      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6071        return leftPath.copyFileFrom(rightPath);
6072      }
6073    },
6074    leftCreateDirectory {
6075      Direction targetDefault() { return leftCreateDirectory; }
6076      Direction targetLeft() { return leftCreateDirectory; }
6077      Direction targetRight() { return rightDeleteDirectory; }
6078      Direction nextDirection() { return rightDeleteDirectory; }
6079      void icon(Graphics g, int columnWidth, int rowHeight) {
6080        int w = columnWidth;
6081        int h = rowHeight;
6082        int q = h/4;
6083        int x = w/2;
6084        int y = h/2;
6085        g.setColor(GREEN);
6086        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6087                                -1*q+x,-1*q+x,-3*q+x,-1*q+x,-1*q+x,
6088                                0*q+x,0*q+x,2*q+x},
6089          new int[]{-1*q+y,-1*q+y,-2*q+y,
6090                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
6091                    2*q+y,1*q+y,1*q+y},
6092          11);
6093      }
6094      boolean defaultCheck() { return true; }
6095      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6096        return copyTree(rightPath, leftPath, 0, message);
6097      }
6098    },
6099    leftDeleteDirectory {
6100      Direction targetDefault() { return rightCreateDirectory; }
6101      Direction targetLeft() { return leftDeleteDirectory; }
6102      Direction targetRight() { return rightCreateDirectory; }
6103      Direction nextDirection() { return rightCreateDirectory; }
6104      void icon(Graphics g, int columnWidth, int rowHeight) {
6105        int w = columnWidth;
6106        int h = rowHeight;
6107        int q = h/4;
6108        int x = w/2;
6109        int y = h/2;
6110        g.setColor(RED);
6111        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
6112                                -3*q+x,-4*q+x,-3*q+x,-4*q+x,-3*q+x,
6113                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
6114          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6115                    2*q+y,1*q+y,0*q+y,-1*q+y,-2*q+y,
6116                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6117          17);
6118      }
6119      boolean defaultCheck() { return false; }
6120      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6121        return deleteTree(leftPath, message);
6122      }
6123    },
6124    leftDeleteFile {
6125      Direction targetDefault() { return rightCreateFile; }
6126      Direction targetLeft() { return leftDeleteFile; }
6127      Direction targetRight() { return rightCreateFile; }
6128      Direction nextDirection() { return rightCreateFile; }
6129      void icon(Graphics g, int columnWidth, int rowHeight) {
6130        int w = columnWidth;
6131        int h = rowHeight;
6132        int q = h/4;
6133        int x = w/2;
6134        int y = h/2;
6135        g.setColor(RED);
6136        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
6137                                -1*q+x,
6138                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
6139          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6140                    0*q+y,
6141                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6142          13);
6143      }
6144      boolean defaultCheck() { return false; }
6145      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6146        return deleteTree(leftPath, message);
6147      }
6148    },
6149    rightDirectoryOverwriteLeftFile {
6150      Direction targetDefault() { return rightDirectoryOverwriteLeftFile; }
6151      Direction targetLeft() { return rightDirectoryOverwriteLeftFile; }
6152      Direction targetRight() { return leftFileOverwriteRightDirectory; }
6153      Direction nextDirection() { return leftFileOverwriteRightDirectory; }
6154      void icon(Graphics g, int columnWidth, int rowHeight) {
6155        int w = columnWidth;
6156        int h = rowHeight;
6157        int q = h/4;
6158        int x = w/2;
6159        int y = h/2;
6160        g.setColor(BROWN);
6161        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6162                                -2*q+x,
6163                                0*q+x,0*q+x,2*q+x},
6164          new int[]{-2*q+y,-1*q+y,-2*q+y,
6165                    0*q+y,
6166                    2*q+y,1*q+y,2*q+y},
6167          7);
6168      }
6169      boolean defaultCheck() { return false; }
6170      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6171        return copyTree(rightPath, leftPath, replaceFileWithDirectory, null);
6172      }
6173    },
6174    rightFileOverwriteLeftDirectory {
6175      Direction targetDefault() { return leftDirectoryOverwriteRightFile; }
6176      Direction targetLeft() { return rightFileOverwriteLeftDirectory; }
6177      Direction targetRight() { return leftDirectoryOverwriteRightFile; }
6178      Direction nextDirection() { return leftDirectoryOverwriteRightFile; }
6179      void icon(Graphics g, int columnWidth, int rowHeight) {
6180        int w = columnWidth;
6181        int h = rowHeight;
6182        int q = h/4;
6183        int x = w/2;
6184        int y = h/2;
6185        g.setColor(BROWN);
6186        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6187                                -2*q+x,
6188                                0*q+x,0*q+x,2*q+x},
6189          new int[]{0*q+y,-1*q+y,-2*q+y,
6190                    0*q+y,
6191                    2*q+y,1*q+y,0*q+y},
6192          7);
6193      }
6194      boolean defaultCheck() { return false; }
6195      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6196        return copyTree(rightPath, leftPath, replaceDirectoryWithFile, null);
6197      }
6198    },
6199    leftCreateLink {
6200      Direction targetDefault() { return leftCreateLink; }
6201      Direction targetLeft() { return leftCreateLink; }
6202      Direction targetRight() { return rightDeleteLink; }
6203      Direction nextDirection() { return rightDeleteLink; }
6204      void icon(Graphics g, int columnWidth, int rowHeight) {
6205        int w = columnWidth;
6206        int h = rowHeight;
6207        int q = h/4;
6208        int x = w/2;
6209        int y = h/2;
6210        g.setColor(BLUE);
6211        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6212                                -2*q+x,
6213                                0*q+x,0*q+x,2*q+x},
6214          new int[]{-1*q+y,-1*q+y,-2*q+y,
6215                    0*q+y,
6216                    2*q+y,1*q+y,1*q+y},
6217          7);
6218      }
6219      boolean defaultCheck() { return false; }
6220      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6221        byte[] target = rightPath.readLink();
6222        if(target == null) return false;
6223        return leftPath.makeLinkTo(target);
6224      }
6225    },
6226    leftDeleteLink {
6227      Direction targetDefault() { return rightCreateLink; }
6228      Direction targetLeft() { return leftDeleteLink; }
6229      Direction targetRight() { return rightCreateLink; }
6230      Direction nextDirection() { return rightCreateLink; }
6231      void icon(Graphics g, int columnWidth, int rowHeight) {
6232        int w = columnWidth;
6233        int h = rowHeight;
6234        int q = h/4;
6235        int x = w/2;
6236        int y = h/2;
6237        g.setColor(BLUE);
6238        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
6239                                -1*q+x,
6240                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
6241          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6242                    0*q+y,
6243                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6244          13);
6245      }
6246      boolean defaultCheck() { return false; }
6247      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6248        return deleteTree(leftPath, message);
6249      }
6250    },
6251    rightLinkOverwriteLeft {
6252      Direction targetDefault() { return leftOverwriteRightLink; }
6253      Direction targetLeft() { return rightLinkOverwriteLeft; }
6254      Direction targetRight() { return leftOverwriteRightLink; }
6255      Direction nextDirection() { return leftOverwriteRightLink; }
6256      void icon(Graphics g, int columnWidth, int rowHeight) {
6257        int w = columnWidth;
6258        int h = rowHeight;
6259        int q = h/4;
6260        int x = w/2;
6261        int y = h/2;
6262        g.setColor(BLACK);
6263
6264        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6265                                -2*q+x,
6266                                0*q+x,0*q+x,2*q+x},
6267          new int[]{-1*q+y,-1*q+y,-2*q+y,
6268                    0*q+y,
6269                    2*q+y,1*q+y,1*q+y},
6270          7);
6271      }
6272      boolean defaultCheck() { return false; }
6273      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6274        if(!deleteTree(leftPath, message)) return false;
6275        byte[] target = rightPath.readLink();
6276        if(target == null) return false;
6277        return leftPath.makeLinkTo(target);
6278      }
6279    },
6280    rightOverwriteLeftLink {
6281      Direction targetDefault() { return rightOverwriteLeftLink; }
6282      Direction targetLeft() { return rightOverwriteLeftLink; }
6283      Direction targetRight() { return leftLinkOverwriteRight; }
6284      Direction nextDirection() { return leftLinkOverwriteRight; }
6285      void icon(Graphics g, int columnWidth, int rowHeight) {
6286        int w = columnWidth;
6287        int h = rowHeight;
6288        int q = h/4;
6289        int x = w/2;
6290        int y = h/2;
6291        g.setColor(BLACK);
6292        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
6293                                -1*q+x,-1*q+x,-3*q+x,-1*q+x,-1*q+x,
6294                                0*q+x,0*q+x,2*q+x},
6295          new int[]{-1*q+y,-1*q+y,-2*q+y,
6296                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
6297                    2*q+y,1*q+y,1*q+y},
6298          11);
6299      }
6300      boolean defaultCheck() { return false; }
6301      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6302        deleteTree(leftPath, message);
6303        return copyTree(rightPath, leftPath, 0, message);
6304      }
6305    },
6306    rightOverwriteNewer {
6307      Direction targetDefault() { return leftOverwriteOlder; }
6308      Direction targetLeft() { return leftOverwriteOlder; }
6309      Direction targetRight() { return rightOverwriteNewer; }
6310      Direction nextDirection() { return leftOverwriteOlder; }
6311      void icon(Graphics g, int columnWidth, int rowHeight) {
6312        int w = columnWidth;
6313        int h = rowHeight;
6314        int q = h/4;
6315        int x = w/2;
6316        int y = h/2;
6317        g.setColor(MAGENTA);
6318        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6319                                2*q+x,
6320                                0*q+x,0*q+x,-2*q+x},
6321          new int[]{-1*q+y,-1*q+y,-2*q+y,
6322                    0*q+y,
6323                    2*q+y,1*q+y,1*q+y},
6324          7);
6325      }
6326      boolean defaultCheck() { return false; }
6327      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6328        return rightPath.copyFileFrom(leftPath);
6329      }
6330      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
6331        long time = leftPath.getMTime();
6332        if(time == 0) return false;
6333        return rightPath.setMTime(time);
6334      }
6335    },
6336    rightOverwriteOlder {
6337      Direction targetDefault() { return rightOverwriteOlder; }
6338      Direction targetLeft() { return leftOverwriteNewer; }
6339      Direction targetRight() { return rightOverwriteOlder; }
6340      Direction nextDirection() { return leftOverwriteNewer; }
6341      void icon(Graphics g, int columnWidth, int rowHeight) {
6342        int w = columnWidth;
6343        int h = rowHeight;
6344        int q = h/4;
6345        int x = w/2;
6346        int y = h/2;
6347        g.setColor(RED);
6348        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6349                                2*q+x,
6350                                0*q+x,0*q+x,-2*q+x},
6351          new int[]{-1*q+y,-1*q+y,-2*q+y,
6352                    0*q+y,
6353                    2*q+y,1*q+y,1*q+y},
6354          7);
6355      }
6356      boolean defaultCheck() { return true; }
6357      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6358        return rightPath.copyFileFrom(leftPath);
6359      }
6360      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
6361        long time = leftPath.getMTime();
6362        if(time == 0) return false;
6363        return rightPath.setMTime(time);
6364      }
6365    },
6366    rightCreateFile {
6367      Direction targetDefault() { return rightCreateFile; }
6368      Direction targetLeft() { return leftDeleteFile; }
6369      Direction targetRight() { return rightCreateFile; }
6370      Direction nextDirection() { return leftDeleteFile; }
6371      void icon(Graphics g, int columnWidth, int rowHeight) {
6372        int w = columnWidth;
6373        int h = rowHeight;
6374        int q = h/4;
6375        int x = w/2;
6376        int y = h/2;
6377        g.setColor(GREEN);
6378        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6379                                2*q+x,
6380                                0*q+x,0*q+x,-2*q+x},
6381          new int[]{-1*q+y,-1*q+y,-2*q+y,
6382                    0*q+y,
6383                    2*q+y,1*q+y,1*q+y},
6384          7);
6385      }
6386      boolean defaultCheck() { return true; }
6387      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6388        return rightPath.copyFileFrom(leftPath);
6389      }
6390    },
6391    rightCreateDirectory {
6392      Direction targetDefault() { return rightCreateDirectory; }
6393      Direction targetLeft() { return leftDeleteDirectory; }
6394      Direction targetRight() { return rightCreateDirectory; }
6395      Direction nextDirection() { return leftDeleteDirectory; }
6396      void icon(Graphics g, int columnWidth, int rowHeight) {
6397
6398        int w = columnWidth;
6399        int h = rowHeight;
6400        int q = h/4;
6401        int x = w/2;
6402        int y = h/2;
6403        g.setColor(GREEN);
6404        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6405                                1*q+x,1*q+x,3*q+x,1*q+x,1*q+x,
6406                                0*q+x,0*q+x,-2*q+x},
6407          new int[]{-1*q+y,-1*q+y,-2*q+y,
6408                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
6409                    2*q+y,1*q+y,1*q+y},
6410          11);
6411      }
6412      boolean defaultCheck() { return true; }
6413      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6414        return copyTree(leftPath, rightPath, 0, message);
6415      }
6416    },
6417    rightDeleteDirectory {
6418      Direction targetDefault() { return leftCreateDirectory; }
6419      Direction targetLeft() { return leftCreateDirectory; }
6420      Direction targetRight() { return rightDeleteDirectory; }
6421      Direction nextDirection() { return leftCreateDirectory; }
6422      void icon(Graphics g, int columnWidth, int rowHeight) {
6423        int w = columnWidth;
6424        int h = rowHeight;
6425        int q = h/4;
6426        int x = w/2;
6427        int y = h/2;
6428        g.setColor(RED);
6429        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
6430                                3*q+x,4*q+x,3*q+x,4*q+x,3*q+x,
6431                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
6432          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6433                    2*q+y,1*q+y,0*q+y,-1*q+y,-2*q+y,
6434                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6435          17);
6436      }
6437      boolean defaultCheck() { return false; }
6438      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6439        return deleteTree(rightPath, message);
6440      }
6441    },
6442    rightDeleteFile {
6443      Direction targetDefault() { return leftCreateFile; }
6444      Direction targetLeft() { return leftCreateFile; }
6445      Direction targetRight() { return rightDeleteFile; }
6446      Direction nextDirection() { return leftCreateFile; }
6447      void icon(Graphics g, int columnWidth, int rowHeight) {
6448        int w = columnWidth;
6449        int h = rowHeight;
6450        int q = h/4;
6451        int x = w/2;
6452        int y = h/2;
6453        g.setColor(RED);
6454        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
6455                                1*q+x,
6456                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
6457          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6458                    0*q+y,
6459                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6460          13);
6461      }
6462      boolean defaultCheck() { return false; }
6463      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6464        return deleteTree(rightPath, message);
6465      }
6466    },
6467    leftDirectoryOverwriteRightFile {
6468      Direction targetDefault() { return leftDirectoryOverwriteRightFile; }
6469      Direction targetLeft() { return rightFileOverwriteLeftDirectory; }
6470      Direction targetRight() { return leftDirectoryOverwriteRightFile; }
6471      Direction nextDirection() { return rightFileOverwriteLeftDirectory; }
6472      void icon(Graphics g, int columnWidth, int rowHeight) {
6473        int w = columnWidth;
6474        int h = rowHeight;
6475        int q = h/4;
6476        int x = w/2;
6477        int y = h/2;
6478        g.setColor(BROWN);
6479        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6480                                2*q+x,
6481                                0*q+x,0*q+x,-2*q+x},
6482          new int[]{-2*q+y,-1*q+y,-2*q+y,
6483                    0*q+y,
6484                    2*q+y,1*q+y,2*q+y},
6485          7);
6486      }
6487      boolean defaultCheck() { return false; }
6488      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6489        return copyTree(leftPath, rightPath, replaceFileWithDirectory, null);
6490      }
6491    },
6492    leftFileOverwriteRightDirectory {
6493      Direction targetDefault() { return rightDirectoryOverwriteLeftFile; }
6494      Direction targetLeft() { return rightDirectoryOverwriteLeftFile; }
6495      Direction targetRight() { return leftFileOverwriteRightDirectory; }
6496      Direction nextDirection() { return rightDirectoryOverwriteLeftFile; }
6497      void icon(Graphics g, int columnWidth, int rowHeight) {
6498        int w = columnWidth;
6499        int h = rowHeight;
6500        int q = h/4;
6501        int x = w/2;
6502        int y = h/2;
6503        g.setColor(BROWN);
6504        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6505                                2*q+x,
6506                                0*q+x,0*q+x,-2*q+x},
6507          new int[]{0*q+y,-1*q+y,-2*q+y,
6508                    0*q+y,
6509                    2*q+y,1*q+y,0*q+y},
6510          7);
6511      }
6512      boolean defaultCheck() { return false; }
6513      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6514        return copyTree(leftPath, rightPath, replaceDirectoryWithFile, message);
6515      }
6516    },
6517    rightCreateLink {
6518      Direction targetDefault() { return rightCreateLink; }
6519      Direction targetLeft() { return leftDeleteLink; }
6520      Direction targetRight() { return rightCreateLink; }
6521      Direction nextDirection() { return leftDeleteLink; }
6522      void icon(Graphics g, int columnWidth, int rowHeight) {
6523        int w = columnWidth;
6524        int h = rowHeight;
6525        int q = h/4;
6526        int x = w/2;
6527        int y = h/2;
6528        g.setColor(BLUE);
6529        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6530                                2*q+x,
6531                                0*q+x,0*q+x,-2*q+x},
6532          new int[]{-1*q+y,-1*q+y,-2*q+y,
6533                    0*q+y,
6534                    2*q+y,1*q+y,1*q+y},
6535          7);
6536      }
6537      boolean defaultCheck() { return false; }
6538      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6539        byte[] target = leftPath.readLink();
6540        if(target == null) return false;
6541        return rightPath.makeLinkTo(target);
6542      }
6543    },
6544    rightDeleteLink {
6545      Direction targetDefault() { return leftCreateLink; }
6546      Direction targetLeft() { return leftCreateLink; }
6547      Direction targetRight() { return rightDeleteLink; }
6548      Direction nextDirection() { return leftCreateLink; }
6549      void icon(Graphics g, int columnWidth, int rowHeight) {
6550        int w = columnWidth;
6551        int h = rowHeight;
6552        int q = h/4;
6553        int x = w/2;
6554        int y = h/2;
6555        g.setColor(BLUE);
6556        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
6557                                1*q+x,
6558                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
6559          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
6560                    0*q+y,
6561                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
6562          13);
6563      }
6564      boolean defaultCheck() { return false; }
6565      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6566        return deleteTree(rightPath, message);
6567      }
6568    },
6569    leftLinkOverwriteRight {
6570      Direction targetDefault() { return rightOverwriteLeftLink; }
6571      Direction targetLeft() { return rightOverwriteLeftLink; }
6572      Direction targetRight() { return leftLinkOverwriteRight; }
6573      Direction nextDirection() { return rightOverwriteLeftLink; }
6574      void icon(Graphics g, int columnWidth, int rowHeight) {
6575        int w = columnWidth;
6576        int h = rowHeight;
6577        int q = h/4;
6578        int x = w/2;
6579        int y = h/2;
6580        g.setColor(BLACK);
6581        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6582                                2*q+x,
6583                                0*q+x,0*q+x,-2*q+x},
6584          new int[]{-1*q+y,-1*q+y,-2*q+y,
6585                    0*q+y,
6586                    2*q+y,1*q+y,1*q+y},
6587          7);
6588      }
6589      boolean defaultCheck() { return false; }
6590      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6591        if(!deleteTree(rightPath, message)) return false;
6592        byte[] target = leftPath.readLink();
6593        if(target == null) return false;
6594        return rightPath.makeLinkTo(target);
6595      }
6596    },
6597    leftOverwriteRightLink {
6598      Direction targetDefault() { return leftOverwriteRightLink; }
6599      Direction targetLeft() { return rightLinkOverwriteLeft; }
6600      Direction targetRight() { return leftOverwriteRightLink; }
6601      Direction nextDirection() { return rightLinkOverwriteLeft; }
6602      void icon(Graphics g, int columnWidth, int rowHeight) {
6603        int w = columnWidth;
6604        int h = rowHeight;
6605        int q = h/4;
6606        int x = w/2;
6607        int y = h/2;
6608        g.setColor(BLACK);
6609        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
6610                                1*q+x,1*q+x,3*q+x,1*q+x,1*q+x,
6611                                0*q+x,0*q+x,-2*q+x},
6612          new int[]{-1*q+y,-1*q+y,-2*q+y,
6613                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
6614                    2*q+y,1*q+y,1*q+y},
6615          11);
6616      }
6617      boolean defaultCheck() { return false; }
6618      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
6619        deleteTree(rightPath, message);
6620        return copyTree(leftPath, rightPath, 0, message);
6621      }
6622    };
6623
6624    /**
6625     * Returns the default Direction for this enum entry.
6626     *
6627     * @return the default Direction for this enum entry
6628     */
6629    abstract Direction targetDefault();
6630
6631    /**
6632     * Returns the left target for this enum entry.
6633     *
6634     * @return the left target for this enum entry
6635     */
6636    abstract Direction targetLeft();
6637
6638    /**
6639     * Returns the right target for this enum entry.
6640     *
6641     * @return the right target for this enum entry
6642     */
6643    abstract Direction targetRight();
6644
6645    /**
6646     * Returns the opposite target for this enum entry.
6647     *
6648     * @return the opposite target for this enum entry
6649     */
6650    abstract Direction nextDirection();
6651
6652    /**
6653     * Draws an icon indicating the operation for this enum entry.
6654     *
6655     * @param g the graphics context
6656     * @param columnWidth the width of the icon to draw
6657     * @param rowHeight the height of the icon to draw
6658     */
6659    abstract void icon(Graphics g, int columnWidth, int rowHeight);
6660
6661    /**
6662     * Returns true if this Direction should be checked by default.
6663     *
6664     * @return true if this Direction should be checked by default
6665     */
6666    abstract boolean defaultCheck();
6667
6668    /**
6669     * Perform the operation indicated by the icon.
6670     *
6671     * @param leftPath the left path for the operation or null
6672     * @param rightPath the right path for the operation or null
6673     * @param message a JLabel used for feedback
6674     * @return true if successful
6675     */
6676    abstract boolean doOperation(MyPath leftPath, MyPath rightPath,
6677                                 JLabel message);
6678    /**
6679     * Only update file times. Both sides must be files or nothing
6680     * happens and false is returned. Returns false unless overridden.
6681     *
6682     * @param leftPath the left path for the operation or null
6683     * @param rightPath the right path for the operation or null
6684     * @param message a JLabel used for feedback
6685     * @return true if successful
6686     */
6687    boolean doTime(MyPath leftPath, MyPath rightPath,
6688                   JLabel message) {
6689      return false;
6690    }
6691  } // enum Direction
6692
6693  Direction[] d = Direction.values(); ////////// maybe not here
6694
6695  /**
6696   * This class holds data for a line of the file comparison
6697   * table. It is a pure data structure.
6698   */
6699  static class CompareTableData {
6700    boolean check;
6701    TableData left;
6702    Direction direction; // current direction of transfer etc.
6703    TableData right;
6704
6705    CompareTableData(TableData left, Direction direction, TableData right) {
6706      this.check = direction.defaultCheck();
6707      this.left = left;
6708      this.direction = direction;
6709      this.right = right;
6710    }
6711
6712    @Override
6713    public String toString() {
6714      return String.valueOf(left) + " " + direction + " " + String.valueOf(right);
6715    }
6716  }
6717
6718  /**
6719   * Table of updates to perform. There are two ComboBoxes: the left
6720   * ComboBox and the right ComboBox. There is one table giving file
6721   * specs for both sides of transfer. Center column gives direction
6722   * of transfer. Far left column is check box for disabling transfer
6723   * or deletion.
6724   */
6725  class PathCompare extends JFrame {
6726    final static long serialVersionUID = 42;
6727
6728    MyPath left;
6729    MyPath right;
6730    ArrayList<CompareTableData> compareData = new ArrayList<CompareTableData>();
6731    JTable compareTable = new JTable();
6732    JLabel[] countLabels = new JLabel[Direction.values().length];
6733    JLabel[] countTotals = new JLabel[4];
6734    JLabel message;
6735    JButton refresh;
6736    JButton synchronize;
6737    boolean localXPDSTHack;
6738    /**
6739     * Compare modification times and return true if less than 2 seconds
6740     * different. This is to compensate for various precisions in time
6741     * stamps between opreating systems. If the windowsXPDSTHack is true
6742     * then file times plus or minus 1 hour plus or minus 2 seconds are
6743     * assumed to be equal (only set this flag on windows XP (and pray).
6744     *
6745     * @param x first file to compare
6746     * @param y second file to compare
6747     * @return true if the modification times are within 2 seconds of each other
6748     */
6749    int dateCompare(TableData x, TableData y) {
6750      long xtime = x.mtime;
6751      long ytime = y.mtime;
6752      long difference = Math.abs(xtime - ytime);
6753      if(difference < 2000) return 0;
6754      if(localXPDSTHack) {
6755        long hourDifference = Math.abs(difference - 3600000);
6756        if(hourDifference < 2000 && x.size == y.size) return 0;
6757      }
6758      return xtime - ytime < 0 ? -1 : 1;
6759    }
6760
6761    /**
6762     * Make table of differences between two directory trees. Caller
6763     * must remember roots. First two arguments must be
6764     * directories. The table should be empty initially and will be
6765     * filled in with differences. Note that file times on links are
6766     * unreliable.
6767     *
6768     * Must sort files/directories/links by name only ignoring
6769     * file/directory/link property.
6770     *
6771     * @param leftPath left-side directory
6772     * @param rightPath right-side directory
6773     * @param table resulting table of differences
6774     */
6775    void compareDirectories(MyPath leftPath, MyPath rightPath,
6776                            ArrayList<CompareTableData> table) {
6777      SwingUtilities.invokeLater(new Runnable() {
6778          @Override
6779          public void run() {
6780            message.setText(leftPath.fullName());
6781          }
6782        });
6783      // do name sort
6784      TreeSet<TableData> leftSet = new TreeSet<TableData>(dataNameComparator);
6785      leftSet.addAll(leftPath.getChildren(true));
6786      TreeSet<TableData> rightSet = new TreeSet<TableData>(dataNameComparator);
6787      rightSet.addAll(rightPath.getChildren(true));
6788      Iterator<TableData> leftFiles = leftSet.iterator();
6789      Iterator<TableData> rightFiles = rightSet.iterator();
6790      TableData leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
6791      TableData rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
6792      while(leftTable != null || rightTable != null) {
6793        int c = leftTable == null ? 1 : rightTable == null ? -1
6794          : dataNameComparator.compare(leftTable, rightTable);
6795        if(c < 0) {
6796          if(leftTable.path.isDirectory()) {
6797            table.add(new CompareTableData(leftTable,
6798                                           Direction.rightCreateDirectory,
6799                                           null));
6800          } else if(leftTable.path.isFile()) {
6801            table.add(new CompareTableData(leftTable,
6802                                           Direction.rightCreateFile, null));
6803          } else if(leftTable.path.isLink()) {
6804            table.add(new CompareTableData(leftTable,
6805                                           Direction.rightCreateLink,
6806                                           null));
6807
6808          } else {
6809            System.err.println("Unknown file type: " + leftTable.path.uri.toString());
6810          }
6811          leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
6812        } else if(c > 0) {
6813          if(rightTable.path.isDirectory()) {
6814            table.add(new CompareTableData(null,
6815                                           Direction.leftCreateDirectory,
6816                                           rightTable));
6817          } else if(rightTable.path.isFile()) {
6818            table.add(new CompareTableData(null,
6819                                           Direction.leftCreateFile,
6820                                           rightTable));
6821          } else if(rightTable.path.isLink()) {
6822            table.add(new CompareTableData(null,
6823                                           Direction.leftCreateLink,
6824                                           rightTable));
6825          } else {
6826            System.err.println("Unknown file type: " + rightTable.path.uri.toString());
6827          }
6828          rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
6829        } else {
6830          if(leftTable.path.isFile()) {
6831            if(rightTable.path.isFile()) {
6832              int comp = dateCompare(leftTable, rightTable);
6833              if(comp != 0) {
6834                if(comp > 0) {
6835                  table.add(new CompareTableData(leftTable,
6836                                                 Direction.rightOverwriteOlder,
6837                                                 rightTable));
6838                } else {
6839                  table.add(new CompareTableData(leftTable,
6840                                                 Direction.leftOverwriteOlder,
6841                                                 rightTable));
6842                }
6843              }
6844            } else if(rightTable.path.isDirectory()) {
6845              table.add(new CompareTableData(leftTable,
6846                                             Direction.rightDirectoryOverwriteLeftFile,
6847                                             rightTable));
6848            } else if(rightTable.path.isLink()) {
6849              table.add(new CompareTableData(leftTable,
6850                                             Direction.leftOverwriteRightLink,
6851                                             rightTable));
6852            } else {
6853              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
6854            }
6855          } else if(leftTable.path.isDirectory()) {
6856            if(rightTable.path.isFile()) {
6857              table.add(new CompareTableData(leftTable,
6858                                             Direction.leftDirectoryOverwriteRightFile,
6859                                             rightTable));
6860            } else if(rightTable.path.isDirectory()) {
6861              compareDirectories(leftTable.path,
6862                                 rightTable.path,
6863                                 table);
6864            } else if(rightTable.path.isLink()) {
6865              table.add(new CompareTableData(leftTable,
6866                                             Direction.leftOverwriteRightLink,
6867                                             rightTable));
6868
6869            } else {
6870              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
6871            }
6872          } else if(leftTable.path.isLink()) {
6873            if(rightTable.path.isFile()) {
6874              table.add(new CompareTableData(leftTable,
6875                                             Direction.rightOverwriteLeftLink,
6876                                             rightTable));
6877            } else if(rightTable.path.isDirectory()) {
6878              table.add(new CompareTableData(leftTable,
6879                                             Direction.rightOverwriteLeftLink,
6880                                             rightTable));
6881            } else if(rightTable.path.isLink()) {
6882              byte[] leftTarget = leftTable.path.readLink();
6883              byte[] rightTarget = rightTable.path.readLink();
6884              if(!Arrays.equals(leftTarget, rightTarget)) {
6885                int comp = dateCompare(leftTable, rightTable);
6886                if(comp != 0) {
6887                  if(comp > 0) {
6888                    table.add(new CompareTableData(leftTable,
6889                                                   Direction.rightOverwriteLeftLink,
6890                                                   rightTable));
6891                  } else {
6892                    table.add(new CompareTableData(leftTable,
6893                                                   Direction.leftOverwriteRightLink,
6894                                                   rightTable));
6895                  }
6896                }
6897              }
6898            } else {
6899              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
6900            }
6901          } else {
6902            System.err.println("Unknown file type: " + leftTable.path.uri.toString());
6903          }
6904          leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
6905          rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
6906        }
6907      }
6908    }
6909
6910    class ComboBoxPanel extends JPanel {
6911      final static long serialVersionUID = 42;
6912
6913      ComboBoxPanel(MyComboBox comboBox) {
6914        setLayout(new BorderLayout());
6915        add(new JButton(" ") {
6916            final static long serialVersionUID = 42;
6917
6918            {
6919              addMouseListener(new MouseAdapter() {
6920                  @Override
6921                  public void mouseClicked(MouseEvent e) {
6922                    MyPath child = comboBox.selectedPath;
6923                    MyPath parent = child.getParent();
6924                    if(child != null && parent != null) {
6925                      comboBox.getModel().setSelectedItem(parent);
6926                    }
6927                  }
6928                });
6929              setToolTipText("Goto parent directory");
6930            }
6931
6932            protected void paintComponent(Graphics g) {
6933              super.paintComponent(g);
6934              int w = getWidth();
6935              int h = getHeight();
6936              // upward arrow
6937              g.fillPolygon(new int[]{w/2-h/2, w/2, w/2+h/2},
6938                            new int[]{h/2+h/4, h/2-h/4, h/2+h/4}, 3);
6939            }
6940          }, BorderLayout.WEST);
6941        add(comboBox, BorderLayout.CENTER);
6942      }
6943    }
6944
6945    ///////////////////////////////////////////////////////////////
6946    /*
6947     * Here is where the JTable goes.
6948     */
6949    ///////////////////////////////////////////////////////////////
6950
6951    static final int checkCol = 0;
6952    static final int directionCol = 4;
6953
6954    class CompareTableModel extends AbstractTableModel {
6955      final static long serialVersionUID = 42;
6956
6957      String tableColumnNames[] = {"x", "path", "left size", "left date", "icon", "right size", "right date"};
6958
6959      @Override
6960      public int getColumnCount() {
6961        return tableColumnNames.length;
6962      }
6963
6964      @Override
6965      public int getRowCount() {
6966        return compareData.size();
6967      }
6968
6969      @Override
6970      public String getColumnName(int col) {
6971        return tableColumnNames[col];
6972      }
6973
6974      /**
6975       * Method used to get data to display after
6976       * getTableCellRendererComponent converts it for displaying.
6977       */
6978      @Override
6979      public Object getValueAt(int row, int col) {
6980        CompareTableData data = compareData.get(row);
6981        if(col == checkCol) return data.check;
6982        return null;
6983      }
6984
6985      /*
6986       * JTable uses this method to determine the default renderer/
6987       * editor for each cell. If we didn't implement this method then
6988       * the editor might not be a String editor.
6989       *
6990       * This is correct even though the cells contain Paths. It is
6991       * fixed in setValueAt
6992       */
6993      @Override
6994      public Class<?> getColumnClass(int c) {
6995        if(c == checkCol) return Boolean.class;
6996        if(c == directionCol) return Direction.class;
6997        return String.class; // all other columns rendered as strings
6998      }
6999
7000      /**
7001       * Disable the default editor if not the first column or the cell
7002       * contains a Direction.
7003       */
7004      @Override
7005      public boolean isCellEditable(int row, int col) {
7006        return col == checkCol || col == directionCol;
7007      }
7008
7009      /**
7010       * Apparently only used if cell is edited. We are using Strings as
7011       * the type of the table cell so must convert to Path and check
7012       * for equality. Must also check for duplicating another file in
7013       * the same directory.
7014       *
7015       * @param value new (edited) value of cell
7016       * @param row row of edited cell
7017       * @param col column of edited cell
7018       */
7019      @Override
7020      public void setValueAt(Object value, int row, int col) {
7021        if(col == checkCol) {
7022          boolean current = compareData.get(row).check;
7023          if(!value.equals(current)) {
7024            compareData.get(row).check = (Boolean)value;
7025            updateCounts();
7026            fireTableCellUpdated(row, col);
7027          }
7028        } else if(col == directionCol) {
7029          compareData.get(row).direction = (Direction)value;
7030          fireTableCellUpdated(row, col);
7031        }
7032      }
7033    }
7034
7035    CompareTableModel compareTableModel = new CompareTableModel();
7036
7037    PathCompare(String title, MyPath left, MyPath right) {
7038      super(title);
7039      this.left = left;
7040      this.right = right;
7041      initPathCompare(left, right);
7042    }
7043
7044    void updateCounts() {
7045      int[] checked = new int[Direction.values().length];
7046      int[] allValues = new int[Direction.values().length];
7047      int topChecked = 0;
7048      int bottomChecked = 0;
7049      int topAll = 0;
7050      int bottomAll = 0;
7051      for(CompareTableData data : compareData) {
7052        if(data.check) {
7053          ++checked[data.direction.ordinal()];
7054          if(data.direction.ordinal() < Direction.values().length/2) {
7055            ++topChecked;
7056          } else {
7057            ++bottomChecked;
7058          }
7059        }
7060        ++allValues[data.direction.ordinal()];
7061        if(data.direction.ordinal() < Direction.values().length/2) {
7062          ++topAll;
7063        } else {
7064          ++bottomAll;
7065        }
7066      }
7067      for(int i = 0; i < countLabels.length; ++i) {
7068        countLabels[i].setText(" " + addCommas("" + checked[i])
7069                               + '/' + addCommas("" + allValues[i]));
7070      }
7071      for(int i = 0; i < 4; ++i) {
7072        countTotals[i].setText("" + i);
7073      }
7074      countTotals[0].setText("Totals");
7075      countTotals[1].setText(" " + addCommas("" + topChecked)
7076                               + '/' + addCommas("" + topAll));
7077      countTotals[2].setText(" " + addCommas("" + (topChecked + bottomChecked))
7078                             + '/' + addCommas("" + (topAll + bottomAll)));
7079      countTotals[3].setText(" " + addCommas("" + bottomChecked)
7080                               + '/' + addCommas("" + bottomAll));
7081    }
7082
7083    void initPathCompare(MyPath leftPath, MyPath rightPath) {
7084      super.setTitle(leftPath.fullName() + "   " + rightPath.fullName());
7085
7086      MyComboBox leftBox = new MyComboBox(leftPath);
7087      MyComboBox rightBox = new MyComboBox(rightPath);
7088      JPanel comboPanel = new JPanel(new SpringLayout());
7089      ComboBoxPanel leftPanel = new ComboBoxPanel(leftBox);
7090      comboPanel.add(leftPanel);
7091      refresh = new JButton("refresh") {
7092          final static long serialVersionUID = 42;
7093          {
7094            addMouseListener(new MouseAdapter() {
7095                @Override
7096                public void mouseClicked(MouseEvent e) {
7097                  left = endEditing(leftBox);
7098                  right = endEditing(rightBox);
7099                  int buttonNumber = e.getButton();
7100                  switch(e.getButton()) {
7101                  case 1:
7102                    localXPDSTHack = windowsXPDSTHack;
7103                    break;
7104                  case 3:
7105                    localXPDSTHack = false;
7106                    break;
7107                  default: return;
7108                  }
7109                  compareData.clear();
7110                  Color background = getBackground();
7111                  Color synBackground = synchronize.getBackground();
7112                  if(background.equals(RED) ||
7113                     synBackground.equals(RED)) {
7114                    System.err.println("BUSY");
7115                    return;
7116                  }
7117                  setBackground(RED);
7118                  new Thread("refresh") {
7119                    @Override
7120                    public void run() {
7121                      compareDirectories(left, right, compareData);
7122                      SwingUtilities.invokeLater(new Runnable() {
7123                          @Override
7124                          public void run() {
7125                            updateCounts();
7126                            compareTableModel.fireTableDataChanged();
7127                            setBackground(background);
7128                          }
7129                        });
7130                    }
7131                  }.start();
7132                }
7133              });
7134          }
7135        };
7136      comboPanel.add(refresh);
7137      ComboBoxPanel rightPanel = new ComboBoxPanel(rightBox);
7138      comboPanel.add(rightPanel);
7139      SpringLayout layout = (SpringLayout)comboPanel.getLayout();
7140      SpringLayout.Constraints panelC = layout.getConstraints(comboPanel);
7141      SpringLayout.Constraints leftC = layout.getConstraints(leftPanel);
7142      SpringLayout.Constraints centerC = layout.getConstraints(refresh);
7143      SpringLayout.Constraints rightC = layout.getConstraints(rightPanel);
7144      leftC.setWidth(Spring.constant(10, 100, 1000));
7145      rightC.setWidth(Spring.constant(10, 100, 1000));
7146      Spring height = Spring.max(leftC.getHeight(), centerC.getHeight());
7147      height = Spring.max(height, rightC.getHeight());
7148      layout.putConstraint("North", leftPanel, 0, "North", refresh);
7149      layout.putConstraint("North", refresh, 0, "North", comboPanel);
7150      layout.putConstraint("North", rightPanel, 0, "North", refresh);
7151      layout.putConstraint("South", comboPanel, 0, "South", refresh);
7152      layout.putConstraint("West", leftPanel, 0, "West", comboPanel);
7153      layout.putConstraint("West", refresh, 0, "East", leftPanel);
7154      layout.putConstraint("West", rightPanel, 0, "East", refresh);
7155      layout.putConstraint("East", comboPanel, 0, "East", rightPanel);
7156      add(comboPanel, BorderLayout.NORTH);
7157      compareTable.setModel(compareTableModel);
7158      compareTable.setOpaque(true);
7159      compareTable.setAutoCreateColumnsFromModel(true);
7160      compareTable.setFillsViewportHeight(true);
7161      compareTable.setRowSelectionAllowed(true);
7162      compareTable.setColumnSelectionAllowed(false);
7163      compareTable.setCellSelectionEnabled(false);
7164      compareTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
7165      compareTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
7166      compareTable.setRowSelectionAllowed(true);
7167      compareTable.setColumnSelectionAllowed(false);
7168      compareTable.setDragEnabled(true);
7169      compareTable.setDropMode(DropMode.ON);
7170
7171      // for Direction column
7172      compareTable.setDefaultRenderer(Direction.class, new DefaultTableCellRenderer() {
7173        final static long serialVersionUID = 42;
7174
7175        Direction direction;
7176        //int column;
7177
7178        @Override
7179        public Component getTableCellRendererComponent(JTable table,
7180                                                       Object direction,
7181                                                       boolean isSelected,
7182                                                       boolean hasFocus,
7183                                                       int row, int column) {
7184          // don't call super to prevent selection from coloring background
7185          // super.getTableCellRendererComponent(table, direction,
7186          //                                     isSelected, hasFocus,
7187          //                                     row, column);
7188          this.direction = compareData.get(row).direction;
7189          //this.column = column;
7190          CompareTableData data = compareData.get(row);
7191          setToolTipText(null); ////////////////
7192          return this;
7193        }
7194
7195        @Override
7196        public void paint(Graphics g) {
7197          //super.paint(g); // enable this and above call to super to show text
7198          direction.icon(g,
7199                         compareTable.getColumnModel().getColumn(directionCol).getWidth(),
7200                         compareTable.getRowHeight() - 1);
7201        }
7202      });
7203      compareTable.setDefaultRenderer(String.class,
7204                                      new DefaultTableCellRenderer() {
7205        final static long serialVersionUID = 42;
7206
7207        @Override
7208        public Component getTableCellRendererComponent(JTable table,
7209                                                       Object value,
7210                                                       boolean isSelected,
7211                                                       boolean hasFocus,
7212                                                       int row,
7213                                                       int viewColumn) {
7214          // don't call super to prevent selection from coloring background
7215          // super.getTableCellRendererComponent(table, direction,
7216          //                                     isSelected, hasFocus,
7217          //                                     row, column);
7218          int column = table.convertColumnIndexToModel(viewColumn);
7219
7220          Color fg = table.getForeground();
7221          Color bg = table.getBackground();
7222
7223          setForeground(fg);
7224          setBackground(bg);
7225
7226          if(isSelected) {
7227            fg = table.getSelectionForeground();
7228            bg = table.getSelectionBackground();
7229          }
7230          setForeground(fg);
7231          setBackground(bg);
7232
7233          CompareTableData data = compareData.get(row);
7234          setToolTipText(null);
7235          switch(column) {
7236          case checkCol: {
7237            //setValue(data.check); ////////////
7238            break;
7239          }
7240          case 1: {
7241            MyPath base = null;
7242            MyPath path = null;
7243            if(data.left != null) {
7244              base = left;
7245              path = data.left.path;
7246            } else if(data.right != null) {
7247              base = right;
7248              path = data.right.path;
7249            } //else path = "Both entries are NULL";
7250            setValue(getSuffix(base, path));
7251            setHorizontalAlignment(JLabel.LEFT);
7252            setToolTipText(getSuffix(base, path).toString());
7253            break;
7254          }
7255          case 2: {
7256            if(data.left == null) {
7257              setValue("");
7258            } else {
7259              if(data.left.path.isLink()) {
7260                byte[] bytes = data.left.path.readLink();
7261                if(bytes == null) setValue("?????");
7262                else setValue(bytesToString(bytes));
7263                setHorizontalAlignment(JLabel.LEFT);
7264              } else {
7265                setValue(addCommas("" + data.left.size));
7266                setHorizontalAlignment(JLabel.RIGHT);
7267              }
7268              if(!isSelected && data.left.path.isLink()) {
7269                setBackground(LIGHT_YELLOW);
7270              }
7271            }
7272            break;
7273          }
7274          case 3: {
7275            if(data.left == null) {
7276              setValue("");
7277            } else {
7278              Date date = new Date(data.left.mtime);
7279              setValue(date.toString());
7280              setHorizontalAlignment(JLabel.LEFT);
7281              if(!isSelected && data.left.path.isLink()) {
7282                setBackground(LIGHT_YELLOW);
7283              }
7284            }
7285            break;
7286          }
7287            //case directionCol: return data.direction;
7288          case 5: {
7289            if(data.right == null) {
7290              setValue("");
7291            } else {
7292              if(data.right.path.isLink()) {
7293                byte[] bytes = data.right.path.readLink();
7294                if(bytes == null) setValue("?????");
7295                else setValue(bytesToString(bytes));
7296                setHorizontalAlignment(JLabel.LEFT);
7297              } else {
7298                setValue(addCommas("" + data.right.size));
7299                setHorizontalAlignment(JLabel.RIGHT);
7300              }
7301              if(!isSelected && data.right.path.isLink()) {
7302                setBackground(LIGHT_YELLOW);
7303              }
7304            }
7305            break;
7306          }
7307          case 6: {
7308            if(data.right == null) {
7309              setValue("");
7310            } else {
7311              Date date = new Date(data.right.mtime);
7312              setValue(date.toString());
7313              setHorizontalAlignment(JLabel.LEFT);
7314              if(!isSelected && data.right.path.isLink()) {
7315                setBackground(LIGHT_YELLOW);
7316              }
7317            }
7318            break;
7319          }
7320          default: throw new Error("Bad table column: " + column);
7321          }
7322          return this;
7323        }
7324      });
7325
7326      //compareTable.setPreferredSize(new Dimension(1000, 1500));
7327      int tableColumnWidths[] = {20,150,150,210,50,150,210};
7328      for(int i = 0; i < compareTableModel.tableColumnNames.length; ++i) {
7329        TableColumn column = compareTable.getColumnModel().getColumn(i);
7330        column.setPreferredWidth(tableColumnWidths[i]);
7331      }
7332
7333      ((DefaultTableCellRenderer)compareTable.getTableHeader().getDefaultRenderer())
7334        .setHorizontalAlignment(SwingConstants.LEFT);
7335
7336      compareTable.addMouseListener(new MouseAdapter() {
7337          @Override
7338          public void mouseClicked(MouseEvent e) {
7339            Point point = e.getPoint();
7340            int col = compareTable.convertColumnIndexToModel(compareTable.columnAtPoint(point));
7341            int row = compareTable.rowAtPoint(point);
7342            if(row < 0) return;
7343            TableData left = compareData.get(row).left;
7344            TableData right = compareData.get(row).right;
7345            if(col == directionCol && row >= 0 && e.getButton() == 1) {
7346              CompareTableData data = compareData.get(row);
7347              compareTableModel.setValueAt(data.direction.nextDirection(), row, col);
7348              updateCounts();
7349            } else if(col == directionCol && row >= 0 && e.getButton() == 3) {
7350              if(left != null && left.path.isFile() &&
7351                 right != null && right.path.isFile()) {
7352                diff(left.path, right.path);
7353              }
7354            } else if(e.getButton() == 3) {
7355              if(left != null) {
7356                if(col == 2 && left.path.isFile()) { // left size column
7357                  makeFileEditWindow(left.path);
7358                } else if(col == 3) { // left date column
7359                  makeBrowser(left.path);
7360                }
7361              }
7362              if(right != null) {
7363                if(col == 5 && right.path.isFile()) { // right size column
7364                  makeFileEditWindow(right.path);
7365                } else if(col == 6) {
7366                  makeBrowser(right.path);
7367                }
7368              }
7369            }
7370          }
7371        });
7372
7373      JScrollPane tableScrollPane = new JScrollPane(compareTable);
7374      int scrollWidth = 0;
7375      for(int i = 0; i < tableColumnWidths.length; ++i) {
7376        scrollWidth += tableColumnWidths[i];
7377      }
7378      scrollWidth += 3*(tableColumnWidths.length - 1); // 3 pixels separation
7379      tableScrollPane.setPreferredSize(new Dimension(scrollWidth, 500));
7380      add(tableScrollPane, BorderLayout.CENTER);
7381
7382      JPanel bottom = new JPanel(new GridBagLayout());
7383      JPanel buttons = new JPanel(new GridLayout(2, 0));
7384
7385      buttons.add(new JButton("Select All") {
7386          final static long serialVersionUID = 42;
7387
7388          {
7389            addMouseListener(new MouseAdapter() {
7390                @Override
7391                public void mouseClicked(MouseEvent e) {
7392                  compareTable.selectAll();
7393                }
7394              });
7395          }
7396        });
7397
7398      buttons.add(new JButton("Check All") {
7399          final static long serialVersionUID = 42;
7400
7401          {
7402            addMouseListener(new MouseAdapter() {
7403                @Override
7404                public void mouseClicked(MouseEvent e) {
7405                  for(int row = 0; row < compareData.size(); ++row) {
7406                    if(!compareData.get(row).check) compareTableModel.setValueAt(true, row, checkCol);
7407                  }
7408                  updateCounts();
7409                }
7410              });
7411          }
7412        });
7413
7414      buttons.add(new JButton("Check Selected") {
7415          final static long serialVersionUID = 42;
7416
7417          {
7418            addMouseListener(new MouseAdapter() {
7419                @Override
7420                public void mouseClicked(MouseEvent e) {
7421                  for(int row : compareTable.getSelectedRows()) {
7422                    if(!compareData.get(row).check) compareTableModel.setValueAt(true, row, checkCol);
7423                  }
7424                  updateCounts();
7425                }
7426              });
7427          }
7428        });
7429
7430      buttons.add(new JButton("Target Left") {
7431          final static long serialVersionUID = 42;
7432
7433          {
7434            addMouseListener(new MouseAdapter() {
7435                @Override
7436                public void mouseClicked(MouseEvent e) {
7437                  for(int row : compareTable.getSelectedRows()) {
7438                    CompareTableData data = compareData.get(row);
7439                    compareTableModel.setValueAt(data.direction.targetLeft(), row, directionCol);
7440
7441                  }
7442                  updateCounts();
7443                }
7444              });
7445          }
7446        });
7447
7448      buttons.add(new JButton("Target Right") {
7449          final static long serialVersionUID = 42;
7450
7451          {
7452            addMouseListener(new MouseAdapter() {
7453                @Override
7454                public void mouseClicked(MouseEvent e) {
7455                  for(int row : compareTable.getSelectedRows()) {
7456                    CompareTableData data = compareData.get(row);
7457                    compareTableModel.setValueAt(data.direction.targetRight(), row, directionCol);
7458
7459                  }
7460                  updateCounts();
7461                }
7462              });
7463          }
7464        });
7465
7466      buttons.add(new JButton("Unselect All") {
7467          final static long serialVersionUID = 42;
7468
7469          {
7470            addMouseListener(new MouseAdapter() {
7471                @Override
7472                public void mouseClicked(MouseEvent e) {
7473                  compareTable.clearSelection();
7474                }
7475              });
7476          }
7477        });
7478
7479      buttons.add(new JButton("Uncheck All") {
7480          final static long serialVersionUID = 42;
7481
7482          {
7483            addMouseListener(new MouseAdapter() {
7484                @Override
7485                public void mouseClicked(MouseEvent e) {
7486                  for(int row = 0; row < compareData.size(); ++row) {
7487                    if(compareData.get(row).check) compareTableModel.setValueAt(false, row, checkCol);
7488                  }
7489                  updateCounts();
7490                }
7491              });
7492          }
7493        });
7494
7495      buttons.add(new JButton("Uncheck Selected") {
7496          final static long serialVersionUID = 42;
7497
7498          {
7499            addMouseListener(new MouseAdapter() {
7500                @Override
7501                public void mouseClicked(MouseEvent e) {
7502                  for(int row : compareTable.getSelectedRows()) {
7503                    if(compareData.get(row).check) compareTableModel.setValueAt(false, row, checkCol);
7504                  }
7505                  updateCounts();
7506                }
7507              });
7508          }
7509        });
7510
7511      buttons.add(new JButton("Default Target") {
7512          final static long serialVersionUID = 42;
7513
7514          {
7515            addMouseListener(new MouseAdapter() {
7516                @Override
7517                public void mouseClicked(MouseEvent e) {
7518                  for(int row : compareTable.getSelectedRows()) {
7519                    CompareTableData data = compareData.get(row);
7520                    compareTableModel.setValueAt(data.direction.targetDefault(), row, directionCol);
7521
7522                  }
7523                  updateCounts();
7524                }
7525              });
7526          }
7527        });
7528
7529      synchronize = new JButton("Synchronize") {
7530          final static long serialVersionUID = 42;
7531
7532          {
7533            addMouseListener(new MouseAdapter() {
7534                @Override
7535                public void mouseClicked(MouseEvent e) {
7536                  int buttonNumber = e.getButton();
7537                  if(buttonNumber != 1 && buttonNumber != 3) return;
7538                  Color background = getBackground();
7539                  Color rBack = refresh.getBackground();
7540                  if(background.equals(RED) ||
7541                     rBack.equals(RED)) {
7542                    System.err.println("BUSY");
7543                    return;
7544                  }
7545                  setBackground(RED);
7546                  new Thread("Synchronize") {
7547                    @Override
7548                    public void run() {
7549                      loop: for(int i = 0; i < compareData.size(); ++i) {
7550                        final int ii = i;
7551                        CompareTableData data = compareData.get(i);
7552                        if(data.check) {
7553                          MyPath l, r;
7554                          String s = null;
7555                          if(data.left != null) {
7556                            l = data.left.path;
7557                            s = getSuffix(left, l);
7558                          } else {
7559                            l = changePrefix(right, left, data.right.path);
7560                          }
7561                          if(data.right != null) {
7562                            r = data.right.path;
7563                            s = getSuffix(right, r);
7564                          } else {
7565                            r = changePrefix(left, right, data.left.path);
7566                          }
7567                          final String suffix = s;
7568                          SwingUtilities.invokeLater(new Runnable() {
7569                              @Override
7570                              public void run() {
7571                                message.setText(suffix);
7572                              }
7573                            });
7574                          boolean b;
7575                          if(buttonNumber == 1) {
7576                            b = data.direction.doOperation(l, r, message);
7577                          } else if(buttonNumber == 3) {
7578                            b = data.direction.doTime(l, r, message);
7579                          } else {
7580                            break loop;
7581                          }
7582                          if(b) {
7583                            SwingUtilities.invokeLater(new Runnable() {
7584                                @Override
7585                                public void run() {
7586                                  compareTableModel.setValueAt(false, ii, checkCol);
7587                                }
7588                              });
7589                          }
7590                        }
7591                      }
7592                      SwingUtilities.invokeLater(new Runnable() {
7593                          @Override
7594                          public void run() {
7595                            setBackground(background);
7596                          }
7597                        });
7598                    }
7599                  }.start();
7600                }
7601              });
7602          }
7603        };
7604      buttons.add(synchronize);
7605
7606      bottom.add(buttons, new GridBagConstraints() {
7607          final static long serialVersionUID = 42;
7608          {
7609          gridx = 0;
7610          gridy = 0;
7611          gridwidth = REMAINDER;
7612          anchor = LINE_START;
7613          fill = HORIZONTAL;
7614          weightx = 0.5;
7615          }
7616        });
7617
7618      JPanel counts = new JPanel(new GridLayout(4, 0));
7619
7620      for(int h = 0; h < d.length; h += d.length/2) { // two rows
7621        JLabel label = new JLabel("Total");
7622        counts.add(label);
7623        countTotals[h == 0 ? 0 : 2] = label;
7624        for(int i = h; i < h + d.length/2; ++i) {
7625          int j = i;
7626          counts.add(new JLabel(" ") {
7627              final static long serialVersionUID = 42;
7628
7629              {
7630                addMouseListener(new MouseAdapter() {
7631                    @Override
7632                    public void mouseClicked(MouseEvent e) {
7633                      for(int k = 0; k < compareData.size(); ++k) {
7634                        if(!compareTable.isRowSelected(k)
7635                           && compareData.get(k).direction.ordinal() == j) {
7636                          compareTable.addRowSelectionInterval(k, k);
7637                        }
7638                      }
7639                    }
7640                  });
7641                setToolTipText("Select matching rows: " + d[j]);
7642              }
7643
7644              @Override
7645              public void paint(Graphics g) {
7646                super.paint(g); // enable this to super to show text
7647                d[j].icon(g, getWidth(), getHeight() - 1);
7648              }
7649            });
7650        }
7651        label = new JLabel("B");
7652        counts.add(label);
7653        countTotals[h == 0 ? 1 : 3] = label;
7654        for(int i = h; i < h + d.length/2; ++i) {
7655          int j = i;
7656          JLabel count = new JLabel("" + j, SwingConstants.CENTER);
7657          //counts.add(new JLabel("A"));
7658          countLabels[j] = count;
7659          counts.add(count); // must add to an array for updating
7660        }
7661      }
7662      bottom.add(counts, new GridBagConstraints() {
7663          final static long serialVersionUID = 42;
7664          {
7665          gridx = 0;
7666          gridy = 1;
7667          gridwidth = REMAINDER;
7668          anchor = LINE_START;
7669          fill = HORIZONTAL;
7670          weightx = 0.5;
7671          }
7672        });
7673
7674      message = new JLabel("message", SwingConstants.LEFT);
7675      bottom.add(message, new GridBagConstraints() {
7676          final static long serialVersionUID = 42;
7677          {
7678            gridx = 0;
7679            gridy = 2;
7680            fill = HORIZONTAL;
7681            weightx = 0.5;
7682          }
7683        });
7684
7685      add(bottom, BorderLayout.SOUTH);
7686
7687      addWindowListener(new WindowAdapter() {
7688          @Override
7689          public void windowClosing(WindowEvent e) {
7690            if(--windowCount == 0) {
7691              finish();
7692            }
7693            dispose();
7694          }
7695        });
7696      ++windowCount;
7697      updateCounts();
7698    }
7699  } // class PathCompare extends JFrame
7700
7701  /**************************************************************
7702   * A SSHWindow is a shell window on a remote system.
7703   **************************************************************/
7704  static class SSHWindow extends JFrame {
7705    final static long serialVersionUID = 42;
7706
7707    final byte ALERT_CODE = 0x07; // ALERT (BELL) code
7708    final byte BACKSPACE_CODE = 0x08; // backspace
7709    final byte ESC_CODE = 0X1b; // ESC code
7710    final byte CNTRL_O = 0x0f; // cntrl-o
7711
7712    final Color[] colors
7713      = new Color[]{BLACK, RED, GREEN, YELLOW,
7714                    BLUE, MAGENTA, CYAN, WHITE};
7715
7716    Session session = null;
7717    ChannelShell channel = null;
7718    InputStream inputStream = null;
7719    OutputStream outputStream = null;
7720    ChannelShell channelShell;
7721    JTextPane text = new JTextPane();
7722    StyledDocument doc = text.getStyledDocument();
7723    int charWidth;
7724    int charHeight;
7725    int cursorLocation = 0; // don't use textPane caret
7726    Style style = text.addStyle("", null);
7727    JScrollPane scroll = new JScrollPane(text);
7728    String title;
7729    boolean dragging = false; // are we currently dragging a selection?
7730
7731    {
7732      // must come first
7733      text.addMouseListener(new MouseListener() {
7734          @Override
7735          public void mouseEntered(MouseEvent e) {
7736            //e.consume();
7737          }
7738          @Override
7739          public void mouseExited(MouseEvent e) {
7740            //e.consume();
7741          }
7742          @Override
7743          public void mousePressed(MouseEvent e) {
7744            e.consume();
7745            switch(e.getButton()) {
7746            case 1: {
7747              Point p = e.getPoint();
7748              int i = text.viewToModel(p);
7749              text.setCaretPosition(i);
7750              dragging = true;
7751              break;
7752            }
7753            case 2: {
7754              try {
7755                String s = (String)clipboard.getData(DataFlavor.stringFlavor);
7756                outputStream.write(s.getBytes("UTF-8"));
7757                outputStream.flush();
7758              } catch(IOException ex) {
7759                ex.printStackTrace();
7760              } catch(UnsupportedFlavorException ex) {
7761                //ex.printStackTrace();
7762                try {
7763                  String s = (String)toolkit.getSystemClipboard().getData(DataFlavor.stringFlavor);
7764                  outputStream.write(s.getBytes("UTF-8"));
7765                  outputStream.flush();
7766                } catch(UnsupportedFlavorException | IOException exx) {
7767                  System.err.println(exx);
7768                  exx.printStackTrace();
7769                }
7770              } finally {}
7771              break;
7772            }
7773            case 3: {
7774              String user = session.getUserName();
7775              String host = session.getHost();
7776              String userHost = user + '@' + host;
7777              makeBrowser(stringToMyPath("//" + userHost));
7778              break;
7779            }
7780            default: {
7781              System.out.println(e.getButton());
7782              break;
7783            }
7784            }
7785          }
7786          @Override
7787          public void mouseReleased(MouseEvent e) {
7788            e.consume();
7789            dragging = false;
7790            if(e.getButton() == 1) {
7791              Point p = e.getPoint();
7792              int i = text.viewToModel(p);
7793              text.moveCaretPosition(i);
7794              // why is this not needed?
7795              //clipboard.setContents(new StringSelection(text.getSelectedText()), null);
7796            }
7797          }
7798          @Override
7799          public void mouseClicked(MouseEvent e) {
7800            e.consume();
7801          }
7802        });
7803      text.addMouseMotionListener(new MouseMotionListener() {
7804          @Override
7805          public void mouseMoved(MouseEvent e) {
7806            e.consume();
7807          }
7808          @Override
7809          public void mouseDragged(MouseEvent e) {
7810            e.consume();
7811            if(dragging) {
7812              Point p = e.getPoint();
7813              int i = text.viewToModel(p);
7814              text.moveCaretPosition(i);
7815            }
7816          }
7817        });
7818
7819      // needed to intercept mouse events
7820      // this must come after the other text mouse listeners
7821      text.setCaret(new DefaultCaret() {
7822          final static long serialVersionUID = 42;
7823          {
7824            setBlinkRate(500);
7825          }
7826        });
7827    }
7828
7829    /**
7830     * Initialize a shell window on a remote system. The validation
7831     * for the connection uses the same validation as the file browser
7832     * windows. The process is in two parts:
7833     *
7834     * 1. For all bytes from the remote end, process them via the
7835     * state machine and adjust the terminal window appropriately.
7836     *
7837     * 2. For all characters typed or pasted to the terminal window,
7838     * possibly encode them and write to the remote end.
7839     *
7840     * Note that characters are not echoed directly but sent to the
7841     * remote end which generally sends them back to the terminal to
7842     * be displayed. Certain characters (RETURN, NEW LINE, BACKSPACE,
7843     * arrow keys) are not echoed directly but send appropriate
7844     * controls to the terminal.
7845     *
7846     * @param userHost the user@host of the system to open the terminal on
7847     */
7848    SSHWindow(String userHost) {
7849      super(userHost);
7850      userHost = userHost.intern();
7851      title = userHost;
7852      this.title = title;
7853      //scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);//////////////////
7854      StyleConstants.setFontFamily(style, "Monospaced");
7855      int fontSize = 15;
7856      StyleConstants.setFontSize(style, fontSize);
7857      FontRenderContext frc = new FontRenderContext(null, false, false);
7858      Font font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
7859      TextLayout layout = new TextLayout("n", font, frc);
7860      float ascent = layout.getAscent();
7861      float descent = layout.getDescent();
7862      float leading = layout.getLeading();
7863      charWidth = (int)layout.getAdvance();
7864      charHeight = (int)Math.ceil(ascent) + (int)Math.ceil(descent) + (int)Math.ceil(leading);
7865      //System.out.println(charWidth + " x " + charHeight);
7866      try {
7867        session = getSession(userHost);
7868        //channel = (ChannelShell)session.openChannel("shell");
7869
7870        ///*
7871        // can't get this to work
7872        //String xHostPort = "11.0"; ///////////////////////
7873        //int xhost = 11;
7874        //int xport = 6000;
7875        //        session.setX11Host("127.0.0.1");
7876        // should get $DISPLAY for port number
7877        //        session.setX11Port(6011); // need to interrogate $DISPLAY
7878        channel = (ChannelShell)session.openChannel("shell");
7879        //        channel.setXForwarding(true);
7880        //channel.setAgentForwarding(true); //////////
7881        //channel = (ChannelShell)session.openChannel("shell");
7882        //*/
7883
7884      } catch(SftpOpenFailedException e) {
7885        e.printStackTrace();
7886        throw new Error("SftpOpenFailedException");
7887      } catch(JSchException e) {
7888        e.printStackTrace();
7889        throw new Error("JSchException");
7890      }
7891      setSession(channel);
7892      enableInputMethods(false);
7893      text.setPreferredSize(new Dimension(800, 448));
7894      getContentPane().add(scroll);
7895      addWindowListener(new WindowAdapter() {
7896          @Override
7897          public void windowClosing(WindowEvent e) {
7898            if(channelShell != null) channelShell.disconnect();
7899            if(--windowCount == 0) {
7900              finish();
7901            }
7902            dispose();
7903          }
7904        });
7905      ++windowCount;
7906    }
7907
7908    void setSession(ChannelShell c) {
7909      channelShell = c;
7910      try {
7911        channel.connect(3*1000);
7912        inputStream = c.getInputStream();
7913        outputStream = c.getOutputStream();
7914      } catch(JSchException e) {
7915        //e.printStackTrace(); ///////// should channel fail to open
7916        System.err.println("Channel failed to open");
7917      } catch(IOException e) {
7918        e.printStackTrace();
7919      }
7920
7921      // process data from the remote end and display appropriately
7922      new Thread("SSH main loop") {
7923
7924        // default doc state
7925        Color defaultForeground = text.getForeground();
7926        Color defaultBackground = text.getBackground();
7927
7928        // current doc state
7929        Color currentForeground = defaultForeground;
7930        Color currentBackground = defaultBackground;
7931
7932        /**
7933         * Sets reverse video on the terminal. This just complements
7934         * the color values.
7935         */
7936        void setReverseVideo() {
7937          // int current = currentForeground.getRGB();
7938          // StyleConstants.setForeground(style, new Color(current ^ 0xffffff));
7939          // current = currentBackground.getRGB();
7940          // StyleConstants.setBackground(style, new Color(current ^ 0xffffff));
7941        }
7942
7943        /**
7944         * Sets the forground and background back to their (uncomplemented)
7945         * colors.
7946         */
7947        void resetReverseVideo() {
7948          // int current = currentForeground.getRGB();
7949          // StyleConstants.setForeground(style, new Color(current));
7950          // current = currentBackground.getRGB();
7951          // StyleConstants.setBackground(style, new Color(current));
7952        }
7953
7954        byte[] buffer = new byte[4096];
7955        int start; // first byte in buffer to process
7956        int end; // last valid byte in buffer + 1
7957        int index; // current byte to process
7958        byte b; // last read byte
7959
7960        ArrayList<Integer> params = new ArrayList<Integer>();
7961
7962        void read() throws IOException {
7963          normalizeBuffer(index);
7964          if(2*end > buffer.length) {
7965            for(int i = start; i < end; ++i) {
7966              buffer[i - start] = buffer[start];
7967            }
7968            end -= start;
7969            index -= start;
7970            start = 0;
7971          }
7972          if(inputStream == null) throw new IOException("can't get channel");
7973          int nBytes = inputStream.read(buffer, end, buffer.length - end);
7974          if(nBytes <= 0) throw new IOException("EOF or something wrong");
7975          end += nBytes;
7976        }
7977
7978        void setDefaults() {
7979          // should be more flexible ////////////////
7980          StyleConstants.setForeground(style, defaultForeground);
7981          currentForeground = defaultForeground;
7982          StyleConstants.setBackground(style, defaultBackground);
7983          currentBackground = defaultBackground;
7984          StyleConstants.setBold(style, false);
7985        }
7986
7987        /**
7988         * The buffer between start and index are characters that have
7989         * not yet been inserted into the document. Sends characters
7990         * from the buffer from start to index to the document and
7991         * then shifts the buffer if appropriate so start is
7992         * zero. Characters are inserted from start up to the
7993         * index. All characters in the buffer before start are not
7994         * needed so all bytes in the buffer can be shifted to make
7995         * start equal to zero.
7996         *
7997         * @param index the range from start to index to insert
7998         */
7999        void normalizeBuffer(int index) {
8000          if(start != index) {
8001            try {
8002              String s = new String(buffer, start, index - start, "UTF-8");
8003              try {
8004                if(cursorLocation + s.length() < doc.getLength()) {
8005                  doc.remove(cursorLocation, s.length());
8006                } else {
8007                  doc.remove(cursorLocation, doc.getLength() - cursorLocation);
8008                }
8009                doc.insertString(cursorLocation, s, style);
8010                cursorLocation += s.length();
8011              } catch(BadLocationException e) {
8012                e.printStackTrace();
8013              }
8014              SwingUtilities.invokeAndWait(new Runnable() {
8015                  @Override
8016                  public void run() {
8017                    text.setCaretPosition(cursorLocation);
8018                  }
8019                });
8020              start = index; // just wrote out String before index
8021            } catch(InterruptedException | InvocationTargetException | UnsupportedEncodingException e) {
8022              e.printStackTrace();
8023            }
8024          }
8025        }
8026
8027        /**
8028         * Reads the next byte from the remote end and advances the
8029         * index. If there are no more unprocessed bytes in the buffer
8030         * a read from the remote end is performed.
8031         *
8032         * @return the next unprocessed byte from the remote end
8033         */
8034        byte getByte() throws IOException {
8035          if(index >= end) read();
8036          return b = buffer[index++];
8037        }
8038
8039        /**
8040         * Reads the next integer from the unprocessed bytes in the
8041         * buffer. This method continues to read digits until a
8042         * non-digit is encountered. If the current byte is a
8043         * non-digit then the index is not incremented and zero is
8044         * returned.
8045         *
8046         * @return the integer that was read
8047         */
8048        int getInt() throws IOException {
8049          int n = 0;
8050          while(b >= '0' && b <= '9') {
8051            n = 10*n + b - '0';
8052            getByte();
8053          }
8054          return n;
8055        }
8056
8057        /**
8058         * Gets the next [control character] terminated string from
8059         * the buffer. The bytes are converted to a string using the
8060         * UTF-8 character set.
8061         *
8062         * @return the string that was read
8063         */
8064        String getString() throws IOException {
8065          byte[] bytes = new byte[256];
8066          int i;
8067          for(i = 0; i < bytes.length; ++i) {
8068            if(b >= ' ') {
8069              bytes[i] = b;
8070              getByte();
8071            } else break;
8072          }
8073          if(b != 7) System.err.println("bad string terminator: " + b);
8074          String s = new String(bytes, 0, i, "UTF-8");
8075          return s;
8076        }
8077
8078        /**
8079         * This method is for debugging.
8080         *
8081         * Prints the contents of the terminal window with \n and \r
8082         * replaced with N and R respectively. The position of the
8083         * cursor is indicated by the @ sign.
8084         *
8085         * @param label a label printed to identify the caller
8086         */
8087        void printText(String label) {
8088          try {
8089            System.out.println("printing text at: " + label);
8090            String s = doc.getText(0, doc.getLength());
8091            s = s.replace('\n', 'N');
8092            s = s.replace('\r', 'R');
8093            System.out.println("====|====|====|====|====|====|====|====|");
8094            System.out.println(s.substring(0, cursorLocation)
8095                               + '@' + s.substring(cursorLocation));
8096            System.out.println("====|====|====|====|====|====|====|====|");
8097          } catch(BadLocationException e) {
8098            e.printStackTrace();
8099          }
8100        }
8101
8102        /*
8103         * This is the Thread "run" method. It implements a state
8104         * machine where the state is kept in the program counter.
8105         */
8106        @Override
8107        public void run() {
8108          setDefaults();
8109          try {
8110            while(true) {
8111              getByte();
8112              switch(b) {
8113              case ESC_CODE: {
8114                normalizeBuffer(index - 1); // don't display ESC
8115                getByte();
8116                switch(b) {
8117                case ']': {
8118                  getByte();
8119                  int n1 = getInt();
8120                  if(b != ';') System.err.println("Format Error");
8121                  getByte();
8122                  switch(n1) {
8123                  case 0:
8124                  case 2: {
8125                    String s = getString();
8126                    //System.err.println("known escape sequence: ESC ] " + s);
8127                    getByte();
8128                    try {
8129                      SwingUtilities.invokeAndWait(new Runnable() {
8130                          @Override
8131                          public void run() {
8132                            setTitle(s);
8133                          }
8134                        });
8135                    } catch(InterruptedException | InvocationTargetException e) {
8136                      e.printStackTrace();
8137                    }
8138                    break;
8139                  }
8140                  default: {
8141                    String s = getString();
8142                    System.err.println("Unknown escape sequence: ESC ] " + s);
8143                  }
8144                  }
8145                  start = index;
8146                  break;
8147                }
8148                case '[': {
8149                  params.clear();
8150                  // done this way to insure at least 1 parameter
8151                  // is this correct????///////////
8152                  do {
8153                    getByte();
8154                    params.add(getInt());
8155                  } while(b == ';');
8156                  //System.out.println("known escape sequence: ESC [ " + params + (char)(b + 0));
8157                  switch(b) {
8158                  case 'A': {
8159                    System.out.println("skipping ESC-[" + params + 'A');
8160                    start = index;
8161                    break;
8162                  }
8163                  case 'B': {
8164                    System.out.println("skipping ESC-[" + params + 'B');
8165                    start = index;
8166                    break;
8167                  }
8168                  case 'C': { // move right # of columns
8169                    try {
8170                      ++cursorLocation; //// could be more
8171                      text.setCaretPosition(cursorLocation);
8172                      start = index;
8173                    } catch(IllegalArgumentException e) {
8174                      e.printStackTrace();
8175                    }
8176                    break;
8177                  }
8178                  case 'D': {
8179                    --cursorLocation; //// could be more
8180                    text.setCaretPosition(cursorLocation);
8181                    start = index;
8182                    break;
8183                  }
8184                  case 'H': {
8185                    System.out.println("skipping ESC-[" + params + 'H');
8186                    start = index;
8187                    break;
8188                  }
8189                  case 'J': {
8190                    System.out.println("skipping ESC-[" + params + 'J');
8191                    start = index;
8192                    break;
8193                  }
8194                  case 'K': {
8195                    ////////// 0 erase from cursor to end of line
8196                    ////////// 1 erase from start of line to cursor
8197                    ////////// 2 erase whole line
8198                    if(params.size() != 1) System.err.println("case K long params");
8199                    int n = params.get(0);
8200                    switch(n) {
8201                    case 0: {
8202                      int end = doc.getLength();
8203                      int pos = cursorLocation;
8204                      try {
8205                        doc.remove(pos, end - pos);
8206                      } catch(BadLocationException e) {
8207                        e.printStackTrace();
8208                      }
8209                      break;
8210                    }
8211                    default: {
8212                      System.err.println("Unknown escape sequence: ESC [" + params + " K");
8213                    }
8214                    }
8215                    start = index;
8216                    break;
8217                  }
8218                  case 'm': {
8219                    for(int i = 0; i < params.size(); ++i) {
8220                      int n = params.get(i);
8221                      switch(n) {
8222                      case 0: {
8223                        setDefaults();
8224                        start = index;
8225                        break;
8226                      }
8227                      case 1: {
8228                        StyleConstants.setBold(style, true);
8229                        break;
8230                      }
8231                      case 7: {
8232                        setReverseVideo(); /////// copy to defaults?
8233                        break;
8234                      }
8235                      case 30:
8236                      case 31:
8237                      case 32:
8238                      case 33:
8239                      case 34:
8240                      case 35:
8241                      case 36:
8242                      case 37: {
8243                        StyleConstants.setForeground(style, colors[n%10]);
8244                        break;
8245                      }
8246                      case 40:
8247                      case 41:
8248                      case 42:
8249                      case 43:
8250                      case 44:
8251                      case 45:
8252                      case 46:
8253                      case 47: {
8254                        StyleConstants.setBackground(style, colors[n%10]);
8255                        break;
8256                      }
8257                      default: {
8258                        System.err.println("Unknown escape sequence: ESC [" + params + " m");
8259                      }
8260                      }
8261                    }
8262                    break;
8263                  }
8264                  case '?': { //// only exactly one argument
8265                    getByte();
8266                    int n = getInt();
8267                    System.out.println("skipping ESC-[" + '?' + n + (char)(b+0));
8268                    start = index;
8269                    break;
8270                  }
8271                    /* ignore ESC [ [ x    ----   also print this for debugging
8272                  case '[': {
8273                    break;
8274                  }
8275                    */
8276                  default: {
8277                    System.err.println("Unknown escape sequence: [" + params + (char)(b+0));
8278                  }
8279                  }
8280                  start = index;
8281                  break;
8282                } // case [
8283                case '=': {
8284                  System.out.println("skipping ESC-[" + params + '=');
8285                  start = index;
8286                  break;
8287                }
8288                case '>': {
8289                  System.out.println("skipping ESC-[" + params + '>');
8290                  start = index;
8291                  break;
8292                }
8293                default: {
8294                  System.err.println("Unknown escape sequence: ESC " + (char)(b+0));
8295                }
8296                }
8297                break;
8298              } // case ESC_CODE
8299              case ALERT_CODE: { // ignore alerts
8300                normalizeBuffer(index - 1); // up to ALERT //////////////////
8301                start = index;
8302                break;
8303              }
8304              case BACKSPACE_CODE: {
8305                normalizeBuffer(index - 1); // up to BACKSPACE ///////////////
8306                start = index;
8307                --cursorLocation;
8308                text.setCaretPosition(cursorLocation);
8309                break;
8310              }
8311              case '\r': {
8312                normalizeBuffer(index - 1); // skip return
8313                start = index;
8314                try {
8315                  int length = doc.getLength();
8316                  String s = doc.getText(0, length);
8317                  int begin = s.lastIndexOf('\n');
8318                  cursorLocation = begin + 1; ////****
8319                } catch(BadLocationException e) {
8320                  e.printStackTrace();
8321                }
8322                break;
8323              }
8324              case '\n': {
8325                cursorLocation = doc.getLength(); ////****
8326                break;
8327              }
8328              case CNTRL_O: { // change to default character set
8329                // not handled
8330                normalizeBuffer(index - 1); // up to ^O //////////////////
8331                start = index;
8332                break;
8333              }
8334              default: {
8335              }
8336              }
8337            }
8338          } catch(IOException e) {
8339            setTitle(title + " ***Disconnected***"); ///////////////////
8340            channel.disconnect();
8341
8342          } finally {
8343            // ordinary character
8344          }
8345        }
8346      }.start();
8347
8348      ////// handle window resize <ESC> ; height ; width t
8349      getContentPane().addComponentListener(new ComponentAdapter() {
8350          @Override
8351          public void componentResized(ComponentEvent e) {
8352            //System.out.println("window size: " + e.getComponent().getSize());
8353            //channelShell.setPtySize(40,24,320,480);
8354          }
8355        });
8356
8357      // process characters that are typed or pasted in and dispatch
8358      // to remote end.
8359      text.addKeyListener(new KeyListener() {
8360          @Override
8361          public void keyTyped(KeyEvent e) {
8362            char c = e.getKeyChar();
8363            try {
8364              if(outputStream == null) throw new IOException();
8365              if(c == 0x7f) {
8366                // so DEL deletes the character after the cursor
8367                outputStream.write(004);
8368                outputStream.flush();
8369              } else {
8370                outputStream.write(c);
8371                outputStream.flush();
8372              }
8373            } catch(IOException ex) {
8374              setTitle(title + " ***Disconnected***");
8375              channel.disconnect();
8376            }
8377            e.consume();
8378          }
8379          @Override
8380          public void keyPressed(KeyEvent e) {
8381            int code = e.getKeyCode();
8382            try {
8383              if(outputStream == null) throw new IOException();
8384              switch(code) {
8385              case 0x26: { // up arrow
8386                outputStream.write(new byte[]{033,'[','A'});
8387                outputStream.flush();
8388                break;
8389              }
8390              case 0x28: { // down arrow
8391                outputStream.write(new byte[]{033,'[','B'});
8392                outputStream.flush();
8393                break;
8394              }
8395              case 0x27: { // right arrow
8396                outputStream.write(new byte[]{033,'[','C'});
8397                outputStream.flush();
8398                break;
8399              }
8400              case 0x25: { // left arrow
8401                outputStream.write(new byte[]{033,'[','D'});
8402                outputStream.flush();
8403                break;
8404              }
8405              }
8406            } catch(IOException ex) {
8407              setTitle(title + " ***Disconnected***");
8408              channel.disconnect();
8409            }
8410            e.consume();
8411          }
8412
8413          @Override
8414          public void keyReleased(KeyEvent e) {
8415            e.consume();
8416          }
8417        });
8418    }
8419  } // static class SSHWindow extends JFrame
8420
8421  /**
8422   * Start a file browser on specified files. If no files are
8423   * specified then start a file browser on the roots of the file
8424   * system.
8425   *
8426   * -diff f1 f2 does a diff on f1 and f2 and shows result in window
8427   * -ssh x@y does an ssh to user x on machine y
8428   * [no params] brings up a file browser on root
8429   * f1 brings up a file browser on f1
8430   * f1 f2 brings up a file comparison window on f1, f2
8431   * [3 or more files] bring up a separate file browser on all files
8432   *
8433   * @param args list of files to start file browsers on
8434   */
8435  public static void main(String[] args) {
8436    if(args.length == 3 && args[0].equals("-diff")) {
8437      final MyPath leftPath = stringToMyPath(args[1]);
8438      final MyPath rightPath = stringToMyPath(args[2]);
8439      diff(leftPath, rightPath);
8440      return;
8441    }
8442    if(args.length >= 2 && args[0].equals("-ssh")) {
8443      for(int i = 1; i < args.length; ++i) {
8444        javax.swing.SwingUtilities.invokeLater(new Runnable() {
8445            @Override
8446            public void run() {
8447              SSHWindow ssh = new SSHWindow(args[1]);
8448              ssh.setLocationByPlatform(true);
8449              ssh.pack();
8450              ssh.setVisible(true);
8451            }
8452          });
8453      }
8454      return;
8455    }
8456    if(args.length == 2) {
8457      final MyPath leftPath = stringToMyPath(args[0]);
8458      final MyPath rightPath = stringToMyPath(args[1]);
8459      javax.swing.SwingUtilities.invokeLater(new Runnable() {
8460          @Override
8461          public void run() {
8462            PathCompare pathCompare = new FileBrowser().new PathCompare("Comparing", leftPath, rightPath);
8463            pathCompare.setLocationByPlatform(true);
8464            pathCompare.pack();
8465            pathCompare.setVisible(true);
8466            return;
8467          }
8468        });
8469      return;
8470    }
8471    if(args.length == 0) {
8472      javax.swing.SwingUtilities.invokeLater(new Runnable() {
8473          @Override
8474          public void run() {
8475            //Create and set up the window.
8476            JFrame frame = new FileBrowser().new Browser();
8477            frame.setLocationByPlatform(true);
8478            //Display the window.
8479            frame.pack();
8480            frame.setVisible(true);
8481          }
8482        });
8483    } else {
8484      for(final String s : args) {
8485        makeBrowser(stringToMyPath(s));
8486      }
8487    }
8488  }
8489} // class FileBrowser