public interface ConfigDao extends CrudRepository<Config, Long> {

    @Query("select c from Config c where c.id = 1")
    Config get();
} public interface HostDao extends CrudRepository<Host, Long> {

    @Query("select h from Host h where h.hostname = :name")
    Host getHostByName(@Param("name") String name);

    @Query("select h from Host h")
    List<Host> getAll();

    @Query("select h.hostname from Host h")
    List<String> getHostNames();
} @Entity
public class Config {
    @Id
    private long id = 1L;
    private boolean showHiddenFiles;
    private boolean savePasswords;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public boolean isShowHiddenFiles() {
        return showHiddenFiles;
    }

    public void setShowHiddenFiles(boolean showHiddenFiles) {
        this.showHiddenFiles = showHiddenFiles;
    }

    public boolean isSavePasswords() {
        return savePasswords;
    }

    public void setSavePasswords(boolean savePasswords) {
        this.savePasswords = savePasswords;
    }
} @Entity
public class Host implements Comparable<Host> {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String username;
    private String hostname;
    private String password;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getHostname() {
        return hostname;
    }

    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Host host = (Host) o;

        if (hostname != null ? !hostname.equals(host.hostname) : host.hostname != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = hostname != null ? hostname.hashCode() : 0;
        result = 31 * result;
        return result;
    }

    @Override
    public int compareTo(Host o) {
        return hostname.compareTo(o.getHostname());
    }
} @Component
public class Connection {
    private static final Logger logger = LoggerFactory.getLogger(Connection.class);
    private static final int timeout = 5000;
    private Session session;
    private ChannelSftp sftpChannel;
    private String remoteHost;
    private String user;
    private String password;
    @Autowired
    private ConfigDao configDao;

    @PreDestroy
    public void destroy() {
        disconnect();
    }

    public synchronized void processSymLink(Node node) {
        try {
            String symPath = this.sftpChannel.readlink(node.getPath());
            node.setLinkDest(symPath);
            File file = new File(symPath);
            if (file.isAbsolute()) {
                node.setFile(!this.sftpChannel.stat(symPath).isDir());
            }
            String absPath = new File(new File(node.getPath()).getParent(), symPath).getAbsolutePath();
            node.setFile(!this.sftpChannel.stat(absPath).isDir());

        } catch (SftpException e) {
            if (e.getMessage() != null && e.getMessage().contains("No such file")) {
                return;
            }
            logger.error("failed to check if dir '{}' is symlink", node.getPath(), e);
        }
    }

    public synchronized List<Node> getNodes(String path) {
        List<Node> nodes = new ArrayList<>();

        if ((session == null) || !session.isConnected()) {
            return nodes;
        }

        for (ChannelSftp.LsEntry entry : getFiles(path)) {
            Node node = new Node();
            nodes.add(node);
            node.setName(entry.getFilename());
            if (path.endsWith("/")) {
                node.setPath(path + entry.getFilename());
            } else {
                node.setPath(path + "/" + entry.getFilename());
            }

            if (entry.getAttrs().isLink()) {
                processSymLink(node);
            } else {
                node.setFile(!entry.getAttrs().isDir());
            }
        }
        Collections.sort(nodes);
        return nodes;
    }

    /**
     * remove path
     *
     * @param path absolute path
     */
    public synchronized void rm(String path) {
        sendCommand("rm -rf " + path);
    }

    public synchronized void mv(String src, String dest) {
        sendCommand("mv -f " + src + " " + dest);
    }

    /**
     * create new directory
     *
     * @param path path of new directory
     */
    public synchronized void mkdir(String path) {
        sendCommand("mkdir -p " + path);
    }

    public synchronized void sendCommand(String command) {

        List<Text> output = new ArrayList<>();
        output.add(JFTText.getRemoteHost(remoteHost));
        output.add(JFTText.textBlack(command));

        try {
            Channel channel = this.session.openChannel("exec");

            ((ChannelExec) channel).setCommand(command);
            channel.setInputStream(null);

            OutputStream out = new PipedOutputStream();
            channel.setOutputStream(out);

            OutputStream outErr = new PipedOutputStream();
            ((ChannelExec) channel).setErrStream(outErr);

            PipedInputStream pout = new PipedInputStream((PipedOutputStream) out);
            PipedInputStream poutErr = new PipedInputStream((PipedOutputStream) outErr);

            channel.connect(timeout);


            try (BufferedReader consoleOutput = new BufferedReader(
                    new InputStreamReader(pout, Charset.defaultCharset()))) {
                String s;
                while ((s = consoleOutput.readLine()) != null) {
                    output.add(JFTText.textBlack("\n" + s));
                }
            }

            try (BufferedReader consoleErr = new BufferedReader(
                    new InputStreamReader(poutErr, Charset.defaultCharset()))) {
                String s;
                while ((s = consoleErr.readLine()) != null) {
                    output.add(JFTText.textRed("\n" + s));
                }
            }

            channel.disconnect();
        } catch (IOException | JSchException e) {
            logger.error("failed to send command: {}", command, e);
        }

        OutputPanel.getInstance().printlnOutputLater(output);
    }

