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