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