    private synchronized List<ChannelSftp.LsEntry> getFiles(String path) {
        final List<ChannelSftp.LsEntry> files = new LinkedList<>();

        if ((path == null) || path.isEmpty()) {
            return files;
        }

        final boolean showHiddenFiles = configDao.get().isShowHiddenFiles();

        try {
            sftpChannel.ls(path, entry -> {
                if (!showHiddenFiles) {
                    if (entry.getFilename().startsWith(".")) {
                        return ChannelSftp.LsEntrySelector.CONTINUE;
                    }
                } else if (entry.getFilename().equals(".")
                        || entry.getFilename().equals("..")) {
                    return ChannelSftp.LsEntrySelector.CONTINUE;
                }
                files.add(entry);
                return ChannelSftp.LsEntrySelector.CONTINUE;
            });
        } catch (SftpException e) {
            // ignore exception. usually happens because of permission denied
        }
        return files;
    }

    /**
     * Connect to remote host
     *
     * @param remoteHost
     * @param user
     * @param password
     * @return true if connected otherwise false
     */
    public synchronized ConnectionState connect(String remoteHost, String user, String password) {
        disconnect();

        this.remoteHost = remoteHost;
        this.user = user;
        this.password = password;

        JSch jsch = new JSch();

        try {
            session = jsch.getSession(user, remoteHost, 22);
            session.setConfig("StrictHostKeyChecking", "no");
            session.setPassword(password);
            session.connect(timeout);

            // create sftp channel:
            Channel channel = session.openChannel("sftp");
            channel.connect(timeout);
            sftpChannel = (ChannelSftp) channel;

        } catch (JSchException e) {
            logger.debug("failed to connect to {}", remoteHost, e);
            return new ConnectionState(e, "failed to connect to " + remoteHost);
        }
        return new ConnectionState();
    }

    public synchronized boolean isConnected() {
        if (session == null || !session.isConnected()) {
            return false;
        }
        return true;
    }

    /**
     * disconnect from remote host
     */
    public synchronized void disconnect() {
        if (sftpChannel != null) {
            sftpChannel.exit();
        }

        if (session != null && session.isConnected()) {
            session.disconnect();
        }
    }

    public synchronized String getUser() {
        return user;
    }

    public synchronized String getPassword() {
        return password;
    }

    public synchronized String getRemoteHost() {
        return remoteHost;
    }
} public class ConnectionState {
    private boolean success;
    private String msg;

    public ConnectionState() {
        success = true;
    }

    public ConnectionState(Exception e, String error) {
        success = false;
        msg = error;
        if (e.getMessage() != null) {
            msg = error + ": " + e.getMessage();
        }
        msg = msg + "\n";
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }
} @Component
public class LocalTerminal {
    private Thread thread;
    private TerminalPanel localTerminalPanel;
    private PtyProcess pty;

    public void connect() throws IOException {
        if (localTerminalPanel == null) {
            throw new IllegalStateException("local terminal panel is not set");
        }
        if (pty != null && pty.isAlive()) {
            return;
        }

        String[] cmd = {"/bin/bash", "-i"};
        Map<String, String> envs = new HashMap<>(System.getenv());
        envs.remove("TERM_PROGRAM"); // for OS X
        envs.put("TERM", "vt102");

        pty = PtyProcess.exec(cmd, envs, System.getProperty("user.home"));

        OutputStream os = pty.getOutputStream();
        InputStream is = pty.getInputStream();

        PrintStream printStream = new PrintStream(os, true);
        localTerminalPanel.setPrintStream(printStream);

        Runnable run = new TerminalWatcher(is, localTerminalPanel.getTextArea());
        thread = new Thread(run);
        thread.start();
    }

    @PreDestroy
    public void disconnect() {
        if (pty != null) {
            pty.destroy();
        }
        if (thread != null) {
            thread.interrupt();
        }
    }

