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