    public void setLocalTerminalPanel(TerminalPanel localTerminalPanel) {
        this.localTerminalPanel = localTerminalPanel;
    }
} @Component
public class RemoteTerminal {
    private Session session;
    private Thread thread;
    private String key;

    public void connect(Connection connection, TerminalPanel remoteTerminalPanel) throws JSchException, IOException {
        String newKey = connection.getUser() + connection.getRemoteHost();
        if (newKey.equals(key)) {
            return;
        }
        key = newKey;

        disconnect();

        JSch jsch = new JSch();
        session = jsch.getSession(connection.getUser(), connection.getRemoteHost(), 22);
        session.setPassword(connection.getPassword());
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect(5000);

        ChannelShell channel = (ChannelShell) session.openChannel("shell");
        channel.setPtyType("vt102");

        OutputStream inputToChannel = channel.getOutputStream();
        PrintStream printStream = new PrintStream(inputToChannel, true);
        remoteTerminalPanel.setPrintStream(printStream);

        InputStream outFromChannel = channel.getInputStream();

        Runnable run = new TerminalWatcher(outFromChannel, remoteTerminalPanel.getTextArea());
        thread = new Thread(run);
        thread.start();

        channel.connect();
    }

    @PreDestroy
    public void disconnect() {
        if (session != null) {
            session.disconnect();
        }
        if (thread != null) {
            thread.interrupt();
        }
    }
} public class TerminalPanel {
    private static final Logger logger = LoggerFactory.getLogger(TerminalPanel.class);
    private TextArea textArea;


    public TerminalPanel() {
        textArea = new TextArea();
        textArea.setFont(Font.font("Monospaced", 14));
    }

    public void setPrintStream(PrintStream printStream) {

        textArea.addEventHandler(KeyEvent.KEY_RELEASED, Event::consume);

        textArea.addEventHandler(KeyEvent.KEY_TYPED, Event::consume);

        textArea.addEventHandler(KeyEvent.KEY_PRESSED,
                event -> {
                    byte[] code = TerminalUtils.getCode(event, TerminalPanel.this);
                    if (code != null) {
                        try {
                            printStream.write(code);
                        } catch (IOException e) {
                            logger.warn("failed to write to stream", e);
                        }
                    } else {
                        printStream.print(event.getText());
                    }

                    event.consume();
                }
        );
    }

    public TextArea getTextArea() {
        return textArea;
    }
} @Component
public class CommonTree {
    private static final Logger logger = LoggerFactory.getLogger(CommonTree.class);
    private static DataFormat dataFormat = new DataFormat("tree");
    @Autowired
    private Connection connection;
    private ExecutorService executorService = Executors.newCachedThreadPool();
    private OutputPanel outputPanel = OutputPanel.getInstance();

    public void expand(TreeView<Node> treeView) {
        List<TreeItem<Node>> items = new ArrayList<>(treeView.getSelectionModel().getSelectedItems());
        if (items.isEmpty() || items.size() > 1) {
            return;
        }

        TreeItem<Node> item = items.get(0);
        if (item.isExpanded()) {
            item.setExpanded(false);
        } else {
            treeView.getSelectionModel().clearSelection();
            item.setExpanded(true);
            treeView.getSelectionModel().select(item);
        }
    }

    public void refresh(TreeView<Node> treeView, Tree tree) {
        List<TreeItem<Node>> items = new ArrayList<>(treeView.getSelectionModel().getSelectedItems());
        if (items.isEmpty() || items.size() > 1) {
            return;
        }

        treeView.getSelectionModel().clearSelection();
        items.get(0).getChildren().setAll(tree.buildChildren(items.get(0)));
        treeView.getSelectionModel().select(items.get(0));
    }

    public void deleteSelectedItems(TreeView<Node> treeView, Tree tree) {
        List<TreeItem<Node>> items = new ArrayList<>(treeView.getSelectionModel().getSelectedItems());
        if (items.isEmpty()) {
            return;
        }

        treeView.getSelectionModel().clearSelection();

        TreeItem<Node> parent = null;

        Map<String, TreeItem<Node>> reducedItems = reduceByPaths(items);

        for (Map.Entry<String, TreeItem<Node>> item : reducedItems.entrySet()) {
            if (item.getValue().getParent() != null) {
                if (parent == null) {
                    parent = item.getValue().getParent();
                } else {
                    if (item.getValue().getParent().getValue().isParentOf(parent.getValue())) {
                        parent = item.getValue().getParent();
                    }
                }
            }
            if (tree.isLocal()) {
                File src = new File(item.getKey());
                if (src.isFile()) {
                    if (!src.delete()) {
                        logger.warn("cannot delete file {}", src.getAbsolutePath());
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("rm " +
                                src.getAbsolutePath() + " "), JFTText.failed());

                    } else {
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("rm " +
                                src.getAbsolutePath()));
                    }
                } else {
                    try {
                        FileUtils.deleteDirectory(src);
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("rm -rf " +
                                src.getAbsolutePath()));
                    } catch (IOException e) {
                        logger.warn("failed to remove dir", e);
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("rm -rf " +
                                src.getAbsolutePath() + " "), JFTText.failed());
                    }
                }
            } else {
                connection.rm(item.getKey());
            }
        }
        if (parent != null) {
            parent.getChildren().setAll(tree.buildChildren(parent));
        }
    }

    public void setDragDropEvent(NodeTreeCell cell, LocalTree localTree) {
        //Source:
        cell.setOnDragDetected(event -> {
            Dragboard db = cell.startDragAndDrop(TransferMode.ANY);
            db.setDragView(cell.getCurrentImage());

            ClipboardContent content = new ClipboardContent();

            List<Node> files = new ArrayList<>();
            ObservableList<TreeItem<Node>> selectedItems = cell.getTreeView().getSelectionModel().getSelectedItems();
            for (TreeItem<Node> node : selectedItems) {
                files.add(node.getValue());
            }

            content.put(dataFormat, files);
            db.setContent(content);

            event.consume();
        });

        //Target:
        cell.setOnDragOver(event -> {
            NodeTreeCell source = (NodeTreeCell) event.getGestureSource();

            //check that if target is file then source is not directory:
            boolean validNodes = !(cell.getItem().isFile() && !source.getItem().isFile());

            if (event.getGestureSource() != cell && validNodes) {
                event.acceptTransferModes(TransferMode.COPY);
            }
            event.consume();
        });

        //Target:
        cell.setOnDragEntered(event -> {
            if (event.getGestureSource() != cell) {
                cell.setStyle("-fx-background-color: green;");
            }
            event.consume();
        });

        //Target
        cell.setOnDragExited(event -> {
            cell.setStyle("");
            event.consume();
        });

        //Target
        cell.setOnDragDropped(event -> {
            Dragboard db = event.getDragboard();
            event.setDropCompleted(true);

            NodeTreeCell source = (NodeTreeCell) event.getGestureSource();
            NodeTreeCell target = (NodeTreeCell) event.getGestureTarget();

            @SuppressWarnings({"unchecked"})
            List<Node> files = (List<Node>) db.getContent(dataFormat);

            String targetPath = cell.getItem().getPath();

            OutputPanel outputPanel = OutputPanel.getInstance();

            if (source.isLocalTree() && target.isLocalTree()) {
                //Local to Local
                for (Node file : files) {
                    File src = new File(file.getPath());
                    File dest = new File(targetPath);

                    if (src.getParentFile().equals(dest)) {
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("cp " +
                                file.getPath() + " " + cell.getItem().getPath() + " "), JFTText.failed());
                        outputPanel.println(JFTText.textBlack("Source and destination are the same"));
                        continue;
                    }

                    if (LocalFileUtils.copy(src, dest)) {
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("cp " +
                                file.getPath() + " " + cell.getItem().getPath()));
                    } else {
                        outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("cp " +
                                file.getPath() + " " + cell.getItem().getPath() + " "), JFTText.failed());
                    }

                    cell.getTreeItem().getChildren().clear();
                    cell.getTreeItem().getChildren().addAll(localTree.buildChildren(cell.getTreeItem()));
                }
            } else if (source.isLocalTree() && !target.isLocalTree()) {
                //Local to Remote
                List<CommandCopy> commandCopies = getCommands(files, source.isLocalTree(), target.isLocalTree(), targetPath);

                for (CommandCopy commandCopy : commandCopies) {
                    CommandCopyExecution commandCopyExecution = new CommandCopyExecution(commandCopy, connection);
                    Future<Boolean> task = executorService.submit(commandCopyExecution);

                }
            } else if (!source.isLocalTree() && !target.isLocalTree()) {
                //Remote to Remote
                List<CommandCopy> commandCopies = getCommands(files, source.isLocalTree(), target.isLocalTree(), targetPath);

                for (CommandCopy commandCopy : commandCopies) {
                    connection.sendCommand(commandCopy.toString());
                }
            } else if (!source.isLocalTree() && target.isLocalTree()) {
                //Remote to Local
                List<CommandCopy> commandCopies = getCommands(files, source.isLocalTree(), target.isLocalTree(), targetPath);

                for (CommandCopy commandCopy : commandCopies) {
                    CommandCopyExecution commandCopyExecution = new CommandCopyExecution(commandCopy, connection);
                    Future<Boolean> task = executorService.submit(commandCopyExecution);
                }
            }
            //cell.getTreeView().getSelectionModel().select(cell.getTreeItem());
            event.consume();
        });

        //Source
        cell.setOnDragDone(event -> {
            event.consume();
        });
    }

    private List<CommandCopy> getCommands(List<Node> files, boolean isSourceLocal, boolean isTargetLocal, String targetPath) {
        List<String> sources = new ArrayList<>();
        for (Node file : files) {
            sources.add(file.getPath());
        }

        CommandCopyFactory commandCopyFactory = new CommandCopyFactory(connection);
        return commandCopyFactory.buildCommands(isSourceLocal, isTargetLocal, targetPath, sources);
    }

    private Map<String, TreeItem<Node>> reduceByPaths(List<TreeItem<Node>> items) {
        Map<String, TreeItem<Node>> map = new TreeMap<>();
        for (TreeItem<Node> item : items) {
            map.put(item.getValue().getPath(), item);
        }
        Map<String, TreeItem<Node>> found = new TreeMap<>();
        for (Map.Entry<String, TreeItem<Node>> path : map.entrySet()) {
            if (found.isEmpty()) {
                found.put(path.getKey(), path.getValue());
            } else {
                boolean add = true;
                for (String foundPath : found.keySet()) {
                    if (path.getKey().startsWith(foundPath)) {
                        add = false;
                        break;
                    }
                }
                if (add) {
                    found.put(path.getKey(), path.getValue());
                }
            }
        }

        return found;
    }

    @PreDestroy
    public void destroy() {
        executorService.shutdownNow();
    }
} public class CustomTreeViewSkin<T extends IndexedCell> extends TreeViewSkin<T> {

    public CustomTreeViewSkin(TreeView treeView) {
        super(treeView);
    }

    @Override
    protected VirtualFlow createVirtualFlow() {
        return new CustomVirtualFlow<T>();
    }
} @Component
public class LocalTree implements Tree {
    @Autowired
    private ConfigDao configDao;

    public TreeItem<Node> createRootNode() {
        Node node = new Node();
        node.setName("/");
        node.setFile(false);
        node.setPath("/");
        node.setLocal(true);
        return createNode(node);
    }

    @Override
    public boolean isLocal() {
        return true;
    }

    @Override
    public TreeItem<Node> createNode(Node node) {
        return new TreeItem<Node>(node) {
            private boolean isFirstTimeChildren = true;

            @Override
            public ObservableList<TreeItem<Node>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    super.getChildren().setAll(buildChildren(this));
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                return getValue().isFile();
            }
        };
    }

    @Override
    public ObservableList<TreeItem<Node>> buildChildren(TreeItem<Node> treeItem) {
        Node f = treeItem.getValue();
        if (f != null && !f.isFile()) {
            File[] files = new File(f.getPath()).listFiles();
            if (files != null) {
                ObservableList<TreeItem<Node>> children = FXCollections.observableArrayList();

                for (File childFile : files) {
                    if (!configDao.get().isShowHiddenFiles() && childFile.isHidden()) {
                        continue;
                    }
                    children.add(createNode(new Node(childFile)));
                }
                Collections.sort(children, (o1, o2) -> o1.getValue().compareTo(o2.getValue()));

                return children;
            }
        }

        return FXCollections.emptyObservableList();
    }
} public class Node implements Comparable<Node>, Serializable {
    private static final Logger logger = LoggerFactory.getLogger(Node.class);
    private static final long serialVersionUID = 1L;
    private String name;
    private String path;
    private String linkDest;
    private boolean isFile;
    private boolean isLocal;

    public Node() {
    }

    public Node(File file) {
        path = file.getAbsolutePath();
        isFile = file.isFile();
        name = file.getName();
        isLocal = true;

        try {
            if (Files.isSymbolicLink(file.toPath())) {
                linkDest = Files.readSymbolicLink(file.toPath()).toString();
            }
        } catch (IOException e) {
            logger.error("Cannot read symbolic link " + file, e);
        }
    }

    public boolean isParentOf(Node node) {
        if (node.getPath().startsWith(path) && !node.getPath().equals(path)) {
            return true;
        }
        return false;
    }


    public String getName() {
        if (linkDest == null) {
            return name;
        }
        return name + " -> " + linkDest;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isFile() {
        return isFile;
    }

    public void setFile(boolean file) {
        isFile = file;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getLinkDest() {
        return linkDest;
    }

    public void setLinkDest(String linkDest) {
        this.linkDest = linkDest;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Node node = (Node) o;

        if (isFile != node.isFile) {
            return false;
        }
        if (linkDest != null ? !linkDest.equals(node.linkDest) : node.linkDest != null) {
            return false;
        }
        if (name != null ? !name.equals(node.name) : node.name != null) {
            return false;
        }
        if (path != null ? !path.equals(node.path) : node.path != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (path != null ? path.hashCode() : 0);
        result = 31 * result + (isFile ? 1 : 0);
        result = 31 * result + (linkDest != null ? linkDest.hashCode() : 0);
        return result;
    }

    @Override
    public int compareTo(Node o) {
        if (!this.isFile && o.isFile) {
            return -1;
        } else if (this.isFile && !o.isFile) {
            return 1;
        }
        return this.name.compareTo(o.name);
    }

    @Override
    public String toString() {
        return getName();
    }

    public boolean isLocal() {
        return isLocal;
    }

    public void setLocal(boolean isLocal) {
        this.isLocal = isLocal;
    }
} public class NodeTreeCell extends TreeCell<Node> {
    private static final Image FOLDER_COLLAPSE_IMAGE = new Image(ClassLoader.getSystemResourceAsStream("folder.png"));
    private static final Image FOLDER_EXPAND_IMAGE = new Image(ClassLoader.getSystemResourceAsStream("folder-open.png"));
    private static final Image FILE_IMAGE = new Image(ClassLoader.getSystemResourceAsStream("file.png"));

    private ContextMenu contextFileMenu = new ContextMenu();
    private ContextMenu contextFolderMenu = new ContextMenu();
    private Connection connection;
    private OutputPanel outputPanel;
    private Image currentImage;
    private Tree tree;
    private CommonTree commonTree;

    public NodeTreeCell(Stage primaryStage, Connection connection, Tree tree, CommonTree commonTree) {
        this.connection = connection;
        this.outputPanel = OutputPanel.getInstance();
        this.tree = tree;
        this.commonTree = commonTree;

        MenuItem refreshMenuFile = new MenuItem("Refresh");
        refreshMenuFile.setOnAction((ActionEvent event) -> refreshItem());
        refreshMenuFile.setAccelerator(new KeyCodeCombination(KeyCode.F5));

        MenuItem refreshMenuFolder = new MenuItem("Refresh");
        refreshMenuFolder.setOnAction((ActionEvent event) -> refreshItem());
        refreshMenuFolder.setAccelerator(new KeyCodeCombination(KeyCode.F5));

        MenuItem deleteMenuFile = new MenuItem("Delete");
        deleteMenuFile.setOnAction((ActionEvent event) -> deleteItems());
        deleteMenuFile.setAccelerator(new KeyCodeCombination(KeyCode.DELETE));

        MenuItem deleteMenuFolder = new MenuItem("Delete");
        deleteMenuFolder.setOnAction((ActionEvent event) -> deleteItems());
        deleteMenuFolder.setAccelerator(new KeyCodeCombination(KeyCode.DELETE));

        MenuItem newFolderMenu = new MenuItem("New Folder");
        newFolderMenu.setOnAction((ActionEvent event) -> createNewFolderDialog(primaryStage));

        MenuItem renameMenuFile = new MenuItem("Rename");
        renameMenuFile.setOnAction((ActionEvent event) -> createRenameDialog(primaryStage));

        MenuItem renameMenuFolder = new MenuItem("Rename");
        renameMenuFolder.setOnAction((ActionEvent event) -> createRenameDialog(primaryStage));

        contextFolderMenu.getItems().addAll(newFolderMenu, refreshMenuFolder, deleteMenuFolder, renameMenuFolder);
        contextFileMenu.getItems().addAll(refreshMenuFile, deleteMenuFile, renameMenuFile);
    }

    private void createRenameDialog(Stage primaryStage) {
        Stage dialog = new Stage();
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.initOwner(primaryStage);

        VBox vbox = new VBox();
        vbox.setPadding(new Insets(10d));
        vbox.setSpacing(8d);
        Button btnOk = new Button("OK");
        Button btnCancel = new Button("Cancel");
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(10d, 10d, 10d, 10d));
        hbox.setSpacing(10d);
        hbox.setAlignment(Pos.CENTER);
        btnOk.setPrefWidth(80d);
        btnCancel.setPrefWidth(80d);
        hbox.getChildren().addAll(btnOk, btnCancel);

        TextField itemField = new TextField(getTreeItem().getValue().getName());

        vbox.getChildren().addAll(new Text("Please enter a new name:"), itemField, hbox);
        Scene myDialogScene = new Scene(vbox);

        dialog.setScene(myDialogScene);
        dialog.setHeight(150d);
        dialog.setWidth(300d);
        dialog.setTitle("Rename");

        double x = primaryStage.getX() + primaryStage.getWidth() / 2. - dialog.getWidth() / 2.;
        double y = primaryStage.getY() + primaryStage.getHeight() / 2. - dialog.getHeight() / 2.;

        dialog.setX(x);
        dialog.setY(y);

        itemField.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                renameItem(itemField.getText().trim(), dialog);
            }
        });

        btnOk.setOnAction(event1 -> {
            renameItem(itemField.getText().trim(), dialog);
        });

        btnCancel.setOnAction(event1 -> dialog.close());

        dialog.showAndWait();
    }

    private void createNewFolderDialog(Stage primaryStage) {
        Stage dialog = new Stage();
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.initOwner(primaryStage);

        VBox vbox = new VBox();
        vbox.setPadding(new Insets(10d));
        vbox.setSpacing(8d);
        Button btnOk = new Button("OK");
        Button btnCancel = new Button("Cancel");
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(10d, 10d, 10d, 10d));
        hbox.setSpacing(10d);
        hbox.setAlignment(Pos.CENTER);
        btnOk.setPrefWidth(80d);
        btnCancel.setPrefWidth(80d);
        hbox.getChildren().addAll(btnOk, btnCancel);

        TextField folderField = new TextField();

        vbox.getChildren().addAll(new Text("Please enter name:"), folderField, hbox);
        Scene myDialogScene = new Scene(vbox);

        dialog.setScene(myDialogScene);
        dialog.setHeight(150d);
        dialog.setWidth(300d);
        dialog.setTitle("New Folder");

        double x = primaryStage.getX() + primaryStage.getWidth() / 2. - dialog.getWidth() / 2.;
        double y = primaryStage.getY() + primaryStage.getHeight() / 2. - dialog.getHeight() / 2.;

        dialog.setX(x);
        dialog.setY(y);

        folderField.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                createFolder(folderField, dialog);
            }
        });
        btnOk.setOnAction(event1 -> {
            createFolder(folderField, dialog);
        });

        btnCancel.setOnAction(event1 -> dialog.close());

        dialog.showAndWait();
    }

    private void renameItem(String newName, Stage dialog) {
        if (newName.isEmpty()) {
            dialog.close();
            return;
        }

        getTreeView().getSelectionModel().clearSelection();

        TreeItem<Node> parent = getTreeItem().getParent();

        File newFile = new File(getTreeItem().getParent().getValue().getPath(), newName);
        File oldFile = new File(getItem().getPath());

        if (isLocalTree()) {
            if (!oldFile.renameTo(newFile)) {
                outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("failed rename  " + oldFile.getAbsolutePath() + " to " +
                        newFile.getAbsolutePath()), JFTText.failed());
                dialog.close();
                return;
            }
        } else {
            connection.mv(oldFile.getAbsolutePath(), newFile.getAbsolutePath());
        }

        Node node = getItem();
        node.setName(newName);
        super.startEdit();
        super.commitEdit(node);

        dialog.close();

        parent.getChildren().setAll(tree.buildChildren(parent));
    }

    private void createFolder(TextField folderField, Stage dialog) {
        String folderName = folderField.getText().trim();
        if (folderName.isEmpty()) {
            dialog.close();
            return;
        }

        File folder = new File(getItem().getPath(), folderName);
        String folderPath = folder.getAbsolutePath();

        getTreeView().getSelectionModel().clearSelection();
        if (tree.isLocal()) {
            if (folder.mkdirs()) {
                outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("mkdir -p " + folderPath));
                getTreeItem().getChildren().setAll(tree.buildChildren(getTreeItem()));
            } else {
                outputPanel.println(JFTText.getLocalHost(), JFTText.textBlack("mkdir -p " + folderPath + " "), JFTText.failed());
            }
        } else {
            connection.mkdir(folderPath);
            getTreeItem().getChildren().setAll(tree.buildChildren(getTreeItem()));
        }

        getTreeView().getSelectionModel().select(getTreeItem());
        dialog.close();
    }

    private void deleteItems() {
        commonTree.deleteSelectedItems(getTreeView(), tree);
    }

    @Override
    protected void updateItem(Node item, boolean empty) {
        super.updateItem(item, empty);

        if (!empty && item != null) {
            if (getTreeItem().getValue().isFile()) {
                setContextMenu(contextFileMenu);
            } else {
                setContextMenu(contextFolderMenu);
            }

            setText(item.getName());
            if (item.isFile()) {
                currentImage = FILE_IMAGE;
            } else {
                if (getTreeItem().isExpanded()) {
                    currentImage = FOLDER_EXPAND_IMAGE;
                } else {
                    currentImage = FOLDER_COLLAPSE_IMAGE;
                }
            }
            setGraphic(new ImageView(currentImage));

            if (item.getLinkDest() != null) {
                setTextFill(Color.BLUE);
            } else {
                setTextFill(Color.BLACK);
            }

        } else {
            setText(null);
            setGraphic(null);
        }
    }

    public boolean isLocalTree() {
        return tree.isLocal();
    }

    public void refreshTree() {
        getTreeView().getSelectionModel().clearSelection();
        TreeItem<Node> root = getTreeView().getRoot();
        root.getChildren().setAll(tree.buildChildren(root));
    }

    private void refreshItem() {
        commonTree.refresh(getTreeView(), tree);
    }

    public Image getCurrentImage() {
        return currentImage;
    }
} @Component
public class RemoteTree implements Tree {
    @Autowired
    private Connection connection;

    public TreeItem<Node> createRootNote() {
        Node node = new Node();
        node.setFile(false);
        node.setPath("/");
        node.setName("/");
        return createNode(node);
    }

    @Override
    public boolean isLocal() {
        return false;
    }

    @Override
    public TreeItem<Node> createNode(Node node) {
        return new TreeItem<Node>(node) {
            private boolean isFirstTimeChildren = true;

            @Override
            public ObservableList<TreeItem<Node>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    super.getChildren().setAll(buildChildren(this));
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                return getValue().isFile();
            }
        };
    }

    @Override
    public ObservableList<TreeItem<Node>> buildChildren(TreeItem<Node> treeItem) {
        Node f = treeItem.getValue();
        if (f != null && !f.isFile()) {
            List<Node> files = connection.getNodes(f.getPath());
            if (files != null) {
                ObservableList<TreeItem<Node>> children = FXCollections.observableArrayList();

                for (Node childFile : files) {
                    children.add(createNode(childFile));
                }

                return children;
            }
        }

        return FXCollections.emptyObservableList();
    }
}
import com.google.common.base.Strings;
import com.jcraft.jsch.JSchException;
import com.sun.javafx.scene.control.skin.TreeViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import org.jftclient.config.dao.ConfigDao;
import org.jftclient.config.dao.HostDao;
import org.jftclient.config.domain.Config;
import org.jftclient.config.domain.Host;
import org.jftclient.ssh.Connection;
import org.jftclient.ssh.ConnectionState;
import org.jftclient.terminal.LocalTerminal;
import org.jftclient.terminal.RemoteTerminal;
import org.jftclient.terminal.TerminalPanel;
import org.jftclient.tree.CommonTree;
import org.jftclient.tree.CustomTreeViewSkin;
import org.jftclient.tree.LocalTree;
import org.jftclient.tree.Node;
import org.jftclient.tree.NodeTreeCell;
import org.jftclient.tree.RemoteTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.awt.*;
import java.io.IOException;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ComboBox;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.PasswordField;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Skin;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToolBar;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
package org.jftclient;






/**
 * @author sergei.malafeev
 */
public class JFTClient extends Application {
    private static final Logger logger = LoggerFactory.getLogger(JFTClient.class);

    private ComboBox<String> hostField = new ComboBox<>();
    private TextField userField = new TextField();
    private PasswordField passwordField = new PasswordField();
    private NodeTreeCell cellLocal;
    private NodeTreeCell cellRemote;
    private TitledPane remotePane = new TitledPane();
    private Stage primaryStage;
    private ConfigDao configDao;